diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml
index 8f59d3543..357dfd7b5 100644
--- a/config/detekt/baseline.xml
+++ b/config/detekt/baseline.xml
@@ -55,6 +55,7 @@
ImplicitDefaultLocale:TimeIntervalUtil.kt$TimeIntervalUtil$String.format("%02d:00 \u2014 %02d:00", i, i + 1)
InvalidPackageDeclaration:HandleActions.kt$package org.hyperskill.app.core.view
LambdaParameterInRestartableEffect:OnComposableShownFirstTime.kt$block
+ LargeClass:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer : StateReducer
LargeClass:StepQuizReducer.kt$StepQuizReducer : StateReducer
LongMethod:AppReducer.kt$AppReducer$private fun handleFetchAppStartupConfigSuccess( state: State, message: Message.FetchAppStartupConfigSuccess ): ReducerResult
LongMethod:ChallengeCard.kt$@Composable fun ChallengeCard( viewState: ChallengeWidgetViewState, onNewMessage: (Message) -> Unit )
@@ -67,6 +68,7 @@
LongMethod:ProblemOfDayCardFormDelegate.kt$ProblemOfDayCardFormDelegate$fun render( dateFormatter: SharedDateFormatter, binding: LayoutProblemOfTheDayCardBinding, state: HomeFeature.ProblemOfDayState, areProblemsLimited: Boolean )
LongMethod:ProfileBadges.kt$@Composable fun ProfileBadges( viewState: BadgesViewState, windowWidthSizeClass: WindowWidthSizeClass, onBadgeClick: (BadgeKind) -> Unit, onExpandButtonClick: (ProfileFeature.Message.BadgesVisibilityButton) -> Unit, modifier: Modifier = Modifier )
LongMethod:ProfileSettingsDialogFragment.kt$ProfileSettingsDialogFragment$override fun onViewCreated(view: View, savedInstanceState: Bundle?)
+ LongMethod:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun createInitialCodeBlocks(step: Step): List<CodeBlock>
LongMethod:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleDeleteButtonClicked( state: State ): StepQuizCodeBlanksReducerResult?
LongMethod:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleSpaceButtonClicked( state: State ): StepQuizCodeBlanksReducerResult?
LongMethod:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleSuggestionClicked( state: State, message: Message.SuggestionClicked ): StepQuizCodeBlanksReducerResult?
@@ -204,6 +206,7 @@
ModifierWithoutDefault:BadgeImage.kt$modifier
NestedBlockDepth:AuthSocialWebViewClient.kt$AuthSocialWebViewClient$override fun shouldOverrideUrlLoading( view: WebView?, request: WebResourceRequest? ): Boolean
NestedBlockDepth:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun setCodeBlockIsActive(codeBlock: CodeBlock, isActive: Boolean): CodeBlock
+ NestedBlockDepth:StepQuizCodeBlanksViewStateMapper.kt$StepQuizCodeBlanksViewStateMapper$private fun mapContentState( state: StepQuizCodeBlanksFeature.State.Content ): StepQuizCodeBlanksViewState.Content
PreviewPublic:BadgeCard.kt$BadgeCardPreview
PreviewPublic:BadgeCard.kt$LastLevelBadgeCardPreview
PreviewPublic:BadgeGrid.kt$PhoneBadgeGridPreview
@@ -248,6 +251,7 @@
ReturnCount:SearchReducer.kt$SearchReducer$private fun handleSearchResultsItemClickedMessage( state: State, message: Message.SearchResultsItemClicked ): SearchReducerResult?
ReturnCount:SharedDateFormatter.kt$SharedDateFormatter$fun formatTimeDistance(millis: Long): String
ReturnCount:StateExtentions.kt$internal fun ChallengeWidgetFeature.State.Content.setCurrentChallengeIntervalProgressAsCompleted(): Challenge?
+ ReturnCount:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleDecreaseIndentLevelButtonClicked( state: State ): StepQuizCodeBlanksReducerResult?
ReturnCount:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleDeleteButtonClicked( state: State ): StepQuizCodeBlanksReducerResult?
ReturnCount:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleSpaceButtonClicked( state: State ): StepQuizCodeBlanksReducerResult?
ReturnCount:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleSuggestionClicked( state: State, message: Message.SuggestionClicked ): StepQuizCodeBlanksReducerResult?
diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj
index 79c449b63..3d14111a9 100644
--- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj
+++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj
@@ -149,6 +149,8 @@
2C2FD622281920B1004E7AF6 /* SentryInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2FD621281920B1004E7AF6 /* SentryInfo.swift */; };
2C2FD62428192123004E7AF6 /* BundlePropertyListDeserializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2FD62328192123004E7AF6 /* BundlePropertyListDeserializer.swift */; };
2C306A0E29B4590C0068FF4F /* StageImplementFeatureViewStateKsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C306A0D29B4590C0068FF4F /* StageImplementFeatureViewStateKsExtensions.swift */; };
+ 2C308B1F2C86E29400E85D14 /* StepQuizCodeBlanksCodeBlocksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C308B1E2C86E29400E85D14 /* StepQuizCodeBlanksCodeBlocksView.swift */; };
+ 2C308B212C86E4C200E85D14 /* StepQuizCodeBlanksActionButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C308B202C86E4C200E85D14 /* StepQuizCodeBlanksActionButtonsView.swift */; };
2C3100532AB194A200C09BFB /* StepQuizParsonsViewDataMapperCodeContentCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3100522AB194A200C09BFB /* StepQuizParsonsViewDataMapperCodeContentCache.swift */; };
2C32374D2837F7190062CAF6 /* Images.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C32374C2837F7190062CAF6 /* Images.swift */; };
2C32375328380C340062CAF6 /* NavigationToolbarInfoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C32375228380C340062CAF6 /* NavigationToolbarInfoItem.swift */; };
@@ -481,6 +483,7 @@
2CBD1917291D392400F5FB0B /* UIView+Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBD1916291D392400F5FB0B /* UIView+Animations.swift */; };
2CBD1919291D399500F5FB0B /* UIKitBounceButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBD1918291D399500F5FB0B /* UIKitBounceButton.swift */; };
2CBD191D291D3BF400F5FB0B /* UIKitRoundedRectangleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBD191C291D3BF400F5FB0B /* UIKitRoundedRectangleButton.swift */; };
+ 2CBEE4C72C870059004486E8 /* StepQuizCodeBlanksIfStatementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBEE4C62C870059004486E8 /* StepQuizCodeBlanksIfStatementView.swift */; };
2CBFB94A28897DBB0044D1BA /* StepQuizCodeFullScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBFB94928897DBB0044D1BA /* StepQuizCodeFullScreenView.swift */; };
2CBFB94C28897DD70044D1BA /* StepQuizCodeFullScreenAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CBFB94B28897DD70044D1BA /* StepQuizCodeFullScreenAssembly.swift */; };
2CC4AAF1280DB513002276A0 /* WebOAuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CC4AAF0280DB513002276A0 /* WebOAuthService.swift */; };
@@ -521,8 +524,8 @@
2CD48D8E28586B6F00CFCC4A /* StepQuizViewDataMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD48D8D28586B6F00CFCC4A /* StepQuizViewDataMapper.swift */; };
2CD4EDF92B79D51E0091F0B2 /* View+SafeAreaInset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD4EDF82B79D51E0091F0B2 /* View+SafeAreaInset.swift */; };
2CD4EDFB2B79D74B0091F0B2 /* TransparentBlurView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD4EDFA2B79D74B0091F0B2 /* TransparentBlurView.swift */; };
- 2CD67CA12C452B2400240C17 /* StepQuizCodeBlanksBlankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD67CA02C452B2400240C17 /* StepQuizCodeBlanksBlankView.swift */; };
- 2CD67CA32C452DED00240C17 /* StepQuizCodeBlanksOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD67CA22C452DED00240C17 /* StepQuizCodeBlanksOptionView.swift */; };
+ 2CD67CA12C452B2400240C17 /* StepQuizCodeBlanksCodeBlockChildBlankView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD67CA02C452B2400240C17 /* StepQuizCodeBlanksCodeBlockChildBlankView.swift */; };
+ 2CD67CA32C452DED00240C17 /* StepQuizCodeBlanksCodeBlockChildTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD67CA22C452DED00240C17 /* StepQuizCodeBlanksCodeBlockChildTextView.swift */; };
2CD7C2D32BFDDC5500DFD5BE /* TopicCompletedModalSpacebotAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD7C2D22BFDDC5500DFD5BE /* TopicCompletedModalSpacebotAvatarView.swift */; };
2CDA9838294432C900ADE539 /* SkeletonCircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDA9837294432C900ADE539 /* SkeletonCircleView.swift */; };
2CDA98412944512D00ADE539 /* ProfileSkeletonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CDA98402944512D00ADE539 /* ProfileSkeletonView.swift */; };
@@ -946,6 +949,8 @@
2C2FD621281920B1004E7AF6 /* SentryInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfo.swift; sourceTree = ""; };
2C2FD62328192123004E7AF6 /* BundlePropertyListDeserializer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundlePropertyListDeserializer.swift; sourceTree = ""; };
2C306A0D29B4590C0068FF4F /* StageImplementFeatureViewStateKsExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StageImplementFeatureViewStateKsExtensions.swift; sourceTree = ""; };
+ 2C308B1E2C86E29400E85D14 /* StepQuizCodeBlanksCodeBlocksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksCodeBlocksView.swift; sourceTree = ""; };
+ 2C308B202C86E4C200E85D14 /* StepQuizCodeBlanksActionButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksActionButtonsView.swift; sourceTree = ""; };
2C3100522AB194A200C09BFB /* StepQuizParsonsViewDataMapperCodeContentCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizParsonsViewDataMapperCodeContentCache.swift; sourceTree = ""; };
2C32374C2837F7190062CAF6 /* Images.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Images.swift; sourceTree = ""; };
2C32375228380C340062CAF6 /* NavigationToolbarInfoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationToolbarInfoItem.swift; sourceTree = ""; };
@@ -1283,6 +1288,7 @@
2CBD1916291D392400F5FB0B /* UIView+Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Animations.swift"; sourceTree = ""; };
2CBD1918291D399500F5FB0B /* UIKitBounceButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitBounceButton.swift; sourceTree = ""; };
2CBD191C291D3BF400F5FB0B /* UIKitRoundedRectangleButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitRoundedRectangleButton.swift; sourceTree = ""; };
+ 2CBEE4C62C870059004486E8 /* StepQuizCodeBlanksIfStatementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksIfStatementView.swift; sourceTree = ""; };
2CBFB94928897DBB0044D1BA /* StepQuizCodeFullScreenView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeFullScreenView.swift; sourceTree = ""; };
2CBFB94B28897DD70044D1BA /* StepQuizCodeFullScreenAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeFullScreenAssembly.swift; sourceTree = ""; };
2CC4AAF0280DB513002276A0 /* WebOAuthService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebOAuthService.swift; sourceTree = ""; };
@@ -1323,8 +1329,8 @@
2CD48D8D28586B6F00CFCC4A /* StepQuizViewDataMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizViewDataMapper.swift; sourceTree = ""; };
2CD4EDF82B79D51E0091F0B2 /* View+SafeAreaInset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+SafeAreaInset.swift"; sourceTree = ""; };
2CD4EDFA2B79D74B0091F0B2 /* TransparentBlurView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparentBlurView.swift; sourceTree = ""; };
- 2CD67CA02C452B2400240C17 /* StepQuizCodeBlanksBlankView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksBlankView.swift; sourceTree = ""; };
- 2CD67CA22C452DED00240C17 /* StepQuizCodeBlanksOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksOptionView.swift; sourceTree = ""; };
+ 2CD67CA02C452B2400240C17 /* StepQuizCodeBlanksCodeBlockChildBlankView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksCodeBlockChildBlankView.swift; sourceTree = ""; };
+ 2CD67CA22C452DED00240C17 /* StepQuizCodeBlanksCodeBlockChildTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksCodeBlockChildTextView.swift; sourceTree = ""; };
2CD7C2D22BFDDC5500DFD5BE /* TopicCompletedModalSpacebotAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopicCompletedModalSpacebotAvatarView.swift; sourceTree = ""; };
2CDA9837294432C900ADE539 /* SkeletonCircleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SkeletonCircleView.swift; sourceTree = ""; };
2CDA98402944512D00ADE539 /* ProfileSkeletonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSkeletonView.swift; sourceTree = ""; };
@@ -2146,6 +2152,52 @@
path = UIKit;
sourceTree = "";
};
+ 2C308B192C86E08F00E85D14 /* CodeBlocks */ = {
+ isa = PBXGroup;
+ children = (
+ 2C308B1E2C86E29400E85D14 /* StepQuizCodeBlanksCodeBlocksView.swift */,
+ 2C308B1B2C86E17300E85D14 /* ActionButtons */,
+ 2CBEE4C42C86E955004486E8 /* Children */,
+ 2CBEE4C52C87003A004486E8 /* Conditions */,
+ 2C308B1D2C86E20E00E85D14 /* Print */,
+ 2C308B1C2C86E20600E85D14 /* Variable */,
+ );
+ path = CodeBlocks;
+ sourceTree = "";
+ };
+ 2C308B1A2C86E09D00E85D14 /* Suggestions */ = {
+ isa = PBXGroup;
+ children = (
+ 2C677D012C4A3F860019AF03 /* StepQuizCodeBlanksSuggestionsView.swift */,
+ );
+ path = Suggestions;
+ sourceTree = "";
+ };
+ 2C308B1B2C86E17300E85D14 /* ActionButtons */ = {
+ isa = PBXGroup;
+ children = (
+ 2C008A262C5771350041D8BB /* StepQuizCodeBlanksActionButton.swift */,
+ 2C308B202C86E4C200E85D14 /* StepQuizCodeBlanksActionButtonsView.swift */,
+ );
+ path = ActionButtons;
+ sourceTree = "";
+ };
+ 2C308B1C2C86E20600E85D14 /* Variable */ = {
+ isa = PBXGroup;
+ children = (
+ 2C3B84E72C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift */,
+ );
+ path = Variable;
+ sourceTree = "";
+ };
+ 2C308B1D2C86E20E00E85D14 /* Print */ = {
+ isa = PBXGroup;
+ children = (
+ 2CB3BC552C46171000F5354F /* StepQuizCodeBlanksPrintInstructionView.swift */,
+ );
+ path = Print;
+ sourceTree = "";
+ };
2C323750283808300062CAF6 /* View */ = {
isa = PBXGroup;
children = (
@@ -3574,6 +3626,24 @@
path = SwiftUI;
sourceTree = "";
};
+ 2CBEE4C42C86E955004486E8 /* Children */ = {
+ isa = PBXGroup;
+ children = (
+ 2CD67CA02C452B2400240C17 /* StepQuizCodeBlanksCodeBlockChildBlankView.swift */,
+ 2CD67CA22C452DED00240C17 /* StepQuizCodeBlanksCodeBlockChildTextView.swift */,
+ 2CE7AA1A2C7C4255000ABCD7 /* StepQuizCodeBlanksCodeBlockChildView.swift */,
+ );
+ path = Children;
+ sourceTree = "";
+ };
+ 2CBEE4C52C87003A004486E8 /* Conditions */ = {
+ isa = PBXGroup;
+ children = (
+ 2CBEE4C62C870059004486E8 /* StepQuizCodeBlanksIfStatementView.swift */,
+ );
+ path = Conditions;
+ sourceTree = "";
+ };
2CBFB94828897D970044D1BA /* StepQuizCodeFullScreen */ = {
isa = PBXGroup;
children = (
@@ -3741,14 +3811,9 @@
2CD67C9D2C451B0200240C17 /* Views */ = {
isa = PBXGroup;
children = (
- 2C008A262C5771350041D8BB /* StepQuizCodeBlanksActionButton.swift */,
- 2CD67CA02C452B2400240C17 /* StepQuizCodeBlanksBlankView.swift */,
- 2CE7AA1A2C7C4255000ABCD7 /* StepQuizCodeBlanksCodeBlockChildView.swift */,
- 2CD67CA22C452DED00240C17 /* StepQuizCodeBlanksOptionView.swift */,
- 2CB3BC552C46171000F5354F /* StepQuizCodeBlanksPrintInstructionView.swift */,
- 2C677D012C4A3F860019AF03 /* StepQuizCodeBlanksSuggestionsView.swift */,
- 2C3B84E72C637AE100FE9D5C /* StepQuizCodeBlanksVariableInstructionView.swift */,
2C84E70B2C47BAB6002EE787 /* StepQuizCodeBlanksView.swift */,
+ 2C308B192C86E08F00E85D14 /* CodeBlocks */,
+ 2C308B1A2C86E09D00E85D14 /* Suggestions */,
);
path = Views;
sourceTree = "";
@@ -5057,6 +5122,7 @@
E9D2D675284E0B30000757AC /* StepQuizMatchingView.swift in Sources */,
2CBC97CD2A555AA20078E445 /* StageImplementProjectCompletedModalView.swift in Sources */,
2CC95C0E2A4EBB970036C73E /* ProjectLevelAvatarView.swift in Sources */,
+ 2CBEE4C72C870059004486E8 /* StepQuizCodeBlanksIfStatementView.swift in Sources */,
2C93C2D4292E5905004D1861 /* HyperskillSentryBreadcrumb+SentryBreadcrumb.swift in Sources */,
2C306A0E29B4590C0068FF4F /* StageImplementFeatureViewStateKsExtensions.swift in Sources */,
2CA7B892289329C600A789EF /* UIView+FindViewController.swift in Sources */,
@@ -5187,7 +5253,7 @@
E9A022AE291D0E3F004317DB /* TopicsRepetitionsCardView.swift in Sources */,
2C5CBBE32948F4B600113007 /* StepQuizSQLViewModel.swift in Sources */,
E9F923F628A2633D00C065A7 /* WelcomeView.swift in Sources */,
- 2CD67CA32C452DED00240C17 /* StepQuizCodeBlanksOptionView.swift in Sources */,
+ 2CD67CA32C452DED00240C17 /* StepQuizCodeBlanksCodeBlockChildTextView.swift in Sources */,
E9BDB4052A7BE1E30069EF98 /* BadgeImageView.swift in Sources */,
2C7FE8A52B98261600F09615 /* PurchaseManager.swift in Sources */,
2C106D9928C1CE6E004FA584 /* SendEmailFeedbackController.swift in Sources */,
@@ -5304,7 +5370,7 @@
2CB9537E2AF2474100CA64BA /* StepQuizHintsFeatureStateKsExtensions.swift in Sources */,
2C963BCA2812D3550036DD53 /* ProfileSettingsView.swift in Sources */,
2C772E7D28ABB4E500A58758 /* AppleIDSocialAuthSDKProvider.swift in Sources */,
- 2CD67CA12C452B2400240C17 /* StepQuizCodeBlanksBlankView.swift in Sources */,
+ 2CD67CA12C452B2400240C17 /* StepQuizCodeBlanksCodeBlockChildBlankView.swift in Sources */,
2C963BCC2812D9330036DD53 /* ProfileSettingsAssembly.swift in Sources */,
E9470C6B29810AB7008ACF9A /* StepQuizOutputProtocol.swift in Sources */,
2C079687285CFFF500EE0487 /* StepQuizSortingAssembly.swift in Sources */,
@@ -5358,6 +5424,7 @@
2CF34F9D2C340DB60054477E /* CommentsSkeletonView.swift in Sources */,
E9D537D02A71056100F21828 /* ProfileBadgesGridItemView.swift in Sources */,
2CB0ADEE2B04AD6D0089D557 /* ChallengeWidgetViewModel.swift in Sources */,
+ 2C308B212C86E4C200E85D14 /* StepQuizCodeBlanksActionButtonsView.swift in Sources */,
2CACBCC22B7A3E4E006D3AB2 /* UsersInterviewWidgetAssembly.swift in Sources */,
E9CC6C0729893F2200D8D070 /* StepQuizInputProtocol.swift in Sources */,
2C96743728882A0C0091B6C9 /* StepQuizCodeDetailsView.swift in Sources */,
@@ -5485,6 +5552,7 @@
E94BB0482A9DF9DD00736B7C /* StepQuizParsonsView.swift in Sources */,
E99CCB0B287E945300898BBF /* HomeViewModel.swift in Sources */,
2C7CB6782ADFD0E8006F78DA /* StepQuizFillBlanksViewDataMapper.swift in Sources */,
+ 2C308B1F2C86E29400E85D14 /* StepQuizCodeBlanksCodeBlocksView.swift in Sources */,
2C0FA879292FD73400A37636 /* ProfileSettingsFeatureViewStateKsExtensions.swift in Sources */,
2C1061AA285C3C3300EBD614 /* StepQuizChoiceAssembly.swift in Sources */,
2CF72AA828477E0600E1C192 /* StepQuizTableRowView.swift in Sources */,
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/StepQuizViewModel.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/StepQuizViewModel.swift
index 908b44506..a862a41c2 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/StepQuizViewModel.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuiz/StepQuizViewModel.swift
@@ -285,6 +285,14 @@ extension StepQuizViewModel: StepQuizCodeBlanksOutputProtocol {
)
)
}
+
+ func handleStepQuizCodeBlanksDidTapDecreaseIndentLevel() {
+ onNewMessage(
+ StepQuizFeatureMessageStepQuizCodeBlanksMessage(
+ message: StepQuizCodeBlanksFeatureMessageDecreaseIndentLevelButtonClicked()
+ )
+ )
+ }
}
// MARK: - StepQuizViewModel: StepQuizProblemOnboardingModalViewControllerDelegate -
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksOutputProtocol.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksOutputProtocol.swift
index 12cd5561a..363b034df 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksOutputProtocol.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksOutputProtocol.swift
@@ -11,4 +11,5 @@ protocol StepQuizCodeBlanksOutputProtocol: AnyObject {
func handleStepQuizCodeBlanksDidTapDelete()
func handleStepQuizCodeBlanksDidTapEnter()
func handleStepQuizCodeBlanksDidTapSpace()
+ func handleStepQuizCodeBlanksDidTapDecreaseIndentLevel()
}
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksViewModel.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksViewModel.swift
index 860a13547..379445416 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksViewModel.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/StepQuizCodeBlanksViewModel.swift
@@ -48,4 +48,10 @@ final class StepQuizCodeBlanksViewModel {
impactFeedbackGenerator.triggerFeedback()
moduleOutput?.handleStepQuizCodeBlanksDidTapSpace()
}
+
+ @MainActor
+ func doDecreaseIndentLevelAction() {
+ impactFeedbackGenerator.triggerFeedback()
+ moduleOutput?.handleStepQuizCodeBlanksDidTapDecreaseIndentLevel()
+ }
}
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksActionButton.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/ActionButtons/StepQuizCodeBlanksActionButton.swift
similarity index 80%
rename from iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksActionButton.swift
rename to iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/ActionButtons/StepQuizCodeBlanksActionButton.swift
index 524220ab5..00744ba2e 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksActionButton.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/ActionButtons/StepQuizCodeBlanksActionButton.swift
@@ -62,6 +62,19 @@ extension StepQuizCodeBlanksActionButton {
action: action
)
}
+
+ static func decreaseIndentLevel(action: @escaping () -> Void) -> StepQuizCodeBlanksActionButton {
+ StepQuizCodeBlanksActionButton(
+ appearance: .init(
+ padding: LayoutInsets(
+ horizontal: LayoutInsets.smallInset,
+ vertical: 5.5
+ )
+ ),
+ imageSystemName: "arrow.left.to.line",
+ action: action
+ )
+ }
}
#if DEBUG
@@ -71,12 +84,14 @@ extension StepQuizCodeBlanksActionButton {
StepQuizCodeBlanksActionButton.delete(action: {})
StepQuizCodeBlanksActionButton.enter(action: {})
StepQuizCodeBlanksActionButton.space(action: {})
+ StepQuizCodeBlanksActionButton.decreaseIndentLevel(action: {})
}
HStack {
StepQuizCodeBlanksActionButton.delete(action: {})
StepQuizCodeBlanksActionButton.enter(action: {})
StepQuizCodeBlanksActionButton.space(action: {})
+ StepQuizCodeBlanksActionButton.decreaseIndentLevel(action: {})
}
.disabled(true)
}
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/ActionButtons/StepQuizCodeBlanksActionButtonsView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/ActionButtons/StepQuizCodeBlanksActionButtonsView.swift
new file mode 100644
index 000000000..f6a041701
--- /dev/null
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/ActionButtons/StepQuizCodeBlanksActionButtonsView.swift
@@ -0,0 +1,63 @@
+import SwiftUI
+
+struct StepQuizCodeBlanksActionButtonsView: View {
+ let isDeleteButtonEnabled: Bool
+ let isSpaceButtonHidden: Bool
+ let isDecreaseIndentLevelButtonHidden: Bool
+
+ let onSpaceTap: () -> Void
+ let onDeleteTap: () -> Void
+ let onEnterTap: () -> Void
+ let onDecreaseIndentLevelTap: () -> Void
+
+ var body: some View {
+ HStack(spacing: LayoutInsets.defaultInset) {
+ Spacer()
+
+ if !isDecreaseIndentLevelButtonHidden {
+ StepQuizCodeBlanksActionButton
+ .decreaseIndentLevel(action: onDecreaseIndentLevelTap)
+ }
+
+ if !isSpaceButtonHidden {
+ StepQuizCodeBlanksActionButton
+ .space(action: onSpaceTap)
+ }
+
+ StepQuizCodeBlanksActionButton
+ .delete(action: onDeleteTap)
+ .disabled(!isDeleteButtonEnabled)
+
+ StepQuizCodeBlanksActionButton
+ .enter(action: onEnterTap)
+ }
+ .padding(.horizontal)
+ }
+}
+
+#if DEBUG
+#Preview {
+ VStack {
+ StepQuizCodeBlanksActionButtonsView(
+ isDeleteButtonEnabled: false,
+ isSpaceButtonHidden: false,
+ isDecreaseIndentLevelButtonHidden: false,
+ onSpaceTap: {},
+ onDeleteTap: {},
+ onEnterTap: {},
+ onDecreaseIndentLevelTap: {}
+ )
+
+ StepQuizCodeBlanksActionButtonsView(
+ isDeleteButtonEnabled: true,
+ isSpaceButtonHidden: true,
+ isDecreaseIndentLevelButtonHidden: true,
+ onSpaceTap: {},
+ onDeleteTap: {},
+ onEnterTap: {},
+ onDecreaseIndentLevelTap: {}
+ )
+ }
+ .padding()
+}
+#endif
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksBlankView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Children/StepQuizCodeBlanksCodeBlockChildBlankView.swift
similarity index 77%
rename from iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksBlankView.swift
rename to iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Children/StepQuizCodeBlanksCodeBlockChildBlankView.swift
index e0418f71e..508a77bab 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksBlankView.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Children/StepQuizCodeBlanksCodeBlockChildBlankView.swift
@@ -1,6 +1,6 @@
import SwiftUI
-struct StepQuizCodeBlanksBlankView: View {
+struct StepQuizCodeBlanksCodeBlockChildBlankView: View {
var width: CGFloat = 208
var height: CGFloat = 48
@@ -17,7 +17,7 @@ struct StepQuizCodeBlanksBlankView: View {
}
}
-extension StepQuizCodeBlanksBlankView {
+extension StepQuizCodeBlanksCodeBlockChildBlankView {
init(style: Style, isActive: Bool) {
let size = style.size
self.init(width: size.width, height: size.height, isActive: isActive)
@@ -41,8 +41,8 @@ extension StepQuizCodeBlanksBlankView {
#if DEBUG
#Preview {
VStack {
- StepQuizCodeBlanksBlankView(style: .small, isActive: true)
- StepQuizCodeBlanksBlankView(style: .large, isActive: false)
+ StepQuizCodeBlanksCodeBlockChildBlankView(style: .small, isActive: true)
+ StepQuizCodeBlanksCodeBlockChildBlankView(style: .large, isActive: false)
}
}
#endif
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksOptionView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Children/StepQuizCodeBlanksCodeBlockChildTextView.swift
similarity index 69%
rename from iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksOptionView.swift
rename to iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Children/StepQuizCodeBlanksCodeBlockChildTextView.swift
index 5d011735d..832fd731f 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksOptionView.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Children/StepQuizCodeBlanksCodeBlockChildTextView.swift
@@ -1,6 +1,6 @@
import SwiftUI
-extension StepQuizCodeBlanksOptionView {
+extension StepQuizCodeBlanksCodeBlockChildTextView {
enum Appearance {
static let insets = LayoutInsets(horizontal: 12, vertical: LayoutInsets.smallInset)
static let minWidth: CGFloat = 48
@@ -9,7 +9,7 @@ extension StepQuizCodeBlanksOptionView {
}
}
-struct StepQuizCodeBlanksOptionView: View {
+struct StepQuizCodeBlanksCodeBlockChildTextView: View {
let text: String
let isActive: Bool
@@ -31,9 +31,9 @@ struct StepQuizCodeBlanksOptionView: View {
#if DEBUG
#Preview {
VStack {
- StepQuizCodeBlanksOptionView(text: "print", isActive: false)
- StepQuizCodeBlanksOptionView(text: "There is a cat on the keyboard, it is true", isActive: true)
- StepQuizCodeBlanksOptionView(text: "Typing messages out of the blue", isActive: true)
+ StepQuizCodeBlanksCodeBlockChildTextView(text: "print", isActive: false)
+ StepQuizCodeBlanksCodeBlockChildTextView(text: "There is a cat on the keyboard, it is true", isActive: true)
+ StepQuizCodeBlanksCodeBlockChildTextView(text: "Typing messages out of the blue", isActive: true)
}
}
#endif
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksCodeBlockChildView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Children/StepQuizCodeBlanksCodeBlockChildView.swift
similarity index 74%
rename from iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksCodeBlockChildView.swift
rename to iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Children/StepQuizCodeBlanksCodeBlockChildView.swift
index ef8b7d51f..6f45335c4 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksCodeBlockChildView.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Children/StepQuizCodeBlanksCodeBlockChildView.swift
@@ -18,9 +18,9 @@ struct StepQuizCodeBlanksCodeBlockChildView: View {
child: StepQuizCodeBlanksViewStateCodeBlockChildItem
) -> some View {
if let value = child.value {
- StepQuizCodeBlanksOptionView(text: value, isActive: child.isActive)
+ StepQuizCodeBlanksCodeBlockChildTextView(text: value, isActive: child.isActive)
} else {
- StepQuizCodeBlanksBlankView(style: .small, isActive: child.isActive)
+ StepQuizCodeBlanksCodeBlockChildBlankView(style: .small, isActive: child.isActive)
}
}
}
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Conditions/StepQuizCodeBlanksIfStatementView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Conditions/StepQuizCodeBlanksIfStatementView.swift
new file mode 100644
index 000000000..3def9b2cc
--- /dev/null
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Conditions/StepQuizCodeBlanksIfStatementView.swift
@@ -0,0 +1,73 @@
+import shared
+import SwiftUI
+
+struct StepQuizCodeBlanksIfStatementView: View {
+ let ifStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemIfStatement
+
+ let onChildTap: (StepQuizCodeBlanksViewStateCodeBlockChildItem) -> Void
+
+ var body: some View {
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(alignment: .center, spacing: LayoutInsets.smallInset) {
+ Text("if")
+ .font(StepQuizCodeBlanksAppearance.blankFont)
+ .foregroundColor(StepQuizCodeBlanksAppearance.blankTextColor)
+
+ ForEach(ifStatementItem.children, id: \.id) { child in
+ StepQuizCodeBlanksCodeBlockChildView(child: child, action: onChildTap)
+ }
+
+ Text(":")
+ .font(StepQuizCodeBlanksAppearance.blankFont)
+ .foregroundColor(StepQuizCodeBlanksAppearance.blankTextColor)
+ }
+ .padding(.horizontal, LayoutInsets.defaultInset)
+ .padding(.vertical, LayoutInsets.smallInset)
+ .background(Color(ColorPalette.violet400Alpha7))
+ .cornerRadius(StepQuizCodeBlanksAppearance.cornerRadius)
+ .padding(.horizontal)
+ }
+ .scrollBounceBehaviorBasedOnSize(axes: .horizontal)
+ }
+}
+
+#if DEBUG
+#Preview {
+ VStack {
+ StepQuizCodeBlanksIfStatementView(
+ ifStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemIfStatement(
+ id: 0,
+ indentLevel: 0,
+ children: [
+ StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: nil)
+ ]
+ ),
+ onChildTap: { _ in }
+ )
+
+ StepQuizCodeBlanksIfStatementView(
+ ifStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemIfStatement(
+ id: 0,
+ indentLevel: 0,
+ children: [
+ StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: "x")
+ ]
+ ),
+ onChildTap: { _ in }
+ )
+
+ StepQuizCodeBlanksIfStatementView(
+ ifStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemIfStatement(
+ id: 0,
+ indentLevel: 0,
+ children: [
+ StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: false, value: "x"),
+ StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 1, isActive: true, value: nil)
+ ]
+ ),
+ onChildTap: { _ in }
+ )
+ }
+ .padding()
+}
+#endif
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksPrintInstructionView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Print/StepQuizCodeBlanksPrintInstructionView.swift
similarity index 94%
rename from iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksPrintInstructionView.swift
rename to iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Print/StepQuizCodeBlanksPrintInstructionView.swift
index ab0cb80e3..c634638d9 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksPrintInstructionView.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Print/StepQuizCodeBlanksPrintInstructionView.swift
@@ -37,6 +37,7 @@ struct StepQuizCodeBlanksPrintInstructionView: View {
StepQuizCodeBlanksPrintInstructionView(
printItem: StepQuizCodeBlanksViewStateCodeBlockItemPrint(
id: 0,
+ indentLevel: 0,
children: [.init(id: 0, isActive: false, value: "")]
),
onChildTap: { _ in }
@@ -44,6 +45,7 @@ struct StepQuizCodeBlanksPrintInstructionView: View {
StepQuizCodeBlanksPrintInstructionView(
printItem: StepQuizCodeBlanksViewStateCodeBlockItemPrint(
id: 0,
+ indentLevel: 0,
children: [.init(id: 0, isActive: true, value: "")]
),
onChildTap: { _ in }
@@ -51,6 +53,7 @@ struct StepQuizCodeBlanksPrintInstructionView: View {
StepQuizCodeBlanksPrintInstructionView(
printItem: StepQuizCodeBlanksViewStateCodeBlockItemPrint(
id: 0,
+ indentLevel: 0,
children: [.init(id: 0, isActive: true, value: "There is a cat on the keyboard, it is true")]
),
onChildTap: { _ in }
@@ -58,6 +61,7 @@ struct StepQuizCodeBlanksPrintInstructionView: View {
StepQuizCodeBlanksPrintInstructionView(
printItem: StepQuizCodeBlanksViewStateCodeBlockItemPrint(
id: 0,
+ indentLevel: 0,
children: [.init(id: 0, isActive: false, value: "There is a cat on the keyboard, it is true")]
),
onChildTap: { _ in }
@@ -66,6 +70,7 @@ struct StepQuizCodeBlanksPrintInstructionView: View {
StepQuizCodeBlanksPrintInstructionView(
printItem: StepQuizCodeBlanksViewStateCodeBlockItemPrint(
id: 0,
+ indentLevel: 0,
children: [
.init(id: 0, isActive: false, value: "x"),
.init(id: 1, isActive: true, value: "")
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/StepQuizCodeBlanksCodeBlocksView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/StepQuizCodeBlanksCodeBlocksView.swift
new file mode 100644
index 000000000..79f8d394f
--- /dev/null
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/StepQuizCodeBlanksCodeBlocksView.swift
@@ -0,0 +1,89 @@
+import shared
+import SwiftUI
+
+struct StepQuizCodeBlanksCodeBlocksView: View {
+ let state: StepQuizCodeBlanksViewStateContent
+
+ let onCodeBlockTap: (StepQuizCodeBlanksViewStateCodeBlockItem) -> Void
+ let onCodeBlockChildTap: (
+ StepQuizCodeBlanksViewStateCodeBlockItem,
+ StepQuizCodeBlanksViewStateCodeBlockChildItem
+ ) -> Void
+
+ let onSpaceTap: () -> Void
+ let onDeleteTap: () -> Void
+ let onEnterTap: () -> Void
+ let onDecreaseIndentLevelTap: () -> Void
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: LayoutInsets.smallInset) {
+ ForEach(state.codeBlocks, id: \.id_) { codeBlock in
+ HStack(spacing: 0) {
+ Spacer()
+ .frame(width: LayoutInsets.defaultInset * CGFloat(codeBlock.indentLevel))
+
+ buildCodeBlockView(
+ codeBlock: codeBlock,
+ onChildTap: { codeBlockChild in
+ onCodeBlockChildTap(codeBlock, codeBlockChild)
+ }
+ )
+ .onTapGesture {
+ onCodeBlockTap(codeBlock)
+ }
+ }
+ }
+
+ if !state.isActionButtonsHidden {
+ StepQuizCodeBlanksActionButtonsView(
+ isDeleteButtonEnabled: state.isDeleteButtonEnabled,
+ isSpaceButtonHidden: state.isSpaceButtonHidden,
+ isDecreaseIndentLevelButtonHidden: state.isDecreaseIndentLevelButtonHidden,
+ onSpaceTap: onSpaceTap,
+ onDeleteTap: onDeleteTap,
+ onEnterTap: onEnterTap,
+ onDecreaseIndentLevelTap: onDecreaseIndentLevelTap
+ )
+ }
+ }
+ .padding(.vertical, LayoutInsets.defaultInset)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(BackgroundView())
+ }
+
+ @ViewBuilder
+ private func buildCodeBlockView(
+ codeBlock: StepQuizCodeBlanksViewStateCodeBlockItem,
+ onChildTap: @escaping (StepQuizCodeBlanksViewStateCodeBlockChildItem) -> Void
+ ) -> some View {
+ switch StepQuizCodeBlanksViewStateCodeBlockItemKs(codeBlock) {
+ case .blank(let blankItem):
+ StepQuizCodeBlanksCodeBlockChildBlankView(
+ style: .large,
+ isActive: blankItem.isActive
+ )
+ .padding(.horizontal)
+ case .print(let printItem):
+ StepQuizCodeBlanksPrintInstructionView(
+ printItem: printItem,
+ onChildTap: onChildTap
+ )
+ case .variable(let variableItem):
+ StepQuizCodeBlanksVariableInstructionView(
+ variableItem: variableItem,
+ onChildTap: onChildTap
+ )
+ case .ifStatement(let ifStatementItem):
+ StepQuizCodeBlanksIfStatementView(
+ ifStatementItem: ifStatementItem,
+ onChildTap: onChildTap
+ )
+ }
+ }
+}
+
+extension StepQuizCodeBlanksCodeBlocksView: Equatable {
+ static func == (lhs: StepQuizCodeBlanksCodeBlocksView, rhs: StepQuizCodeBlanksCodeBlocksView) -> Bool {
+ lhs.state.isEqual(rhs)
+ }
+}
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksVariableInstructionView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Variable/StepQuizCodeBlanksVariableInstructionView.swift
similarity index 96%
rename from iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksVariableInstructionView.swift
rename to iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Variable/StepQuizCodeBlanksVariableInstructionView.swift
index c595c99db..7d1217b7f 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksVariableInstructionView.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Variable/StepQuizCodeBlanksVariableInstructionView.swift
@@ -38,6 +38,7 @@ struct StepQuizCodeBlanksVariableInstructionView: View {
StepQuizCodeBlanksVariableInstructionView(
variableItem: StepQuizCodeBlanksViewStateCodeBlockItemVariable(
id: 0,
+ indentLevel: 0,
children: [
StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: nil),
StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 1, isActive: false, value: nil)
@@ -49,6 +50,7 @@ struct StepQuizCodeBlanksVariableInstructionView: View {
StepQuizCodeBlanksVariableInstructionView(
variableItem: StepQuizCodeBlanksViewStateCodeBlockItemVariable(
id: 0,
+ indentLevel: 0,
children: [
StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: false, value: "fruit_a"),
StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 1, isActive: true, value: nil)
@@ -60,6 +62,7 @@ struct StepQuizCodeBlanksVariableInstructionView: View {
StepQuizCodeBlanksVariableInstructionView(
variableItem: StepQuizCodeBlanksViewStateCodeBlockItemVariable(
id: 0,
+ indentLevel: 0,
children: [
StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: false, value: "fruit_a"),
StepQuizCodeBlanksViewStateCodeBlockChildItem(
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksView.swift
index 5e3e8ecfd..804758741 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksView.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksView.swift
@@ -23,12 +23,16 @@ struct StepQuizCodeBlanksView: View {
titleView
Divider()
- codeBlocksView(
- codeBlocks: contentState.codeBlocks,
- isDeleteButtonEnabled: contentState.isDeleteButtonEnabled,
- isSpaceButtonHidden: contentState.isSpaceButtonHidden,
- isActionButtonsHidden: contentState.isActionButtonsHidden
+ StepQuizCodeBlanksCodeBlocksView(
+ state: contentState,
+ onCodeBlockTap: viewModel.doCodeBlockMainAction(_:),
+ onCodeBlockChildTap: viewModel.doCodeBlockChildMainAction(codeBlock:codeBlockChild:),
+ onSpaceTap: viewModel.doSpaceAction,
+ onDeleteTap: viewModel.doDeleteAction,
+ onEnterTap: viewModel.doEnterAction,
+ onDecreaseIndentLevelTap: viewModel.doDecreaseIndentLevelAction
)
+ .equatable()
Divider()
StepQuizCodeBlanksSuggestionsView(
@@ -36,6 +40,7 @@ struct StepQuizCodeBlanksView: View {
isAnimationEffectActive: contentState.isSuggestionsHighlightEffectActive,
onSuggestionTap: viewModel.doSuggestionMainAction(_:)
)
+ .equatable()
Divider()
}
.padding(.horizontal, -LayoutInsets.defaultInset)
@@ -52,78 +57,6 @@ struct StepQuizCodeBlanksView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.background(BackgroundView())
}
-
- @MainActor
- private func codeBlocksView(
- codeBlocks: [StepQuizCodeBlanksViewStateCodeBlockItem],
- isDeleteButtonEnabled: Bool,
- isSpaceButtonHidden: Bool,
- isActionButtonsHidden: Bool
- ) -> some View {
- VStack(alignment: .leading, spacing: LayoutInsets.smallInset) {
- ForEach(codeBlocks, id: \.id_) { codeBlock in
- switch StepQuizCodeBlanksViewStateCodeBlockItemKs(codeBlock) {
- case .blank(let blankItem):
- StepQuizCodeBlanksBlankView(
- style: .large,
- isActive: blankItem.isActive
- )
- .padding(.horizontal)
- .onTapGesture {
- viewModel.doCodeBlockMainAction(codeBlock)
- }
- case .print(let printItem):
- StepQuizCodeBlanksPrintInstructionView(
- printItem: printItem,
- onChildTap: { codeBlockChild in
- viewModel.doCodeBlockChildMainAction(
- codeBlock: codeBlock,
- codeBlockChild: codeBlockChild
- )
- }
- )
- .onTapGesture {
- viewModel.doCodeBlockMainAction(codeBlock)
- }
- case .variable(let variableItem):
- StepQuizCodeBlanksVariableInstructionView(
- variableItem: variableItem,
- onChildTap: { codeBlockChild in
- viewModel.doCodeBlockChildMainAction(
- codeBlock: codeBlock,
- codeBlockChild: codeBlockChild
- )
- }
- )
- .onTapGesture {
- viewModel.doCodeBlockMainAction(codeBlock)
- }
- }
- }
-
- if !isActionButtonsHidden {
- HStack(spacing: LayoutInsets.defaultInset) {
- Spacer()
-
- if !isSpaceButtonHidden {
- StepQuizCodeBlanksActionButton
- .space(action: viewModel.doSpaceAction)
- }
-
- StepQuizCodeBlanksActionButton
- .delete(action: viewModel.doDeleteAction)
- .disabled(!isDeleteButtonEnabled)
-
- StepQuizCodeBlanksActionButton
- .enter(action: viewModel.doEnterAction)
- }
- .padding(.horizontal)
- }
- }
- .padding(.vertical, LayoutInsets.defaultInset)
- .frame(maxWidth: .infinity, alignment: .leading)
- .background(BackgroundView())
- }
}
extension StepQuizCodeBlanksView: Equatable {
@@ -138,10 +71,13 @@ extension StepQuizCodeBlanksView: Equatable {
StepQuizCodeBlanksView(
viewStateKs: .content(
StepQuizCodeBlanksViewStateContent(
- codeBlocks: [StepQuizCodeBlanksViewStateCodeBlockItemBlank(id: 0, isActive: true)],
+ codeBlocks: [
+ StepQuizCodeBlanksViewStateCodeBlockItemBlank(id: 0, indentLevel: 0, isActive: true)
+ ],
suggestions: [Suggestion.Print()],
isDeleteButtonEnabled: true,
isSpaceButtonHidden: true,
+ isDecreaseIndentLevelButtonHidden: true,
onboardingState: StepQuizCodeBlanksFeatureOnboardingStateUnavailable()
)
),
@@ -161,6 +97,7 @@ extension StepQuizCodeBlanksView: Equatable {
codeBlocks: [
StepQuizCodeBlanksViewStateCodeBlockItemPrint(
id: 0,
+ indentLevel: 0,
children: [
StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: nil)
]
@@ -172,6 +109,7 @@ extension StepQuizCodeBlanksView: Equatable {
],
isDeleteButtonEnabled: false,
isSpaceButtonHidden: true,
+ isDecreaseIndentLevelButtonHidden: true,
onboardingState: StepQuizCodeBlanksFeatureOnboardingStateUnavailable()
)
),
@@ -191,6 +129,7 @@ extension StepQuizCodeBlanksView: Equatable {
codeBlocks: [
StepQuizCodeBlanksViewStateCodeBlockItemPrint(
id: 0,
+ indentLevel: 0,
children: [
StepQuizCodeBlanksViewStateCodeBlockChildItem(
id: 0,
@@ -201,6 +140,7 @@ extension StepQuizCodeBlanksView: Equatable {
),
StepQuizCodeBlanksViewStateCodeBlockItemPrint(
id: 1,
+ indentLevel: 0,
children: [
StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: nil)
]
@@ -212,6 +152,7 @@ extension StepQuizCodeBlanksView: Equatable {
],
isDeleteButtonEnabled: false,
isSpaceButtonHidden: true,
+ isDecreaseIndentLevelButtonHidden: true,
onboardingState: StepQuizCodeBlanksFeatureOnboardingStateUnavailable()
)
),
diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksSuggestionsView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/Suggestions/StepQuizCodeBlanksSuggestionsView.swift
similarity index 79%
rename from iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksSuggestionsView.swift
rename to iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/Suggestions/StepQuizCodeBlanksSuggestionsView.swift
index b14c313c5..090929573 100644
--- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/StepQuizCodeBlanksSuggestionsView.swift
+++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/Suggestions/StepQuizCodeBlanksSuggestionsView.swift
@@ -21,7 +21,7 @@ struct StepQuizCodeBlanksSuggestionsView: View {
onSuggestionTap(suggestion)
},
label: {
- StepQuizCodeBlanksOptionView(
+ StepQuizCodeBlanksCodeBlockChildTextView(
text: suggestion.text,
isActive: true
)
@@ -31,7 +31,7 @@ struct StepQuizCodeBlanksSuggestionsView: View {
)
.pulseEffect(
shape: RoundedRectangle(
- cornerRadius: StepQuizCodeBlanksOptionView.Appearance.cornerRadius
+ cornerRadius: StepQuizCodeBlanksCodeBlockChildTextView.Appearance.cornerRadius
),
isActive: isAnimationEffectActive
)
@@ -42,7 +42,7 @@ struct StepQuizCodeBlanksSuggestionsView: View {
// Preserve height to avoid layout jumps
if suggestions.isEmpty {
- StepQuizCodeBlanksOptionView(text: "", isActive: false)
+ StepQuizCodeBlanksCodeBlockChildTextView(text: "", isActive: false)
.hidden()
}
}
@@ -50,6 +50,13 @@ struct StepQuizCodeBlanksSuggestionsView: View {
}
}
+extension StepQuizCodeBlanksSuggestionsView: Equatable {
+ static func == (lhs: StepQuizCodeBlanksSuggestionsView, rhs: StepQuizCodeBlanksSuggestionsView) -> Bool {
+ lhs.isAnimationEffectActive == rhs.isAnimationEffectActive &&
+ lhs.suggestions.map(\.text) == rhs.suggestions.map(\.text)
+ }
+}
+
#if DEBUG
#Preview {
VStack {
diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt
index 87f2f6d9e..8b0a7e01a 100644
--- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt
+++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt
@@ -21,6 +21,7 @@ enum class HyperskillAnalyticTarget(val targetName: String) {
DELETE("delete"),
ENTER("enter"),
SPACE("space"),
+ DECREASE_INDENT_LEVEL("decrease_indent_level"),
DONE("done"),
YES("yes"),
NO("no"),
diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksClickedDecreaseIndentLevelHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksClickedDecreaseIndentLevelHyperskillAnalyticEvent.kt
new file mode 100644
index 000000000..9875f2f6c
--- /dev/null
+++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/analytic/StepQuizCodeBlanksClickedDecreaseIndentLevelHyperskillAnalyticEvent.kt
@@ -0,0 +1,41 @@
+package org.hyperskill.app.step_quiz_code_blanks.domain.analytic
+
+import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction
+import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent
+import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart
+import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute
+import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import ru.nobird.app.core.model.mapOfNotNull
+
+/**
+ * Represents click on the "Decrease indent level" button in the code block analytic event.
+ *
+ * JSON payload:
+ * ```
+ * {
+ * "route": "/learn/step/1",
+ * "action": "click",
+ * "part": "code_blanks",
+ * "target": "decrease_indent_level",
+ * "context":
+ * {
+ * "code_block": "Print(isActive=true, suggestions=[ConstantString(text=suggestion)], selectedSuggestion=null)"
+ * }
+ * }
+ * ```
+ *
+ * @see HyperskillAnalyticEvent
+ */
+class StepQuizCodeBlanksClickedDecreaseIndentLevelHyperskillAnalyticEvent(
+ route: HyperskillAnalyticRoute,
+ codeBlock: CodeBlock?
+) : HyperskillAnalyticEvent(
+ route = route,
+ action = HyperskillAnalyticAction.CLICK,
+ part = HyperskillAnalyticPart.CODE_BLANKS,
+ target = HyperskillAnalyticTarget.DECREASE_INDENT_LEVEL,
+ context = mapOfNotNull(
+ StepQuizCodeBlanksAnalyticParams.PARAM_CODE_BLOCK to codeBlock?.analyticRepresentation
+ )
+)
\ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt
index 8faf3aced..241028bbd 100644
--- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt
+++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/CodeBlock.kt
@@ -1,12 +1,15 @@
package org.hyperskill.app.step_quiz_code_blanks.domain.model
import org.hyperskill.app.core.utils.indexOfFirstOrNull
+import ru.nobird.app.core.model.cast
sealed class CodeBlock {
companion object;
internal abstract val isActive: Boolean
+ internal abstract val indentLevel: Int
+
internal abstract val suggestions: List
internal abstract val children: List
@@ -21,8 +24,15 @@ sealed class CodeBlock {
internal fun activeChildIndex(): Int? =
children.indexOfFirstOrNull { it.isActive }
+ internal fun areAllChildrenUnselected(): Boolean =
+ children.all { it is CodeBlockChild.SelectSuggestion && it.selectedSuggestion == null }
+
+ internal fun hasAnySelectedChild(): Boolean =
+ children.any { it is CodeBlockChild.SelectSuggestion && it.selectedSuggestion != null }
+
internal data class Blank(
override val isActive: Boolean,
+ override val indentLevel: Int = 0,
override val suggestions: List
) : CodeBlock() {
override val children: List = emptyList()
@@ -34,6 +44,7 @@ sealed class CodeBlock {
}
internal data class Print(
+ override val indentLevel: Int = 0,
override val children: List
) : CodeBlock() {
override val isActive: Boolean = false
@@ -45,6 +56,7 @@ sealed class CodeBlock {
override fun toReplyString(): String =
buildString {
+ append(buildIndentString(indentLevel))
append("print(")
append(joinChildrenToReplyString(children))
append(")")
@@ -55,6 +67,7 @@ sealed class CodeBlock {
}
internal data class Variable(
+ override val indentLevel: Int = 0,
override val children: List
) : CodeBlock() {
val name: CodeBlockChild.SelectSuggestion?
@@ -72,6 +85,7 @@ sealed class CodeBlock {
override fun toReplyString(): String =
buildString {
+ append(buildIndentString(indentLevel))
append(name?.toReplyString() ?: "")
append(" = ")
append(joinChildrenToReplyString(values))
@@ -80,8 +94,34 @@ sealed class CodeBlock {
override fun toString(): String =
"Variable(children=$children)"
}
+
+ internal data class IfStatement(
+ override val indentLevel: Int = 0,
+ override val children: List
+ ) : CodeBlock() {
+ override val isActive: Boolean = false
+
+ override val suggestions: List = emptyList()
+
+ override val analyticRepresentation: String
+ get() = "IfStatement(children=$children)"
+
+ override fun toReplyString(): String =
+ buildString {
+ append(buildIndentString(indentLevel))
+ append("if ")
+ append(joinChildrenToReplyString(children))
+ append(":")
+ }
+
+ override fun toString(): String =
+ "IfStatement(children=$children)"
+ }
}
+internal fun CodeBlock.Companion.buildIndentString(indentLevel: Int): String =
+ "\t".repeat(indentLevel)
+
internal fun CodeBlock.Companion.joinChildrenToReplyString(children: List): String =
buildString {
children.forEachIndexed { index, child ->
@@ -100,4 +140,20 @@ internal fun CodeBlock.Companion.joinChildrenToReplyString(children: List): CodeBlock =
+ when (this) {
+ is CodeBlock.Blank -> this
+ is CodeBlock.Print -> copy(children = children.cast())
+ is CodeBlock.Variable -> copy(children = children.cast())
+ is CodeBlock.IfStatement -> copy(children = children.cast())
+ }
+
+internal fun CodeBlock.updatedIndentLevel(indentLevel: Int): CodeBlock =
+ when (this) {
+ is CodeBlock.Blank -> copy(indentLevel = indentLevel)
+ is CodeBlock.Print -> copy(indentLevel = indentLevel)
+ is CodeBlock.Variable -> copy(indentLevel = indentLevel)
+ is CodeBlock.IfStatement -> copy(indentLevel = indentLevel)
}
\ No newline at end of file
diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/Suggestion.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/Suggestion.kt
index 219daf9b4..0d95eaa5d 100644
--- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/Suggestion.kt
+++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/domain/model/Suggestion.kt
@@ -19,6 +19,13 @@ sealed class Suggestion {
"Variable(text='$text')"
}
+ data object IfStatement : Suggestion() {
+ override val text: String = "if"
+
+ override val analyticRepresentation: String =
+ "IfStatement(text='$text')"
+ }
+
data class ConstantString(
override val text: String
) : Suggestion() {
diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeature.kt
index 0e1c8f402..8cc1fe4e3 100644
--- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeature.kt
+++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeature.kt
@@ -29,6 +29,8 @@ object StepQuizCodeBlanksFeature {
val codeBlocks: List,
val onboardingState: OnboardingState = OnboardingState.Unavailable
) : State {
+ companion object;
+
internal val codeBlanksStringsSuggestions: List =
step.codeBlanksStringsSuggestions()
@@ -58,6 +60,7 @@ object StepQuizCodeBlanksFeature {
data object DeleteButtonClicked : Message
data object EnterButtonClicked : Message
data object SpaceButtonClicked : Message
+ data object DecreaseIndentLevelButtonClicked : Message
}
internal sealed interface InternalMessage : Message {
diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt
index 8c7884ffb..31f62faa4 100644
--- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt
+++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducer.kt
@@ -5,6 +5,7 @@ import org.hyperskill.app.step.domain.model.Step
import org.hyperskill.app.step.domain.model.StepRoute
import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent
import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent
+import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedDecreaseIndentLevelHyperskillAnalyticEvent
import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedDeleteHyperskillAnalyticEvent
import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedEnterHyperskillAnalyticEvent
import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedSpaceHyperskillAnalyticEvent
@@ -12,6 +13,8 @@ import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlan
import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.updatedChildren
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.updatedIndentLevel
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.Action
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.InternalAction
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.InternalMessage
@@ -36,6 +39,7 @@ class StepQuizCodeBlanksReducer(
Message.DeleteButtonClicked -> handleDeleteButtonClicked(state)
Message.EnterButtonClicked -> handleEnterButtonClicked(state)
Message.SpaceButtonClicked -> handleSpaceButtonClicked(state)
+ Message.DecreaseIndentLevelButtonClicked -> handleDecreaseIndentLevelButtonClicked(state)
} ?: (state to emptySet())
private fun initialize(
@@ -60,27 +64,28 @@ class StepQuizCodeBlanksReducer(
}
val activeCodeBlockIndex = state.activeCodeBlockIndex()
+ val activeCodeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] }
val actions = setOf(
InternalAction.LogAnalyticEvent(
StepQuizCodeBlanksClickedSuggestionHyperskillAnalyticEvent(
route = stepRoute.analyticRoute,
- codeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] },
+ codeBlock = activeCodeBlock,
suggestion = message.suggestion
)
)
)
- if (activeCodeBlockIndex == null) {
+ if (activeCodeBlock == null) {
return state to actions
}
- val activeCodeBlock = state.codeBlocks[activeCodeBlockIndex]
val newCodeBlock =
when (activeCodeBlock) {
is CodeBlock.Blank -> when (message.suggestion) {
Suggestion.Print ->
CodeBlock.Print(
+ indentLevel = activeCodeBlock.indentLevel,
children = listOf(
CodeBlockChild.SelectSuggestion(
isActive = true,
@@ -92,6 +97,7 @@ class StepQuizCodeBlanksReducer(
)
Suggestion.Variable ->
CodeBlock.Variable(
+ indentLevel = activeCodeBlock.indentLevel,
children = listOf(
CodeBlockChild.SelectSuggestion(
isActive = true,
@@ -105,20 +111,35 @@ class StepQuizCodeBlanksReducer(
)
)
)
+ Suggestion.IfStatement ->
+ CodeBlock.IfStatement(
+ indentLevel = activeCodeBlock.indentLevel,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = state.codeBlanksVariablesSuggestions +
+ state.codeBlanksStringsSuggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
else -> activeCodeBlock
}
- is CodeBlock.Print -> {
+ is CodeBlock.Print,
+ is CodeBlock.IfStatement -> {
activeCodeBlock.activeChildIndex()?.let { activeChildIndex ->
- activeCodeBlock.copy(
- children = activeCodeBlock.children.mutate {
+ val activeChild = activeCodeBlock.children[activeChildIndex] as CodeBlockChild.SelectSuggestion
+ val newChildren = activeCodeBlock.children
+ .mutate {
set(
activeChildIndex,
- activeCodeBlock.children[activeChildIndex].copy(
+ activeChild.copy(
selectedSuggestion = message.suggestion as? Suggestion.ConstantString
)
)
}
- )
+ .cast>()
+ activeCodeBlock.updatedChildren(newChildren)
} ?: activeCodeBlock
}
is CodeBlock.Variable -> {
@@ -228,7 +249,8 @@ class StepQuizCodeBlanksReducer(
val newChildren = when (targetCodeBlock) {
is CodeBlock.Print,
- is CodeBlock.Variable -> {
+ is CodeBlock.Variable,
+ is CodeBlock.IfStatement -> {
targetCodeBlock.children.mapIndexed { index, child ->
require(child is CodeBlockChild.SelectSuggestion)
if (index == message.codeBlockChildItem.id) {
@@ -250,11 +272,7 @@ class StepQuizCodeBlanksReducer(
targetCodeBlock?.let { targetCodeBlock ->
set(
targetCodeBlockIndex,
- when (targetCodeBlock) {
- is CodeBlock.Print -> targetCodeBlock.copy(children = newChildren)
- is CodeBlock.Variable -> targetCodeBlock.copy(children = newChildren)
- else -> targetCodeBlock
- }
+ targetCodeBlock.updatedChildren(newChildren)
)
}
}
@@ -271,17 +289,18 @@ class StepQuizCodeBlanksReducer(
}
val activeCodeBlockIndex = state.activeCodeBlockIndex()
+ val activeCodeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] }
val actions = setOf(
InternalAction.LogAnalyticEvent(
StepQuizCodeBlanksClickedDeleteHyperskillAnalyticEvent(
route = stepRoute.analyticRoute,
- codeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] }
+ codeBlock = activeCodeBlock
)
)
)
- if (activeCodeBlockIndex == null) {
+ if (activeCodeBlock == null) {
return state to actions
}
@@ -303,12 +322,13 @@ class StepQuizCodeBlanksReducer(
activeCodeBlockIndex,
createBlankCodeBlock(
isActive = true,
+ indentLevel = activeCodeBlock.indentLevel,
isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable
)
)
}
- when (val activeCodeBlock = state.codeBlocks[activeCodeBlockIndex]) {
+ when (activeCodeBlock) {
is CodeBlock.Blank -> {
if (state.codeBlocks.size > 1) {
removeActiveCodeBlockAndSetNextActive()
@@ -385,7 +405,50 @@ class StepQuizCodeBlanksReducer(
)
)
- activeCodeBlock.children.all { it.selectedSuggestion == null } ->
+ activeChildIndex == 0 || activeCodeBlock.areAllChildrenUnselected() ->
+ if (state.codeBlocks.size > 1) {
+ removeActiveCodeBlockAndSetNextActive()
+ } else {
+ replaceActiveCodeWithBlank()
+ }
+ }
+ }
+ is CodeBlock.IfStatement -> {
+ val activeChildIndex = activeCodeBlock.activeChildIndex() ?: return@mutate
+ val activeChild = activeCodeBlock.children[activeChildIndex]
+
+ val nextCodeBlock = state.codeBlocks.getOrNull(activeCodeBlockIndex + 1)
+
+ when {
+ activeChild.selectedSuggestion != null ->
+ set(
+ activeCodeBlockIndex,
+ activeCodeBlock.copy(
+ children = activeCodeBlock.children.mutate {
+ set(
+ activeChildIndex,
+ activeChild.copy(selectedSuggestion = null)
+ )
+ }
+ )
+ )
+
+ activeChildIndex > 0 ->
+ set(
+ activeCodeBlockIndex,
+ activeCodeBlock.copy(
+ children = activeCodeBlock.children.mutate {
+ set(
+ activeChildIndex - 1,
+ this[activeChildIndex - 1].copy(isActive = true)
+ )
+ removeAt(activeChildIndex)
+ }
+ )
+ )
+
+ (activeChildIndex == 0 || activeCodeBlock.areAllChildrenUnselected()) &&
+ (nextCodeBlock?.let { it.indentLevel == activeCodeBlock.indentLevel } ?: true) ->
if (state.codeBlocks.size > 1) {
removeActiveCodeBlockAndSetNextActive()
} else {
@@ -407,17 +470,24 @@ class StepQuizCodeBlanksReducer(
}
val activeCodeBlockIndex = state.activeCodeBlockIndex()
+ val activeCodeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] }
val actions = setOf(
InternalAction.LogAnalyticEvent(
StepQuizCodeBlanksClickedEnterHyperskillAnalyticEvent(
route = stepRoute.analyticRoute,
- codeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] }
+ codeBlock = activeCodeBlock
)
)
)
- return if (activeCodeBlockIndex != null) {
+ return if (activeCodeBlock != null) {
+ val indentLevel =
+ when (activeCodeBlock) {
+ is CodeBlock.IfStatement -> activeCodeBlock.indentLevel + 1
+ else -> activeCodeBlock.indentLevel
+ }
+
val newCodeBlocks = state.codeBlocks.mutate {
set(
activeCodeBlockIndex,
@@ -427,6 +497,7 @@ class StepQuizCodeBlanksReducer(
activeCodeBlockIndex + 1,
createBlankCodeBlock(
isActive = true,
+ indentLevel = indentLevel,
isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable
)
)
@@ -445,24 +516,25 @@ class StepQuizCodeBlanksReducer(
}
val activeCodeBlockIndex = state.activeCodeBlockIndex()
+ val activeCodeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] }
val actions = setOf(
InternalAction.LogAnalyticEvent(
StepQuizCodeBlanksClickedSpaceHyperskillAnalyticEvent(
route = stepRoute.analyticRoute,
- codeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] }
+ codeBlock = activeCodeBlock
)
)
)
- if (activeCodeBlockIndex == null) {
+ if (activeCodeBlock == null) {
return state to actions
}
- val activeCodeBlock = state.codeBlocks[activeCodeBlockIndex]
val newChildren = when (activeCodeBlock) {
is CodeBlock.Print,
- is CodeBlock.Variable -> {
+ is CodeBlock.Variable,
+ is CodeBlock.IfStatement -> {
activeCodeBlock.activeChildIndex()?.let { activeChildIndex ->
val activeChild = activeCodeBlock.children[activeChildIndex] as CodeBlockChild.SelectSuggestion
@@ -504,11 +576,7 @@ class StepQuizCodeBlanksReducer(
newChildren?.let {
set(
activeCodeBlockIndex,
- when (activeCodeBlock) {
- is CodeBlock.Print -> activeCodeBlock.copy(children = newChildren)
- is CodeBlock.Variable -> activeCodeBlock.copy(children = newChildren)
- else -> activeCodeBlock
- }
+ activeCodeBlock.updatedChildren(newChildren)
)
}
}
@@ -516,11 +584,46 @@ class StepQuizCodeBlanksReducer(
return state.copy(codeBlocks = newCodeBlocks) to actions
}
+ private fun handleDecreaseIndentLevelButtonClicked(
+ state: State
+ ): StepQuizCodeBlanksReducerResult? {
+ if (state !is State.Content) {
+ return null
+ }
+
+ val activeCodeBlockIndex = state.activeCodeBlockIndex()
+ val activeCodeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] }
+
+ val actions = setOf(
+ InternalAction.LogAnalyticEvent(
+ StepQuizCodeBlanksClickedDecreaseIndentLevelHyperskillAnalyticEvent(
+ route = stepRoute.analyticRoute,
+ codeBlock = activeCodeBlock
+ )
+ )
+ )
+
+ if (activeCodeBlock == null || activeCodeBlock.indentLevel < 1) {
+ return state to actions
+ }
+ val newIndentLevel = activeCodeBlock.indentLevel - 1
+
+ return state.copy(
+ codeBlocks = state.codeBlocks.mutate {
+ set(
+ activeCodeBlockIndex,
+ activeCodeBlock.updatedIndentLevel(newIndentLevel)
+ )
+ }
+ ) to actions
+ }
+
private fun setCodeBlockIsActive(codeBlock: CodeBlock, isActive: Boolean): CodeBlock =
when (codeBlock) {
is CodeBlock.Blank -> codeBlock.copy(isActive = isActive)
+ is CodeBlock.Print,
is CodeBlock.Variable,
- is CodeBlock.Print -> {
+ is CodeBlock.IfStatement -> {
if (isActive) {
if (codeBlock.activeChild() != null) {
codeBlock
@@ -533,34 +636,28 @@ class StepQuizCodeBlanksReducer(
child.copy(isActive = false)
}
}
- when (codeBlock) {
- is CodeBlock.Print -> codeBlock.copy(children = newChildren)
- is CodeBlock.Variable -> codeBlock.copy(children = newChildren)
- else -> codeBlock
- }
+ codeBlock.updatedChildren(newChildren)
}
} else {
val newChildren = codeBlock.children.map { child ->
require(child is CodeBlockChild.SelectSuggestion)
child.copy(isActive = false)
}
- when (codeBlock) {
- is CodeBlock.Print -> codeBlock.copy(children = newChildren)
- is CodeBlock.Variable -> codeBlock.copy(children = newChildren)
- else -> codeBlock
- }
+ codeBlock.updatedChildren(newChildren)
}
}
}
private fun createBlankCodeBlock(
isActive: Boolean,
+ indentLevel: Int,
isVariableSuggestionAvailable: Boolean
): CodeBlock.Blank =
CodeBlock.Blank(
isActive = isActive,
+ indentLevel = indentLevel,
suggestions = if (isVariableSuggestionAvailable) {
- listOf(Suggestion.Print, Suggestion.Variable)
+ listOf(Suggestion.Print, Suggestion.Variable, Suggestion.IfStatement)
} else {
listOf(Suggestion.Print)
}
@@ -570,6 +667,7 @@ class StepQuizCodeBlanksReducer(
if (step.id == 47580L) {
listOf(
CodeBlock.Variable(
+ indentLevel = 0,
children = listOf(
CodeBlockChild.SelectSuggestion(
isActive = false,
@@ -584,6 +682,7 @@ class StepQuizCodeBlanksReducer(
)
),
CodeBlock.Variable(
+ indentLevel = 0,
children = listOf(
CodeBlockChild.SelectSuggestion(
isActive = false,
@@ -598,6 +697,7 @@ class StepQuizCodeBlanksReducer(
)
),
CodeBlock.Variable(
+ indentLevel = 0,
children = listOf(
CodeBlockChild.SelectSuggestion(
isActive = false,
@@ -613,6 +713,7 @@ class StepQuizCodeBlanksReducer(
),
createBlankCodeBlock(
isActive = true,
+ indentLevel = 0,
isVariableSuggestionAvailable = StepQuizCodeBlanksFeature.isVariableSuggestionsAvailable(step)
)
)
@@ -620,6 +721,7 @@ class StepQuizCodeBlanksReducer(
listOf(
createBlankCodeBlock(
isActive = true,
+ indentLevel = 0,
isVariableSuggestionAvailable = StepQuizCodeBlanksFeature.isVariableSuggestionsAvailable(step)
)
)
diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/mapper/StepQuizCodeBlanksViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/mapper/StepQuizCodeBlanksViewStateMapper.kt
index fc176599a..899f8f17d 100644
--- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/mapper/StepQuizCodeBlanksViewStateMapper.kt
+++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/mapper/StepQuizCodeBlanksViewStateMapper.kt
@@ -17,13 +17,16 @@ object StepQuizCodeBlanksViewStateMapper {
state: StepQuizCodeBlanksFeature.State.Content
): StepQuizCodeBlanksViewState.Content {
val codeBlocks = state.codeBlocks.mapIndexed(::mapCodeBlock)
- val activeCodeBlock = state.activeCodeBlockIndex()?.let { state.codeBlocks[it] }
+
+ val activeCodeBlockIndex = state.activeCodeBlockIndex()
+ val activeCodeBlock = activeCodeBlockIndex?.let { state.codeBlocks[it] }
val suggestions =
when (activeCodeBlock) {
is CodeBlock.Blank -> activeCodeBlock.suggestions
is CodeBlock.Print,
- is CodeBlock.Variable ->
+ is CodeBlock.Variable,
+ is CodeBlock.IfStatement ->
(activeCodeBlock.activeChild() as? CodeBlockChild.SelectSuggestion)?.let {
if (it.selectedSuggestion == null) {
it.suggestions
@@ -41,23 +44,39 @@ object StepQuizCodeBlanksViewStateMapper {
is CodeBlock.Variable -> {
activeCodeBlock.activeChildIndex()?.let { activeChildIndex ->
when {
- activeChildIndex > 1 ->
+ activeChildIndex == 0 || activeChildIndex > 1 ->
true
activeCodeBlock.children[activeChildIndex].selectedSuggestion == null &&
- activeCodeBlock.children.any { it.selectedSuggestion != null } ->
+ activeCodeBlock.hasAnySelectedChild() ->
false
else -> true
}
} ?: false
}
+ is CodeBlock.IfStatement -> {
+ activeCodeBlock.activeChildIndex()?.let { activeChildIndex ->
+ when {
+ activeChildIndex > 0 ->
+ true
+
+ activeCodeBlock.children[activeChildIndex].selectedSuggestion != null ->
+ true
+
+ else ->
+ codeBlocks.getOrNull(activeCodeBlockIndex + 1)
+ ?.let { it.indentLevel == activeCodeBlock.indentLevel } ?: true
+ }
+ } ?: false
+ }
null -> false
}
val isSpaceButtonHidden = if (state.codeBlanksOperationsSuggestions.isNotEmpty()) {
when (activeCodeBlock) {
- is CodeBlock.Print -> {
+ is CodeBlock.Print,
+ is CodeBlock.IfStatement -> {
val activeChild = activeCodeBlock.activeChild() as? CodeBlockChild.SelectSuggestion
activeChild?.selectedSuggestion == null
}
@@ -75,11 +94,20 @@ object StepQuizCodeBlanksViewStateMapper {
true
}
+ val isDecreaseIndentLevelButtonHidden =
+ when {
+ activeCodeBlock == null -> true
+ activeCodeBlock.indentLevel < 1 -> true
+ state.codeBlocks.getOrNull(activeCodeBlockIndex - 1) is CodeBlock.IfStatement -> true
+ else -> false
+ }
+
return StepQuizCodeBlanksViewState.Content(
codeBlocks = codeBlocks,
suggestions = suggestions,
isDeleteButtonEnabled = isDeleteButtonEnabled,
isSpaceButtonHidden = isSpaceButtonHidden,
+ isDecreaseIndentLevelButtonHidden = isDecreaseIndentLevelButtonHidden,
onboardingState = state.onboardingState
)
}
@@ -90,15 +118,27 @@ object StepQuizCodeBlanksViewStateMapper {
): StepQuizCodeBlanksViewState.CodeBlockItem =
when (codeBlock) {
is CodeBlock.Blank ->
- StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = index, isActive = codeBlock.isActive)
+ StepQuizCodeBlanksViewState.CodeBlockItem.Blank(
+ id = index,
+ indentLevel = codeBlock.indentLevel,
+ isActive = codeBlock.isActive
+ )
is CodeBlock.Print ->
StepQuizCodeBlanksViewState.CodeBlockItem.Print(
id = index,
+ indentLevel = codeBlock.indentLevel,
children = codeBlock.children.mapIndexed(::mapCodeBlockChild)
)
is CodeBlock.Variable ->
StepQuizCodeBlanksViewState.CodeBlockItem.Variable(
id = index,
+ indentLevel = codeBlock.indentLevel,
+ children = codeBlock.children.mapIndexed(::mapCodeBlockChild)
+ )
+ is CodeBlock.IfStatement ->
+ StepQuizCodeBlanksViewState.CodeBlockItem.IfStatement(
+ id = index,
+ indentLevel = codeBlock.indentLevel,
children = codeBlock.children.mapIndexed(::mapCodeBlockChild)
)
}
diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/model/StepQuizCodeBlanksViewState.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/model/StepQuizCodeBlanksViewState.kt
index 5221faeee..0cddcf03b 100644
--- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/model/StepQuizCodeBlanksViewState.kt
+++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_code_blanks/view/model/StepQuizCodeBlanksViewState.kt
@@ -11,6 +11,7 @@ sealed interface StepQuizCodeBlanksViewState {
val suggestions: List,
val isDeleteButtonEnabled: Boolean,
val isSpaceButtonHidden: Boolean,
+ val isDecreaseIndentLevelButtonHidden: Boolean,
internal val onboardingState: OnboardingState = OnboardingState.Unavailable
) : StepQuizCodeBlanksViewState {
val isActionButtonsHidden: Boolean
@@ -23,10 +24,13 @@ sealed interface StepQuizCodeBlanksViewState {
sealed interface CodeBlockItem {
val id: Int
+ val indentLevel: Int
+
val children: List
data class Blank(
override val id: Int,
+ override val indentLevel: Int = 0,
val isActive: Boolean
) : CodeBlockItem {
override val children: List = emptyList()
@@ -34,11 +38,13 @@ sealed interface StepQuizCodeBlanksViewState {
data class Print(
override val id: Int,
+ override val indentLevel: Int = 0,
override val children: List
) : CodeBlockItem
data class Variable(
override val id: Int,
+ override val indentLevel: Int = 0,
override val children: List
) : CodeBlockItem {
val name: CodeBlockChildItem?
@@ -47,6 +53,12 @@ sealed interface StepQuizCodeBlanksViewState {
val values: List
get() = children.drop(1)
}
+
+ data class IfStatement(
+ override val id: Int,
+ override val indentLevel: Int = 0,
+ override val children: List
+ ) : CodeBlockItem
}
data class CodeBlockChildItem(
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt
deleted file mode 100644
index d0160f37b..000000000
--- a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksReducerTest.kt
+++ /dev/null
@@ -1,1349 +0,0 @@
-package org.hyperskill.step_quiz_code_blanks
-
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import kotlin.test.assertTrue
-import org.hyperskill.app.step.domain.model.Block
-import org.hyperskill.app.step.domain.model.Step
-import org.hyperskill.app.step.domain.model.StepRoute
-import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent
-import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent
-import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedDeleteHyperskillAnalyticEvent
-import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedEnterHyperskillAnalyticEvent
-import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedSpaceHyperskillAnalyticEvent
-import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedSuggestionHyperskillAnalyticEvent
-import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
-import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
-import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
-import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
-import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.OnboardingState
-import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
-import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
-import org.hyperskill.step.domain.model.stub
-
-class StepQuizCodeBlanksReducerTest {
- private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
-
- @Test
- fun `Initialize should return Content state with active Blank and Print and Variable suggestions`() {
- val step = Step.stub(
- id = 1,
- block = Block.stub(options = Block.Options(codeBlanksVariables = listOf("a", "b")))
- )
-
- val message = StepQuizCodeBlanksFeature.InternalMessage.Initialize(step)
- val (state, actions) = reducer.reduce(StepQuizCodeBlanksFeature.State.Idle, message)
-
- val expectedState = StepQuizCodeBlanksFeature.State.Content(
- step = step,
- codeBlocks = listOf(
- CodeBlock.Blank(
- isActive = true,
- suggestions = listOf(Suggestion.Print, Suggestion.Variable)
- )
- )
- )
-
- assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
- assertEquals(expectedState.codeBlocks, state.codeBlocks)
- assertTrue(actions.isEmpty())
- }
-
- @Test
- fun `Initialize should return Content state with active Blank and Print suggestion`() {
- val step = Step.stub(id = 1)
-
- val message = StepQuizCodeBlanksFeature.InternalMessage.Initialize(step)
- val (state, actions) = reducer.reduce(StepQuizCodeBlanksFeature.State.Idle, message)
-
- val expectedState = StepQuizCodeBlanksFeature.State.Content(
- step = step,
- codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)))
- )
-
- assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
- assertEquals(expectedState.codeBlocks, state.codeBlocks)
- assertTrue(actions.isEmpty())
- }
-
- @Test
- fun `SuggestionClicked should not update state if no active code block`() {
- val initialState =
- stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = false, suggestions = emptyList())))
-
- val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Print)
- val (state, actions) = reducer.reduce(initialState, message)
-
- assertEquals(initialState, state)
- assertContainsSuggestionClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SuggestionClicked should not update state if suggestion does not exist`() {
- val initialState =
- stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())))
-
- val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.ConstantString("test"))
- val (state, actions) = reducer.reduce(initialState, message)
-
- assertEquals(initialState, state)
- assertContainsSuggestionClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SuggestionClicked should not update state if state is not Content`() {
- val initialState = StepQuizCodeBlanksFeature.State.Idle
- val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Print)
- val (state, actions) = reducer.reduce(initialState, message)
-
- assertEquals(initialState, state)
- assertTrue(actions.isEmpty())
- }
-
- @Test
- fun `SuggestionClicked should update active Blank code block to Print if suggestion exists`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(
- isActive = true,
- suggestions = listOf(Suggestion.Print)
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Print)
- val (state, actions) = reducer.reduce(initialState, message)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = initialState.codeBlanksStringsSuggestions,
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsSuggestionClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SuggestionClicked should update active Blank code block to Variable if suggestion exists`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(
- isActive = true,
- suggestions = listOf(Suggestion.Print, Suggestion.Variable)
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Variable)
- val (state, actions) = reducer.reduce(initialState, message)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = initialState.codeBlanksVariablesSuggestions,
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = initialState.codeBlanksStringsSuggestions,
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsSuggestionClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SuggestionClicked should update Print code block with selected suggestion`() {
- val suggestion = Suggestion.ConstantString("suggestion")
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(suggestion),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion)
- val (state, actions) = reducer.reduce(initialState, message)
-
- assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
- assertEquals(suggestion, (state.codeBlocks[0] as CodeBlock.Print).children[0].selectedSuggestion)
- assertContainsSuggestionClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SuggestionClicked should update Variable code block with selected suggestion for name`() {
- val suggestion = Suggestion.ConstantString("suggestion")
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(suggestion),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(suggestion),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion)
- val (state, actions) = reducer.reduce(initialState, message)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(suggestion),
- selectedSuggestion = suggestion
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(suggestion),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
- assertEquals(expectedState.codeBlocks, state.codeBlocks)
- assertContainsSuggestionClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SuggestionClicked should update Variable code block with selected suggestion for value`() {
- val suggestion = Suggestion.ConstantString("suggestion")
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(suggestion),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(suggestion),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion)
- val (state, actions) = reducer.reduce(initialState, message)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(suggestion),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(suggestion),
- selectedSuggestion = suggestion
- )
- )
- )
- )
- )
-
- assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
- assertEquals(expectedState.codeBlocks, state.codeBlocks)
- assertContainsSuggestionClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `CodeBlockClicked should not update state if state is not Content`() {
- val initialState = StepQuizCodeBlanksFeature.State.Idle
- val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked(
- codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = true)
- )
- val (state, actions) = reducer.reduce(initialState, message)
-
- assertEquals(initialState, state)
- assertTrue(actions.isEmpty())
- }
-
- @Test
- fun `CodeBlockClicked should update active Print code block`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = false, suggestions = emptyList()),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true, suggestions = emptyList(), selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked(
- codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = false)
- )
- val (state, actions) = reducer.reduce(initialState, message)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = true, suggestions = emptyList()),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false, suggestions = emptyList(), selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertTrue {
- actions.any {
- it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
- it.analyticEvent is StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent
- }
- }
- }
-
- @Test
- fun `CodeBlockClicked should update active Variable code block`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false, suggestions = emptyList(), selectedSuggestion = null
- )
- )
- ),
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true, suggestions = emptyList(), selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false, suggestions = emptyList(), selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked(
- codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = false)
- )
- val (state, actions) = reducer.reduce(initialState, message)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true, suggestions = emptyList(), selectedSuggestion = null
- )
- )
- ),
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false, suggestions = emptyList(), selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false, suggestions = emptyList(), selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertTrue {
- actions.any {
- it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
- it.analyticEvent is StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent
- }
- }
- }
-
- @Test
- fun `CodeBlockChildClicked should not update state if state is not Content`() {
- val initialState = StepQuizCodeBlanksFeature.State.Idle
- val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked(
- codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Variable(id = 0, children = emptyList()),
- codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null)
- )
- val (state, actions) = reducer.reduce(initialState, message)
-
- assertEquals(initialState, state)
- assertTrue(actions.isEmpty())
- }
-
- @Test
- fun `CodeBlockChildClicked should not update state if target code block is not found`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked(
- codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Variable(id = 1, children = emptyList()),
- codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null)
- )
- val (state, actions) = reducer.reduce(initialState, message)
-
- assertEquals(initialState, state)
- assertContainsCodeBlockChildClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `CodeBlockChildClicked should update state to activate the clicked Variable child`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked(
- codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Variable(id = 0, children = emptyList()),
- codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null)
- )
- val (state, actions) = reducer.reduce(initialState, message)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsCodeBlockChildClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `CodeBlockChildClicked should update state to activate the clicked Print child`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked(
- codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Print(id = 0, children = emptyList()),
- codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null)
- )
- val (state, actions) = reducer.reduce(initialState, message)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsCodeBlockChildClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `DeleteButtonClicked should not update state if state is not Content`() {
- val initialState = StepQuizCodeBlanksFeature.State.Idle
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
-
- assertEquals(initialState, state)
- assertTrue(actions.isEmpty())
- }
-
- @Test
- fun `DeleteButtonClicked should log analytic event and not update state if no active code block`() {
- val initialState =
- stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = false, suggestions = emptyList())))
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
-
- assertEquals(initialState, state)
- assertContainsDeleteButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `DeleteButtonClicked should not update state if active code block is Blank and single`() {
- val initialState =
- stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())))
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
-
- assertEquals(initialState, state)
- assertContainsDeleteButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `DeleteButtonClicked should clear suggestion if active Print code block has selected suggestion`() {
- val suggestion = Suggestion.ConstantString("suggestion")
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(suggestion),
- selectedSuggestion = suggestion
- )
- )
- )
- )
- )
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(suggestion),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsDeleteButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `DeleteButtonClicked should set next code block as active if no code block before deleted`() {
- val initialStates = listOf(
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- ),
- CodeBlock.Blank(isActive = false, suggestions = emptyList())
- )
- ),
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = true, suggestions = emptyList()),
- CodeBlock.Blank(isActive = false, suggestions = emptyList())
- )
- ),
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = true, suggestions = emptyList()),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- ),
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- ),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- ),
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- ),
- CodeBlock.Blank(isActive = false, suggestions = emptyList())
- )
- ),
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = true, suggestions = emptyList()),
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- ),
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- ),
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = Suggestion.ConstantString("suggestion")
- )
- )
- )
- )
- )
- )
- val expectedStates = listOf(
- initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
- initialStates[1].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
- initialStates[2].copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- ),
- initialStates[3].copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- ),
- initialStates[4].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
- initialStates[5].copy(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- ),
- initialStates[6].copy(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = Suggestion.ConstantString("suggestion")
- )
- )
- )
- )
- )
- )
-
- initialStates.zip(expectedStates).forEach { (initialState, expectedState) ->
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
- assertEquals(expectedState, state)
- assertContainsDeleteButtonClickedAnalyticEvent(actions)
- }
- }
-
- @Test
- fun `DeleteButtonClicked should set previous code block as active if has code block before deleted`() {
- val initialStates = listOf(
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = false, suggestions = emptyList()),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- ),
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- ),
- CodeBlock.Blank(isActive = true, suggestions = emptyList())
- )
- ),
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(Suggestion.ConstantString("suggestion")),
- selectedSuggestion = Suggestion.ConstantString("suggestion")
- )
- )
- ),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- ),
- stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = false, suggestions = emptyList()),
- CodeBlock.Blank(isActive = true, suggestions = emptyList())
- )
- )
- )
- val expectedStates = listOf(
- initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
- initialStates[1].copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- ),
- initialStates[2].copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(Suggestion.ConstantString("suggestion")),
- selectedSuggestion = Suggestion.ConstantString("suggestion")
- )
- )
- )
- )
- ),
- initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
- )
-
- initialStates.zip(expectedStates).forEach { (initialState, expectedState) ->
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
- assertEquals(expectedState, state)
- assertContainsDeleteButtonClickedAnalyticEvent(actions)
- }
- }
-
- @Test
- fun `DeleteButtonClicked should not update state if no active code block`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
-
- assertEquals(initialState, state)
- assertContainsDeleteButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `DeleteButtonClicked should replace single Print code block with Blank`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)))
- )
-
- assertEquals(expectedState, state)
- assertContainsDeleteButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `DeleteButtonClicked should replace single Variable code block with Blank`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)))
- )
-
- assertEquals(expectedState, state)
- assertContainsDeleteButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `EnterButtonClicked should not update state if state is not Content`() {
- val initialState = StepQuizCodeBlanksFeature.State.Idle
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked)
-
- assertEquals(initialState, state)
- assertTrue(actions.isEmpty())
- }
-
- @Test
- fun `EnterButtonClicked should log analytic event and not update state if no active code block`() {
- val initialState =
- stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = false, suggestions = emptyList())))
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked)
-
- assertEquals(initialState, state)
- assertContainsEnterButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `EnterButtonClicked should log analytic event and add new active Blank block if active code block exists`() {
- val initialState =
- stubContentState(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())))
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = false, suggestions = emptyList()),
- CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print))
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsEnterButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `EnterButtonClicked should add new active Blank block after active code block`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = true, suggestions = emptyList()),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = false, suggestions = emptyList()),
- CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsEnterButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SpaceButtonClicked should not update state if state is not Content`() {
- val initialState = StepQuizCodeBlanksFeature.State.Idle
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
-
- assertEquals(initialState, state)
- assertTrue(actions.isEmpty())
- }
-
- @Test
- fun `SpaceButtonClicked should not update state if active Print block has no active child`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = Suggestion.ConstantString("suggestion")
- )
- )
- )
- )
- )
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
-
- assertEquals(initialState, state)
- assertContainsSpaceButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SpaceButtonClicked should add a new child to active Print code block`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(Suggestion.ConstantString("suggestion")),
- selectedSuggestion = Suggestion.ConstantString("suggestion")
- )
- )
- )
- )
- )
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(Suggestion.ConstantString("suggestion")),
- selectedSuggestion = Suggestion.ConstantString("suggestion")
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsSpaceButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SpaceButtonClicked should add a new child to active Variable code block`() {
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(Suggestion.ConstantString("x")),
- selectedSuggestion = Suggestion.ConstantString("x")
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(Suggestion.ConstantString("suggestion")),
- selectedSuggestion = Suggestion.ConstantString("suggestion")
- )
- )
- )
- )
- )
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(Suggestion.ConstantString("x")),
- selectedSuggestion = Suggestion.ConstantString("x")
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(Suggestion.ConstantString("suggestion")),
- selectedSuggestion = Suggestion.ConstantString("suggestion")
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsSpaceButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `SpaceButtonClicked should add a new child with operations suggestions after closing parentheses`() {
- val initialState = stubContentState(
- step = Step.stub(
- id = 1,
- block = Block.stub(
- options = Block.Options(codeBlanksOperations = listOf("*", "+"))
- )
- ),
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(Suggestion.ConstantString(")")),
- selectedSuggestion = Suggestion.ConstantString(")")
- )
- )
- )
- )
- )
-
- val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
-
- val expectedState = initialState.copy(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = listOf(Suggestion.ConstantString(")")),
- selectedSuggestion = Suggestion.ConstantString(")")
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = initialState.codeBlanksOperationsSuggestions,
- selectedSuggestion = null
- )
- )
- )
- )
- )
-
- assertEquals(expectedState, state)
- assertContainsSpaceButtonClickedAnalyticEvent(actions)
- }
-
- @Test
- fun `Onboarding should be unavailable`() {
- val initialState = StepQuizCodeBlanksFeature.State.Idle
- val (state, _) = reducer.reduce(
- initialState,
- StepQuizCodeBlanksFeature.InternalMessage.Initialize(Step.stub(id = 1))
- )
-
- assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
- assertTrue(state.onboardingState is OnboardingState.Unavailable)
- }
-
- @Test
- fun `Onboarding should be available`() {
- val initialState = StepQuizCodeBlanksFeature.State.Idle
- val (state, _) = reducer.reduce(
- initialState,
- StepQuizCodeBlanksFeature.InternalMessage.Initialize(Step.stub(id = 47329))
- )
-
- assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
- assertTrue(state.onboardingState is OnboardingState.HighlightSuggestions)
- }
-
- @Test
- fun `Onboarding SuggestionClicked should update onboardingState to HighlightCallToActionButton`() {
- val suggestion = Suggestion.ConstantString("suggestion")
- val initialState = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = listOf(suggestion),
- selectedSuggestion = null
- )
- )
- )
- ),
- onboardingState = OnboardingState.HighlightSuggestions
- )
-
- val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion)
- val (state, _) = reducer.reduce(initialState, message)
-
- assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
- assertEquals(OnboardingState.HighlightCallToActionButton, state.onboardingState)
- }
-
- private fun assertContainsSuggestionClickedAnalyticEvent(actions: Set) {
- assertTrue {
- actions.any {
- it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
- it.analyticEvent is StepQuizCodeBlanksClickedSuggestionHyperskillAnalyticEvent
- }
- }
- }
-
- private fun assertContainsCodeBlockChildClickedAnalyticEvent(actions: Set) {
- assertTrue {
- actions.any {
- it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
- it.analyticEvent is StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent
- }
- }
- }
-
- private fun assertContainsDeleteButtonClickedAnalyticEvent(actions: Set) {
- assertTrue {
- actions.any {
- it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
- it.analyticEvent is StepQuizCodeBlanksClickedDeleteHyperskillAnalyticEvent
- }
- }
- }
-
- private fun assertContainsEnterButtonClickedAnalyticEvent(actions: Set) {
- assertTrue {
- actions.any {
- it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
- it.analyticEvent is StepQuizCodeBlanksClickedEnterHyperskillAnalyticEvent
- }
- }
- }
-
- private fun assertContainsSpaceButtonClickedAnalyticEvent(actions: Set) {
- assertTrue {
- actions.any {
- it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
- it.analyticEvent is StepQuizCodeBlanksClickedSpaceHyperskillAnalyticEvent
- }
- }
- }
-
- private fun stubContentState(
- step: Step = Step.stub(id = 1),
- codeBlocks: List,
- onboardingState: OnboardingState = OnboardingState.Unavailable
- ): StepQuizCodeBlanksFeature.State.Content =
- StepQuizCodeBlanksFeature.State.Content(
- step = step,
- codeBlocks = codeBlocks,
- onboardingState = onboardingState
- )
-}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksViewStateMapperTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksViewStateMapperTest.kt
deleted file mode 100644
index bbaa7e896..000000000
--- a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksViewStateMapperTest.kt
+++ /dev/null
@@ -1,596 +0,0 @@
-package org.hyperskill.step_quiz_code_blanks
-
-import kotlin.test.Test
-import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertTrue
-import org.hyperskill.app.step.domain.model.Step
-import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
-import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
-import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
-import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
-import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.OnboardingState
-import org.hyperskill.app.step_quiz_code_blanks.view.mapper.StepQuizCodeBlanksViewStateMapper
-import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
-import org.hyperskill.step.domain.model.stub
-
-class StepQuizCodeBlanksViewStateMapperTest {
- @Test
- fun `map should return Idle view state for Idle state`() {
- val state = StepQuizCodeBlanksFeature.State.Idle
- val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
- assertEquals(StepQuizCodeBlanksViewState.Idle, viewState)
- }
-
- @Test
- fun `Content with print suggestion and disabled delete button when active code block is Blank`() {
- val state = stubState(
- codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)))
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = true)),
- suggestions = listOf(Suggestion.Print),
- isDeleteButtonEnabled = false,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with suggestions and enabled delete button when active code block is Print`() {
- val suggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = suggestions,
- selectedSuggestion = null
- )
- )
- )
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Print(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = true,
- value = null
- )
- )
- )
- ),
- suggestions = suggestions,
- isDeleteButtonEnabled = true,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with sequence of filled Print and active Blank`() {
- val printSuggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = printSuggestions,
- selectedSuggestion = printSuggestions[0]
- )
- )
- ),
- CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print))
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Print(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = false,
- value = printSuggestions[0].text
- )
- )
- ),
- StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 1, isActive = true)
- ),
- suggestions = listOf(Suggestion.Print),
- isDeleteButtonEnabled = true,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with sequence of filled Print and active not filled Print`() {
- val printSuggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = printSuggestions,
- selectedSuggestion = printSuggestions[0]
- )
- )
- ),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = printSuggestions,
- selectedSuggestion = null
- )
- )
- )
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Print(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = false,
- value = printSuggestions[0].text
- )
- )
- ),
- StepQuizCodeBlanksViewState.CodeBlockItem.Print(
- id = 1,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = true,
- value = null
- )
- )
- )
- ),
- suggestions = printSuggestions,
- isDeleteButtonEnabled = true,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with active Variable and disabled delete button`() {
- val suggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = suggestions,
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = suggestions,
- selectedSuggestion = suggestions[0]
- )
- )
- )
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Variable(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = true,
- value = null
- ),
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 1,
- isActive = false,
- value = suggestions[0].text
- )
- )
- )
- ),
- suggestions = suggestions,
- isDeleteButtonEnabled = false,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with active not filled Variable and enabled delete button`() {
- val suggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = suggestions,
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = suggestions,
- selectedSuggestion = null
- )
- )
- )
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Variable(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = true,
- value = null
- ),
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 1,
- isActive = false,
- value = null
- )
- )
- )
- ),
- suggestions = suggestions,
- isDeleteButtonEnabled = true,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with active filled Variable and enabled delete button`() {
- val suggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = suggestions,
- selectedSuggestion = suggestions[0]
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = suggestions,
- selectedSuggestion = suggestions[1]
- )
- )
- )
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Variable(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = true,
- value = suggestions[0].text
- ),
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 1,
- isActive = false,
- value = suggestions[1].text
- )
- )
- )
- ),
- suggestions = emptyList(),
- isDeleteButtonEnabled = true,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with suggestions when active code block is Blank`() {
- val suggestions = listOf(Suggestion.Print, Suggestion.Variable)
- val state = stubState(
- codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = suggestions))
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = true)),
- suggestions = suggestions,
- isDeleteButtonEnabled = false,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with suggestions when active code block is Print and no selected suggestion`() {
- val suggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = suggestions,
- selectedSuggestion = null
- )
- )
- )
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Print(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = true,
- value = null
- )
- )
- )
- ),
- suggestions = suggestions,
- isDeleteButtonEnabled = true,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with no suggestions when active code block is Print and has selected suggestion`() {
- val suggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = suggestions,
- selectedSuggestion = suggestions[0]
- )
- )
- )
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Print(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = true,
- value = suggestions[0].text
- )
- )
- )
- ),
- suggestions = emptyList(),
- isDeleteButtonEnabled = true,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with suggestions when active code block is Variable and active child has no selected suggestion`() {
- val suggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = suggestions,
- selectedSuggestion = null
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = suggestions,
- selectedSuggestion = null
- )
- )
- )
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Variable(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = true,
- value = null
- ),
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 1,
- isActive = false,
- value = null
- )
- )
- )
- ),
- suggestions = suggestions,
- isDeleteButtonEnabled = true,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Content with no suggestions when active code block is Variable and active child has selected suggestion`() {
- val suggestions = listOf(
- Suggestion.ConstantString("1"),
- Suggestion.ConstantString("2")
- )
- val state = stubState(
- codeBlocks = listOf(
- CodeBlock.Variable(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = suggestions,
- selectedSuggestion = suggestions[0]
- ),
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = suggestions,
- selectedSuggestion = suggestions[1]
- )
- )
- )
- )
- )
- val expectedViewState = StepQuizCodeBlanksViewState.Content(
- codeBlocks = listOf(
- StepQuizCodeBlanksViewState.CodeBlockItem.Variable(
- id = 0,
- children = listOf(
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 0,
- isActive = true,
- value = suggestions[0].text
- ),
- StepQuizCodeBlanksViewState.CodeBlockChildItem(
- id = 1,
- isActive = false,
- value = suggestions[1].text
- )
- )
- )
- ),
- suggestions = emptyList(),
- isDeleteButtonEnabled = true,
- isSpaceButtonHidden = true
- )
-
- val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertEquals(expectedViewState, actualViewState)
- }
-
- @Test
- fun `Action buttons hidden when onboarding is available`() {
- val state = stubState(
- codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())),
- onboardingState = OnboardingState.HighlightSuggestions
- )
- val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
- assertTrue(viewState.isActionButtonsHidden)
- }
-
- @Test
- fun `Action buttons not hidden when onboarding is unavailable`() {
- val state = stubState(
- codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())),
- onboardingState = OnboardingState.Unavailable
- )
- val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
- assertFalse(viewState.isActionButtonsHidden)
- }
-
- @Test
- fun `Suggestions highlight effect is active when onboardingState is HighlightSuggestions`() {
- val state = stubState(
- codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())),
- onboardingState = OnboardingState.HighlightSuggestions
- )
- val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
-
- assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
- assertTrue(viewState.isSuggestionsHighlightEffectActive)
- }
-
- private fun stubState(
- codeBlocks: List,
- onboardingState: OnboardingState = OnboardingState.Unavailable
- ): StepQuizCodeBlanksFeature.State.Content =
- StepQuizCodeBlanksFeature.State.Content(
- step = Step.stub(id = 0),
- codeBlocks = codeBlocks,
- onboardingState = onboardingState
- )
-}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksCreateReplyTest.kt
similarity index 56%
rename from shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt
rename to shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksCreateReplyTest.kt
index 60fc9f235..a4e9aa148 100644
--- a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/StepQuizCodeBlanksStateExtensionsTest.kt
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksCreateReplyTest.kt
@@ -1,60 +1,31 @@
-package org.hyperskill.step_quiz_code_blanks
+package org.hyperskill.step_quiz_code_blanks.presentation
import kotlin.test.Test
import kotlin.test.assertEquals
-import kotlin.test.assertFalse
-import kotlin.test.assertNull
-import kotlin.test.assertTrue
import org.hyperskill.app.step.domain.model.Block
import org.hyperskill.app.step.domain.model.Step
import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
-import org.hyperskill.app.step_quiz_code_blanks.presentation.activeCodeBlockIndex
import org.hyperskill.app.step_quiz_code_blanks.presentation.createReply
-import org.hyperskill.app.step_quiz_code_blanks.presentation.isVariableSuggestionsAvailable
import org.hyperskill.app.submissions.domain.model.Reply
import org.hyperskill.step.domain.model.stub
-class StepQuizCodeBlanksStateExtensionsTest {
- @Test
- fun `activeCodeBlockIndex should return null if no active code block`() {
- val state = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = false, suggestions = emptyList()),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = false,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
- )
- )
- assertNull(state.activeCodeBlockIndex())
+class StepQuizCodeBlanksCreateReplyTest {
+ companion object {
+ private const val REPLY_CODE_LANGUAGE = "python3"
+ private const val REPLY_CODE_PREFIX = "# solved with code blanks\n"
}
- @Test
- fun `activeCodeBlockIndex should return index of the active code block`() {
- val state = stubContentState(
- codeBlocks = listOf(
- CodeBlock.Blank(isActive = false, suggestions = emptyList()),
- CodeBlock.Print(
- children = listOf(
- CodeBlockChild.SelectSuggestion(
- isActive = true,
- suggestions = emptyList(),
- selectedSuggestion = null
- )
- )
- )
+ private val step = Step.stub(
+ id = 1,
+ block = Block.stub(
+ options = Block.Options(
+ codeTemplates = mapOf(REPLY_CODE_LANGUAGE to "# put your python code here")
)
)
- assertEquals(1, state.activeCodeBlockIndex())
- }
+ )
@Test
fun `createReply should return Reply with code from code blocks and language from step options`() {
@@ -70,24 +41,14 @@ class StepQuizCodeBlanksStateExtensionsTest {
),
CodeBlock.Blank(isActive = true, suggestions = emptyList())
)
- val step = Step.stub(id = 1).copy(
- block = Block.stub(
- options = Block.Options(
- codeTemplates = mapOf("python3" to "# put your python code here")
- )
- )
- )
- val state = stubContentState(
- step = step,
- codeBlocks = codeBlocks
- )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(step = step, codeBlocks = codeBlocks)
val expectedReply = Reply.code(
code = buildString {
- append("# solved with code blanks\n")
+ append(REPLY_CODE_PREFIX)
append("print(\"test\")\n")
},
- language = "python3"
+ language = REPLY_CODE_LANGUAGE
)
assertEquals(expectedReply, state.createReply())
@@ -120,24 +81,14 @@ class StepQuizCodeBlanksStateExtensionsTest {
)
),
)
- val step = Step.stub(id = 1).copy(
- block = Block.stub(
- options = Block.Options(
- codeTemplates = mapOf("python3" to "# put your python code here")
- )
- )
- )
- val state = stubContentState(
- step = step,
- codeBlocks = codeBlocks
- )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(step = step, codeBlocks = codeBlocks)
val expectedReply = Reply.code(
code = buildString {
- append("# solved with code blanks\n")
+ append(REPLY_CODE_PREFIX)
append("a = 1\nprint(a)")
},
- language = "python3"
+ language = REPLY_CODE_LANGUAGE
)
assertEquals(expectedReply, state.createReply())
@@ -248,27 +199,17 @@ class StepQuizCodeBlanksStateExtensionsTest {
)
)
)
- val step = Step.stub(id = 1).copy(
- block = Block.stub(
- options = Block.Options(
- codeTemplates = mapOf("python3" to "# put your python code here")
- )
- )
- )
- val state = stubContentState(
- step = step,
- codeBlocks = codeBlocks
- )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(step = step, codeBlocks = codeBlocks)
val expectedReply = Reply.code(
code = buildString {
- append("# solved with code blanks\n")
+ append(REPLY_CODE_PREFIX)
append("x = 1000\n")
append("r = 5\n")
append("y = 10\n")
append("print(x * (1 + r / 100) ** y)")
},
- language = "python3"
+ language = REPLY_CODE_LANGUAGE
)
assertEquals(expectedReply, state.createReply())
@@ -393,69 +334,253 @@ class StepQuizCodeBlanksStateExtensionsTest {
)
)
)
- val step = Step.stub(id = 1).copy(
- block = Block.stub(
- options = Block.Options(
- codeTemplates = mapOf("python3" to "# put your python code here")
- )
- )
- )
- val state = stubContentState(
- step = step,
- codeBlocks = codeBlocks
- )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(step = step, codeBlocks = codeBlocks)
val expectedReply = Reply.code(
code = buildString {
- append("# solved with code blanks\n")
+ append(REPLY_CODE_PREFIX)
append("x = 1000\n")
append("r = 5\n")
append("y = 10\n")
append("a = x * (1 + r / 100) ** y\n")
append("print(a)")
},
- language = "python3"
+ language = REPLY_CODE_LANGUAGE
)
assertEquals(expectedReply, state.createReply())
}
@Test
- fun `isVariableSuggestionsAvailable should return true if variable suggestions are available`() {
- val step = Step.stub(
- id = 1,
- block = Block.stub(options = Block.Options(codeBlanksVariables = listOf("a", "b")))
+ fun `createReply should return correct Reply with single IfStatement`() {
+ val codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("a")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("33")
+ )
+ )
+ ),
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("b")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("200")
+ )
+ )
+ ),
+ CodeBlock.IfStatement(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("b")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString(">")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("a")
+ )
+ )
+ ),
+ CodeBlock.Print(
+ indentLevel = 1,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("\"b is greater than a\"")
+ )
+ )
+ )
)
- val state = stubContentState(
- step = step,
- codeBlocks = emptyList()
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(step = step, codeBlocks = codeBlocks)
+
+ val expectedReply = Reply.code(
+ code = buildString {
+ append(REPLY_CODE_PREFIX)
+ append("a = 33\n")
+ append("b = 200\n")
+ append("if b > a:\n")
+ append("\tprint(\"b is greater than a\")")
+ },
+ language = REPLY_CODE_LANGUAGE
)
- assertTrue(state.isVariableSuggestionsAvailable)
+ assertEquals(expectedReply, state.createReply())
}
@Test
- fun `isVariableSuggestionsAvailable should return false if variable suggestions are not available`() {
- listOf(null, emptyList()).forEach { codeBlanksVariables ->
- val step = Step.stub(
- id = 1,
- block = Block.stub(options = Block.Options(codeBlanksVariables = codeBlanksVariables))
- )
- val state = stubContentState(
- step = step,
- codeBlocks = emptyList()
+ fun `createReply should return correct Reply with multiple IfStatement and indentation level of 2`() {
+ val codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("x")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("10")
+ )
+ )
+ ),
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("y")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("5")
+ )
+ )
+ ),
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("z")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("15")
+ )
+ )
+ ),
+ CodeBlock.IfStatement(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("x")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString(">")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("y")
+ )
+ )
+ ),
+ CodeBlock.Print(
+ indentLevel = 1,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("\"x is greater than y\"")
+ )
+ )
+ ),
+ CodeBlock.IfStatement(
+ indentLevel = 1,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("z")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString(">")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("x")
+ )
+ )
+ ),
+ CodeBlock.Print(
+ indentLevel = 2,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("\"z is greater than x\"")
+ )
+ )
+ ),
+ CodeBlock.IfStatement(
+ indentLevel = 1,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("z")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("<")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("x")
+ )
+ )
+ ),
+ CodeBlock.Print(
+ indentLevel = 2,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("\"z is less than x\"")
+ )
+ )
)
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(step = step, codeBlocks = codeBlocks)
- assertFalse(state.isVariableSuggestionsAvailable)
- }
- }
-
- private fun stubContentState(
- step: Step = Step.stub(id = 1),
- codeBlocks: List
- ): StepQuizCodeBlanksFeature.State.Content =
- StepQuizCodeBlanksFeature.State.Content(
- step = step,
- codeBlocks = codeBlocks
+ val expectedReply = Reply.code(
+ code = buildString {
+ append(REPLY_CODE_PREFIX)
+ append("x = 10\n")
+ append("y = 5\n")
+ append("z = 15\n")
+ append("if x > y:\n")
+ append("\tprint(\"x is greater than y\")\n")
+ append("\tif z > x:\n")
+ append("\t\tprint(\"z is greater than x\")\n")
+ append("\tif z < x:\n")
+ append("\t\tprint(\"z is less than x\")")
+ },
+ language = REPLY_CODE_LANGUAGE
)
+
+ assertEquals(expectedReply, state.createReply())
+ }
}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeatureStateStub.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeatureStateStub.kt
new file mode 100644
index 000000000..20feff2f7
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksFeatureStateStub.kt
@@ -0,0 +1,18 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import org.hyperskill.app.step.domain.model.Step
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.OnboardingState
+import org.hyperskill.step.domain.model.stub
+
+fun StepQuizCodeBlanksFeature.State.Content.Companion.stub(
+ step: Step = Step.stub(id = 1),
+ codeBlocks: List = emptyList(),
+ onboardingState: OnboardingState = OnboardingState.Unavailable
+): StepQuizCodeBlanksFeature.State.Content =
+ StepQuizCodeBlanksFeature.State.Content(
+ step = step,
+ codeBlocks = codeBlocks,
+ onboardingState = onboardingState
+ )
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerCodeBlockChildClickedTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerCodeBlockChildClickedTest.kt
new file mode 100644
index 000000000..feea44ebf
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerCodeBlockChildClickedTest.kt
@@ -0,0 +1,164 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.StepRoute
+import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
+import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
+
+class StepQuizCodeBlanksReducerCodeBlockChildClickedTest {
+ private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
+
+ @Test
+ fun `CodeBlockChildClicked should not update state if state is not Content`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Idle
+ val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked(
+ codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Variable(id = 0, children = emptyList()),
+ codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null)
+ )
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ assertEquals(initialState, state)
+ assertTrue(actions.isEmpty())
+ }
+
+ @Test
+ fun `CodeBlockChildClicked should not update state if target code block is not found`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked(
+ codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Variable(id = 1, children = emptyList()),
+ codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null)
+ )
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ assertEquals(initialState, state)
+ assertContainsCodeBlockChildClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `CodeBlockChildClicked should update state to activate the clicked Variable child`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked(
+ codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Variable(id = 0, children = emptyList()),
+ codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null)
+ )
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsCodeBlockChildClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `CodeBlockChildClicked should update state to activate the clicked Print child`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked(
+ codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Print(id = 0, children = emptyList()),
+ codeBlockChildItem = StepQuizCodeBlanksViewState.CodeBlockChildItem(id = 0, isActive = false, value = null)
+ )
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsCodeBlockChildClickedAnalyticEvent(actions)
+ }
+
+ private fun assertContainsCodeBlockChildClickedAnalyticEvent(actions: Set) {
+ assertTrue {
+ actions.any {
+ it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
+ it.analyticEvent is StepQuizCodeBlanksClickedCodeBlockChildHyperskillAnalyticEvent
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerCodeBlockClickedTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerCodeBlockClickedTest.kt
new file mode 100644
index 000000000..7a88b7b36
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerCodeBlockClickedTest.kt
@@ -0,0 +1,129 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.StepRoute
+import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
+import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
+
+class StepQuizCodeBlanksReducerCodeBlockClickedTest {
+ private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
+
+ @Test
+ fun `CodeBlockClicked should not update state if state is not Content`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Idle
+ val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked(
+ codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = true)
+ )
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ assertEquals(initialState, state)
+ assertTrue(actions.isEmpty())
+ }
+
+ @Test
+ fun `CodeBlockClicked should update active Print code block`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = false, suggestions = emptyList()),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true, suggestions = emptyList(), selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked(
+ codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = false)
+ )
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = true, suggestions = emptyList()),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false, suggestions = emptyList(), selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsCodeBlockClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `CodeBlockClicked should update active Variable code block`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false, suggestions = emptyList(), selectedSuggestion = null
+ )
+ )
+ ),
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true, suggestions = emptyList(), selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false, suggestions = emptyList(), selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked(
+ codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = false)
+ )
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true, suggestions = emptyList(), selectedSuggestion = null
+ )
+ )
+ ),
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false, suggestions = emptyList(), selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false, suggestions = emptyList(), selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsCodeBlockClickedAnalyticEvent(actions)
+ }
+
+ private fun assertContainsCodeBlockClickedAnalyticEvent(actions: Set) {
+ assertTrue {
+ actions.any {
+ it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
+ it.analyticEvent is StepQuizCodeBlanksClickedCodeBlockHyperskillAnalyticEvent
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerDecreaseIndentLevelButtonClickedTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerDecreaseIndentLevelButtonClickedTest.kt
new file mode 100644
index 000000000..63c0f39d7
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerDecreaseIndentLevelButtonClickedTest.kt
@@ -0,0 +1,97 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.StepRoute
+import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedDecreaseIndentLevelHyperskillAnalyticEvent
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
+
+class StepQuizCodeBlanksReducerDecreaseIndentLevelButtonClickedTest {
+ private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
+
+ @Test
+ fun `DecreaseIndentLevelButtonClicked should not update state if no active code block`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(CodeBlock.Blank(isActive = false, suggestions = emptyList()))
+ )
+
+ val (state, actions) = reducer.reduce(
+ initialState,
+ StepQuizCodeBlanksFeature.Message.DecreaseIndentLevelButtonClicked
+ )
+
+ assertEquals(initialState, state)
+ assertContainsDecreaseIndentLevelAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `DecreaseIndentLevelButtonClicked should not decrease indent level below 1`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(CodeBlock.Blank(isActive = true, indentLevel = 0, suggestions = emptyList()))
+ )
+
+ val (state, actions) = reducer.reduce(
+ initialState,
+ StepQuizCodeBlanksFeature.Message.DecreaseIndentLevelButtonClicked
+ )
+
+ assertEquals(initialState, state)
+ assertContainsDecreaseIndentLevelAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `DecreaseIndentLevelButtonClicked should decrease indent level by 1`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(CodeBlock.Blank(isActive = true, indentLevel = 1, suggestions = emptyList()))
+ )
+
+ val (state, actions) = reducer.reduce(
+ initialState,
+ StepQuizCodeBlanksFeature.Message.DecreaseIndentLevelButtonClicked
+ )
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(CodeBlock.Blank(isActive = true, indentLevel = 0, suggestions = emptyList()))
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsDecreaseIndentLevelAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `DecreaseIndentLevelButtonClicked should decrease indent level for active code block only`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = false, indentLevel = 3, suggestions = emptyList()),
+ CodeBlock.Blank(isActive = true, indentLevel = 2, suggestions = emptyList())
+ )
+ )
+
+ val (state, actions) = reducer.reduce(
+ initialState,
+ StepQuizCodeBlanksFeature.Message.DecreaseIndentLevelButtonClicked
+ )
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = false, indentLevel = 3, suggestions = emptyList()),
+ CodeBlock.Blank(isActive = true, indentLevel = 1, suggestions = emptyList())
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsDecreaseIndentLevelAnalyticEvent(actions)
+ }
+
+ private fun assertContainsDecreaseIndentLevelAnalyticEvent(actions: Set) {
+ assertTrue {
+ actions.any {
+ it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
+ it.analyticEvent is StepQuizCodeBlanksClickedDecreaseIndentLevelHyperskillAnalyticEvent
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerDeleteButtonClickedTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerDeleteButtonClickedTest.kt
new file mode 100644
index 000000000..2ab0d37fe
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerDeleteButtonClickedTest.kt
@@ -0,0 +1,470 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.StepRoute
+import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedDeleteHyperskillAnalyticEvent
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
+
+class StepQuizCodeBlanksReducerDeleteButtonClickedTest {
+ private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
+
+ @Test
+ fun `DeleteButtonClicked should not update state if state is not Content`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Idle
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
+
+ assertEquals(initialState, state)
+ assertTrue(actions.isEmpty())
+ }
+
+ @Test
+ fun `DeleteButtonClicked should not update state if no active code block`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
+
+ assertEquals(initialState, state)
+ assertContainsDeleteButtonClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `DeleteButtonClicked should not update state if active code block is Blank and single`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = true,
+ suggestions = emptyList()
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
+
+ assertEquals(initialState, state)
+ assertContainsDeleteButtonClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `DeleteButtonClicked should clear suggestion if active Print code block has selected suggestion`() {
+ val suggestion = Suggestion.ConstantString("suggestion")
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = suggestion
+ )
+ )
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsDeleteButtonClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `DeleteButtonClicked should set next code block as active if no code block before deleted`() {
+ val initialStates = listOf(
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ ),
+ CodeBlock.Blank(isActive = false, suggestions = emptyList())
+ )
+ ),
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = true, suggestions = emptyList()),
+ CodeBlock.Blank(isActive = false, suggestions = emptyList())
+ )
+ ),
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = true, suggestions = emptyList()),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ ),
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ ),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ ),
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ ),
+ CodeBlock.Blank(isActive = false, suggestions = emptyList())
+ )
+ ),
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = true, suggestions = emptyList()),
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ ),
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ ),
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ )
+ )
+ )
+ )
+ )
+ )
+ val expectedStates = listOf(
+ initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
+ initialStates[1].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
+ initialStates[2].copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ ),
+ initialStates[3].copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ ),
+ initialStates[4].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
+ initialStates[5].copy(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ ),
+ initialStates[6].copy(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ )
+ )
+ )
+ )
+ )
+ )
+
+ initialStates.zip(expectedStates).forEach { (initialState, expectedState) ->
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
+ assertEquals(expectedState, state)
+ assertContainsDeleteButtonClickedAnalyticEvent(actions)
+ }
+ }
+
+ @Test
+ fun `DeleteButtonClicked should set previous code block as active if has code block before deleted`() {
+ val initialStates = listOf(
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = false, suggestions = emptyList()),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ ),
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ ),
+ CodeBlock.Blank(isActive = true, suggestions = emptyList())
+ )
+ ),
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(Suggestion.ConstantString("suggestion")),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ )
+ )
+ ),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ ),
+ StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = false, suggestions = emptyList()),
+ CodeBlock.Blank(isActive = true, suggestions = emptyList())
+ )
+ )
+ )
+ val expectedStates = listOf(
+ initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
+ initialStates[1].copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ ),
+ initialStates[2].copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(Suggestion.ConstantString("suggestion")),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ )
+ )
+ )
+ )
+ ),
+ initialStates[0].copy(codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList()))),
+ )
+
+ initialStates.zip(expectedStates).forEach { (initialState, expectedState) ->
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
+ assertEquals(expectedState, state)
+ assertContainsDeleteButtonClickedAnalyticEvent(actions)
+ }
+ }
+
+ @Test
+ fun `DeleteButtonClicked should replace single Print code block with Blank`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)))
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsDeleteButtonClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `DeleteButtonClicked should replace single Variable code block with Blank`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)))
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsDeleteButtonClickedAnalyticEvent(actions)
+ }
+
+ private fun assertContainsDeleteButtonClickedAnalyticEvent(actions: Set) {
+ assertTrue {
+ actions.any {
+ it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
+ it.analyticEvent is StepQuizCodeBlanksClickedDeleteHyperskillAnalyticEvent
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerEnterButtonClickedTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerEnterButtonClickedTest.kt
new file mode 100644
index 000000000..1f52b1479
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerEnterButtonClickedTest.kt
@@ -0,0 +1,114 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.StepRoute
+import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedEnterHyperskillAnalyticEvent
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
+
+class StepQuizCodeBlanksReducerEnterButtonClickedTest {
+ private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
+
+ @Test
+ fun `EnterButtonClicked should not update state if state is not Content`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Idle
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked)
+
+ assertEquals(initialState, state)
+ assertTrue(actions.isEmpty())
+ }
+
+ @Test
+ fun `EnterButtonClicked should log analytic event and not update state if no active code block`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = false,
+ suggestions = emptyList()
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked)
+
+ assertEquals(initialState, state)
+ assertContainsEnterButtonClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `EnterButtonClicked should log analytic event and add new active Blank block if active code block exists`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = true,
+ suggestions = emptyList()
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = false, suggestions = emptyList()),
+ CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print))
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsEnterButtonClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `EnterButtonClicked should add new active Blank block after active code block`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = true, suggestions = emptyList()),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = false, suggestions = emptyList()),
+ CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsEnterButtonClickedAnalyticEvent(actions)
+ }
+
+ private fun assertContainsEnterButtonClickedAnalyticEvent(actions: Set) {
+ assertTrue {
+ actions.any {
+ it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
+ it.analyticEvent is StepQuizCodeBlanksClickedEnterHyperskillAnalyticEvent
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerInitializeTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerInitializeTest.kt
new file mode 100644
index 000000000..ef8ad0c50
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerInitializeTest.kt
@@ -0,0 +1,59 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.Block
+import org.hyperskill.app.step.domain.model.Step
+import org.hyperskill.app.step.domain.model.StepRoute
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
+import org.hyperskill.step.domain.model.stub
+
+class StepQuizCodeBlanksReducerInitializeTest {
+ private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
+
+ @Test
+ fun `Initialize should return Content state with active Blank and Print and Variable and If suggestions`() {
+ val step = Step.stub(
+ id = 1,
+ block = Block.stub(options = Block.Options(codeBlanksVariables = listOf("a", "b")))
+ )
+
+ val message = StepQuizCodeBlanksFeature.InternalMessage.Initialize(step)
+ val (state, actions) = reducer.reduce(StepQuizCodeBlanksFeature.State.Idle, message)
+
+ val expectedState = StepQuizCodeBlanksFeature.State.Content(
+ step = step,
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = true,
+ suggestions = listOf(Suggestion.Print, Suggestion.Variable, Suggestion.IfStatement)
+ )
+ )
+ )
+
+ assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
+ assertEquals(expectedState.codeBlocks, state.codeBlocks)
+ assertTrue(actions.isEmpty())
+ }
+
+ @Test
+ fun `Initialize should return Content state with active Blank and Print suggestion`() {
+ val step = Step.stub(id = 1)
+
+ val message = StepQuizCodeBlanksFeature.InternalMessage.Initialize(step)
+ val (state, actions) = reducer.reduce(StepQuizCodeBlanksFeature.State.Idle, message)
+
+ val expectedState = StepQuizCodeBlanksFeature.State.Content(
+ step = step,
+ codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)))
+ )
+
+ assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
+ assertEquals(expectedState.codeBlocks, state.codeBlocks)
+ assertTrue(actions.isEmpty())
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerOnboardingTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerOnboardingTest.kt
new file mode 100644
index 000000000..e6f4e5daf
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerOnboardingTest.kt
@@ -0,0 +1,67 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.Step
+import org.hyperskill.app.step.domain.model.StepRoute
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.OnboardingState
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
+import org.hyperskill.step.domain.model.stub
+
+class StepQuizCodeBlanksReducerOnboardingTest {
+ private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
+
+ @Test
+ fun `Onboarding should be unavailable`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Idle
+ val (state, _) = reducer.reduce(
+ initialState,
+ StepQuizCodeBlanksFeature.InternalMessage.Initialize(Step.stub(id = 1))
+ )
+
+ assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
+ assertTrue(state.onboardingState is OnboardingState.Unavailable)
+ }
+
+ @Test
+ fun `Onboarding should be available`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Idle
+ val (state, _) = reducer.reduce(
+ initialState,
+ StepQuizCodeBlanksFeature.InternalMessage.Initialize(Step.stub(id = 47329))
+ )
+
+ assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
+ assertTrue(state.onboardingState is OnboardingState.HighlightSuggestions)
+ }
+
+ @Test
+ fun `Onboarding SuggestionClicked should update onboardingState to HighlightCallToActionButton`() {
+ val suggestion = Suggestion.ConstantString("suggestion")
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = null
+ )
+ )
+ )
+ ),
+ onboardingState = OnboardingState.HighlightSuggestions
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion)
+ val (state, _) = reducer.reduce(initialState, message)
+
+ assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
+ assertEquals(OnboardingState.HighlightCallToActionButton, state.onboardingState)
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerSpaceButtonClickedTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerSpaceButtonClickedTest.kt
new file mode 100644
index 000000000..b1295d945
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerSpaceButtonClickedTest.kt
@@ -0,0 +1,198 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.Block
+import org.hyperskill.app.step.domain.model.Step
+import org.hyperskill.app.step.domain.model.StepRoute
+import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedSpaceHyperskillAnalyticEvent
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
+import org.hyperskill.step.domain.model.stub
+
+class StepQuizCodeBlanksReducerSpaceButtonClickedTest {
+ private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
+
+ @Test
+ fun `SpaceButtonClicked should not update state if state is not Content`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Idle
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
+
+ assertEquals(initialState, state)
+ assertTrue(actions.isEmpty())
+ }
+
+ @Test
+ fun `SpaceButtonClicked should not update state if active Print block has no active child`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ )
+ )
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
+
+ assertEquals(initialState, state)
+ assertContainsSpaceButtonClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `SpaceButtonClicked should add a new child to active Print code block`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(Suggestion.ConstantString("suggestion")),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ )
+ )
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(Suggestion.ConstantString("suggestion")),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsSpaceButtonClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `SpaceButtonClicked should add a new child to active Variable code block`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(Suggestion.ConstantString("x")),
+ selectedSuggestion = Suggestion.ConstantString("x")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(Suggestion.ConstantString("suggestion")),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ )
+ )
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(Suggestion.ConstantString("x")),
+ selectedSuggestion = Suggestion.ConstantString("x")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(Suggestion.ConstantString("suggestion")),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsSpaceButtonClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `SpaceButtonClicked should add a new child with operations suggestions after closing parentheses`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = Step.stub(
+ id = 1,
+ block = Block.stub(
+ options = Block.Options(codeBlanksOperations = listOf("*", "+"))
+ )
+ ),
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(Suggestion.ConstantString(")")),
+ selectedSuggestion = Suggestion.ConstantString(")")
+ )
+ )
+ )
+ )
+ )
+
+ val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(Suggestion.ConstantString(")")),
+ selectedSuggestion = Suggestion.ConstantString(")")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = initialState.codeBlanksOperationsSuggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsSpaceButtonClickedAnalyticEvent(actions)
+ }
+
+ private fun assertContainsSpaceButtonClickedAnalyticEvent(actions: Set) {
+ assertTrue {
+ actions.any {
+ it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
+ it.analyticEvent is StepQuizCodeBlanksClickedSpaceHyperskillAnalyticEvent
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerSuggestionClickedTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerSuggestionClickedTest.kt
new file mode 100644
index 000000000..5eedd6de7
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerSuggestionClickedTest.kt
@@ -0,0 +1,263 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.StepRoute
+import org.hyperskill.app.step_quiz_code_blanks.domain.analytic.StepQuizCodeBlanksClickedSuggestionHyperskillAnalyticEvent
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksReducer
+
+class StepQuizCodeBlanksReducerSuggestionClickedTest {
+ private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null))
+
+ @Test
+ fun `SuggestionClicked should not update state if no active code block`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = false,
+ suggestions = emptyList()
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Print)
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ assertEquals(initialState, state)
+ assertContainsSuggestionClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `SuggestionClicked should not update state if suggestion does not exist`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = true,
+ suggestions = emptyList()
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.ConstantString("test"))
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ assertEquals(initialState, state)
+ assertContainsSuggestionClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `SuggestionClicked should not update state if state is not Content`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Idle
+ val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Print)
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ assertEquals(initialState, state)
+ assertTrue(actions.isEmpty())
+ }
+
+ @Test
+ fun `SuggestionClicked should update active Blank code block to Print if suggestion exists`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = true,
+ suggestions = listOf(Suggestion.Print)
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Print)
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = initialState.codeBlanksStringsSuggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsSuggestionClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `SuggestionClicked should update active Blank code block to Variable if suggestion exists`() {
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = true,
+ suggestions = listOf(Suggestion.Print, Suggestion.Variable)
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.Variable)
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = initialState.codeBlanksVariablesSuggestions,
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = initialState.codeBlanksStringsSuggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertEquals(expectedState, state)
+ assertContainsSuggestionClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `SuggestionClicked should update Print code block with selected suggestion`() {
+ val suggestion = Suggestion.ConstantString("suggestion")
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion)
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
+ assertEquals(suggestion, (state.codeBlocks[0] as CodeBlock.Print).children[0].selectedSuggestion)
+ assertContainsSuggestionClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `SuggestionClicked should update Variable code block with selected suggestion for name`() {
+ val suggestion = Suggestion.ConstantString("suggestion")
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion)
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = suggestion
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
+ assertEquals(expectedState.codeBlocks, state.codeBlocks)
+ assertContainsSuggestionClickedAnalyticEvent(actions)
+ }
+
+ @Test
+ fun `SuggestionClicked should update Variable code block with selected suggestion for value`() {
+ val suggestion = Suggestion.ConstantString("suggestion")
+ val initialState = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion)
+ val (state, actions) = reducer.reduce(initialState, message)
+
+ val expectedState = initialState.copy(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = listOf(suggestion),
+ selectedSuggestion = suggestion
+ )
+ )
+ )
+ )
+ )
+
+ assertTrue(state is StepQuizCodeBlanksFeature.State.Content)
+ assertEquals(expectedState.codeBlocks, state.codeBlocks)
+ assertContainsSuggestionClickedAnalyticEvent(actions)
+ }
+
+ private fun assertContainsSuggestionClickedAnalyticEvent(actions: Set) {
+ assertTrue {
+ actions.any {
+ it is StepQuizCodeBlanksFeature.InternalAction.LogAnalyticEvent &&
+ it.analyticEvent is StepQuizCodeBlanksClickedSuggestionHyperskillAnalyticEvent
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensionsTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensionsTest.kt
new file mode 100644
index 000000000..02dc52f29
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksStateExtensionsTest.kt
@@ -0,0 +1,85 @@
+package org.hyperskill.step_quiz_code_blanks.presentation
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.Block
+import org.hyperskill.app.step.domain.model.Step
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.presentation.activeCodeBlockIndex
+import org.hyperskill.app.step_quiz_code_blanks.presentation.isVariableSuggestionsAvailable
+import org.hyperskill.step.domain.model.stub
+
+class StepQuizCodeBlanksStateExtensionsTest {
+ @Test
+ fun `activeCodeBlockIndex should return null if no active code block`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = false, suggestions = emptyList()),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+ assertNull(state.activeCodeBlockIndex())
+ }
+
+ @Test
+ fun `activeCodeBlockIndex should return index of the active code block`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(isActive = false, suggestions = emptyList()),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+ assertEquals(1, state.activeCodeBlockIndex())
+ }
+
+ @Test
+ fun `isVariableSuggestionsAvailable should return true if variable suggestions are available`() {
+ val step = Step.stub(
+ id = 1,
+ block = Block.stub(options = Block.Options(codeBlanksVariables = listOf("a", "b")))
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = emptyList()
+ )
+
+ assertTrue(state.isVariableSuggestionsAvailable)
+ }
+
+ @Test
+ fun `isVariableSuggestionsAvailable should return false if variable suggestions are not available`() {
+ listOf(null, emptyList()).forEach { codeBlanksVariables ->
+ val step = Step.stub(
+ id = 1,
+ block = Block.stub(options = Block.Options(codeBlanksVariables = codeBlanksVariables))
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = emptyList()
+ )
+
+ assertFalse(state.isVariableSuggestionsAvailable)
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperIsDecreaseIndentLevelButtonHiddenTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperIsDecreaseIndentLevelButtonHiddenTest.kt
new file mode 100644
index 000000000..e69afd6c1
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperIsDecreaseIndentLevelButtonHiddenTest.kt
@@ -0,0 +1,104 @@
+package org.hyperskill.step_quiz_code_blanks.view
+
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.view.mapper.StepQuizCodeBlanksViewStateMapper
+import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
+import org.hyperskill.step_quiz_code_blanks.presentation.stub
+
+class StepQuizCodeBlanksViewStateMapperIsDecreaseIndentLevelButtonHiddenTest {
+ @Test
+ fun `isDecreaseIndentLevelButtonHidden should be true when no active code block`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = false,
+ suggestions = emptyList()
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDecreaseIndentLevelButtonHidden)
+ }
+
+ @Test
+ fun `isDecreaseIndentLevelButtonHidden should be true when active code block's indent level is less than 1`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(CodeBlock.Blank(isActive = true, indentLevel = 0, suggestions = emptyList()))
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDecreaseIndentLevelButtonHidden)
+ }
+
+ @Test
+ fun `isDecreaseIndentLevelButtonHidden should be false when active code block's indent level is 1 or more`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ indentLevel = 1,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertFalse(viewState.isDecreaseIndentLevelButtonHidden)
+ }
+
+ @Test
+ fun `isDecreaseIndentLevelButtonHidden should be true when previous code block is IfStatement`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.IfStatement(indentLevel = 1, children = emptyList()),
+ CodeBlock.Blank(isActive = true, indentLevel = 1, suggestions = emptyList())
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDecreaseIndentLevelButtonHidden)
+ }
+
+ @Test
+ fun `isDecreaseIndentLevelButtonHidden should be false when previous code block is not IfStatement`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(indentLevel = 1, children = emptyList()),
+ CodeBlock.Print(
+ indentLevel = 1,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertFalse(viewState.isDecreaseIndentLevelButtonHidden)
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperIsDeleteButtonEnabledTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperIsDeleteButtonEnabledTest.kt
new file mode 100644
index 000000000..15de1d104
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperIsDeleteButtonEnabledTest.kt
@@ -0,0 +1,403 @@
+package org.hyperskill.step_quiz_code_blanks.view
+
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.view.mapper.StepQuizCodeBlanksViewStateMapper
+import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
+import org.hyperskill.step_quiz_code_blanks.presentation.stub
+
+class StepQuizCodeBlanksViewStateMapperIsDeleteButtonEnabledTest {
+ @Test
+ fun `isDeleteButtonEnabled should be false when active code block is Blank and single`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print)))
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertFalse(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be true when active code block is Print and single`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = listOf(Suggestion.Print),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be true when Variable active name is unselected`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be true when Variable active name is selected`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[0]
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be true when Variable active value child index is greater than one`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[0]
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[1]
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be false when Variable name is selected and active value is unselected`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[0]
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertFalse(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be true when Variable name is selected and active value is selected`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[0]
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[1]
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be true when Variable name is unselected and active value is unselected`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be true when Variable name is unselected and active value is selected`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[0]
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be true when IfStatement active child index greater than zero`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.IfStatement(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[0]
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ @Test
+ fun `isDeleteButtonEnabled should be true when IfStatement child is selected`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.IfStatement(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[0]
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ /* ktlint-disable */
+ @Test
+ fun `isDeleteButtonEnabled should be true when IfStatement child is unselected and next code block on same indent level`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.IfStatement(
+ indentLevel = 1,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ ),
+ CodeBlock.Print(
+ indentLevel = 1,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isDeleteButtonEnabled)
+ }
+
+ /* ktlint-disable */
+ @Test
+ fun `isDeleteButtonEnabled should be false when IfStatement child is unselected and next code block on different indent level`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.IfStatement(
+ indentLevel = 1,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ ),
+ CodeBlock.Print(
+ indentLevel = 2,
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertFalse(viewState.isDeleteButtonEnabled)
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperIsSpaceButtonHiddenTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperIsSpaceButtonHiddenTest.kt
new file mode 100644
index 000000000..027aedd37
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperIsSpaceButtonHiddenTest.kt
@@ -0,0 +1,239 @@
+package org.hyperskill.step_quiz_code_blanks.view
+
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.hyperskill.app.step.domain.model.Block
+import org.hyperskill.app.step.domain.model.Step
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.view.mapper.StepQuizCodeBlanksViewStateMapper
+import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
+import org.hyperskill.step.domain.model.stub
+import org.hyperskill.step_quiz_code_blanks.presentation.stub
+
+class StepQuizCodeBlanksViewStateMapperIsSpaceButtonHiddenTest {
+ private val step = Step.stub(
+ id = 0,
+ block = Block.stub(
+ options = Block.Options(
+ codeBlanksOperations = listOf("+")
+ )
+ )
+ )
+
+ @Test
+ fun `isSpaceButtonHidden should be true when codeBlanksOperationsSuggestions is empty`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = Step.stub(id = 0),
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isSpaceButtonHidden)
+ }
+
+ @Test
+ fun `isSpaceButtonHidden should be true when no active code block`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = listOf(
+ CodeBlock.Blank(
+ isActive = false,
+ suggestions = emptyList()
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isSpaceButtonHidden)
+ }
+
+ @Test
+ fun `isSpaceButtonHidden should be true when active Print code block has no active child`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isSpaceButtonHidden)
+ }
+
+ @Test
+ fun `isSpaceButtonHidden should be true when active Print code block child has no selected suggestion`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isSpaceButtonHidden)
+ }
+
+ @Test
+ fun `isSpaceButtonHidden should be false when active Print code block child has selected suggestion`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("suggestion")
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertFalse(viewState.isSpaceButtonHidden)
+ }
+
+ /* ktlint-disable */
+ @Test
+ fun `isSpaceButtonHidden should be true when active Variable code block's first child has no selected suggestion`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isSpaceButtonHidden)
+ }
+
+ @Test
+ fun `isSpaceButtonHidden should be true when active Variable code block's second child has no selected suggestion`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("x")
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isSpaceButtonHidden)
+ }
+
+ @Test
+ fun `isSpaceButtonHidden should be false when active IfStatement code block child has selected suggestion`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = listOf(
+ CodeBlock.IfStatement(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = Suggestion.ConstantString("if")
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertFalse(viewState.isSpaceButtonHidden)
+ }
+
+ @Test
+ fun `isSpaceButtonHidden should be true when active IfStatement code block child has no selected suggestion`() {
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ step = step,
+ codeBlocks = listOf(
+ CodeBlock.IfStatement(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = emptyList(),
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.isSpaceButtonHidden)
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperSequencesTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperSequencesTest.kt
new file mode 100644
index 000000000..e7c16c1df
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperSequencesTest.kt
@@ -0,0 +1,170 @@
+package org.hyperskill.step_quiz_code_blanks.view
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.view.mapper.StepQuizCodeBlanksViewStateMapper
+import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
+import org.hyperskill.step_quiz_code_blanks.presentation.stub
+
+class StepQuizCodeBlanksViewStateMapperSequencesTest {
+ @Test
+ fun `map should return Idle view state for Idle state`() {
+ val state = StepQuizCodeBlanksFeature.State.Idle
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+ assertEquals(StepQuizCodeBlanksViewState.Idle, viewState)
+ }
+
+ @Test
+ fun `Content with active not filled Print`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+ val expectedViewState = StepQuizCodeBlanksViewState.Content(
+ codeBlocks = listOf(
+ StepQuizCodeBlanksViewState.CodeBlockItem.Print(
+ id = 0,
+ children = listOf(
+ StepQuizCodeBlanksViewState.CodeBlockChildItem(
+ id = 0,
+ isActive = true,
+ value = null
+ )
+ )
+ )
+ ),
+ suggestions = suggestions,
+ isDeleteButtonEnabled = true,
+ isSpaceButtonHidden = true,
+ isDecreaseIndentLevelButtonHidden = true
+ )
+
+ val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertEquals(expectedViewState, actualViewState)
+ }
+
+ @Test
+ fun `Content with sequence of filled Print and active Blank`() {
+ val printSuggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = printSuggestions,
+ selectedSuggestion = printSuggestions[0]
+ )
+ )
+ ),
+ CodeBlock.Blank(isActive = true, suggestions = listOf(Suggestion.Print))
+ )
+ )
+ val expectedViewState = StepQuizCodeBlanksViewState.Content(
+ codeBlocks = listOf(
+ StepQuizCodeBlanksViewState.CodeBlockItem.Print(
+ id = 0,
+ children = listOf(
+ StepQuizCodeBlanksViewState.CodeBlockChildItem(
+ id = 0,
+ isActive = false,
+ value = printSuggestions[0].text
+ )
+ )
+ ),
+ StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 1, isActive = true)
+ ),
+ suggestions = listOf(Suggestion.Print),
+ isDeleteButtonEnabled = true,
+ isSpaceButtonHidden = true,
+ isDecreaseIndentLevelButtonHidden = true
+ )
+
+ val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertEquals(expectedViewState, actualViewState)
+ }
+
+ @Test
+ fun `Content with sequence of filled Print and active not filled Print`() {
+ val printSuggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = printSuggestions,
+ selectedSuggestion = printSuggestions[0]
+ )
+ )
+ ),
+ CodeBlock.Print(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = printSuggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+ val expectedViewState = StepQuizCodeBlanksViewState.Content(
+ codeBlocks = listOf(
+ StepQuizCodeBlanksViewState.CodeBlockItem.Print(
+ id = 0,
+ children = listOf(
+ StepQuizCodeBlanksViewState.CodeBlockChildItem(
+ id = 0,
+ isActive = false,
+ value = printSuggestions[0].text
+ )
+ )
+ ),
+ StepQuizCodeBlanksViewState.CodeBlockItem.Print(
+ id = 1,
+ children = listOf(
+ StepQuizCodeBlanksViewState.CodeBlockChildItem(
+ id = 0,
+ isActive = true,
+ value = null
+ )
+ )
+ )
+ ),
+ suggestions = printSuggestions,
+ isDeleteButtonEnabled = true,
+ isSpaceButtonHidden = true,
+ isDecreaseIndentLevelButtonHidden = true
+ )
+
+ val actualViewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertEquals(expectedViewState, actualViewState)
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperSuggestionsTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperSuggestionsTest.kt
new file mode 100644
index 000000000..fbfaf6d78
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateMapperSuggestionsTest.kt
@@ -0,0 +1,114 @@
+package org.hyperskill.step_quiz_code_blanks.view
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertTrue
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlock
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.CodeBlockChild
+import org.hyperskill.app.step_quiz_code_blanks.domain.model.Suggestion
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature
+import org.hyperskill.app.step_quiz_code_blanks.view.mapper.StepQuizCodeBlanksViewStateMapper
+import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
+import org.hyperskill.step_quiz_code_blanks.presentation.stub
+
+class StepQuizCodeBlanksViewStateMapperSuggestionsTest {
+ @Test
+ fun `Non empty suggestions when active code block is Blank`() {
+ val suggestions = listOf(Suggestion.Print, Suggestion.Variable)
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = suggestions))
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertEquals(suggestions, viewState.suggestions)
+ }
+
+ @Test
+ fun `Empty suggestions when code block active child has selected suggestion`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = suggestions[0]
+ )
+ )
+ val codeBlocks = listOf(
+ CodeBlock.Print(children = children),
+ CodeBlock.Variable(children = children),
+ CodeBlock.IfStatement(children = children)
+ )
+
+ codeBlocks.forEach { codeBlock ->
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(codeBlocks = listOf(codeBlock))
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertTrue(viewState.suggestions.isEmpty())
+ }
+ }
+
+ @Test
+ fun `Non empty suggestions when code block active child is unselected`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ val codeBlocks = listOf(
+ CodeBlock.Print(children = children),
+ CodeBlock.Variable(children = children),
+ CodeBlock.IfStatement(children = children)
+ )
+
+ codeBlocks.forEach { codeBlock ->
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(codeBlocks = listOf(codeBlock))
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertEquals(suggestions, viewState.suggestions)
+ }
+ }
+
+ @Test
+ fun `Non empty suggestions when active code block is Variable and active child has no selected suggestion`() {
+ val suggestions = listOf(
+ Suggestion.ConstantString("1"),
+ Suggestion.ConstantString("2")
+ )
+ val state = StepQuizCodeBlanksFeature.State.Content.stub(
+ codeBlocks = listOf(
+ CodeBlock.Variable(
+ children = listOf(
+ CodeBlockChild.SelectSuggestion(
+ isActive = true,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ ),
+ CodeBlockChild.SelectSuggestion(
+ isActive = false,
+ suggestions = suggestions,
+ selectedSuggestion = null
+ )
+ )
+ )
+ )
+ )
+
+ val viewState = StepQuizCodeBlanksViewStateMapper.map(state)
+
+ assertTrue(viewState is StepQuizCodeBlanksViewState.Content)
+ assertEquals(suggestions, viewState.suggestions)
+ }
+}
\ No newline at end of file
diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateTest.kt
new file mode 100644
index 000000000..ac692810f
--- /dev/null
+++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/view/StepQuizCodeBlanksViewStateTest.kt
@@ -0,0 +1,39 @@
+package org.hyperskill.step_quiz_code_blanks.view
+
+import kotlin.test.Test
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.OnboardingState
+import org.hyperskill.app.step_quiz_code_blanks.view.model.StepQuizCodeBlanksViewState
+
+class StepQuizCodeBlanksViewStateTest {
+ @Test
+ fun `isActionButtonsHidden should be true when onboarding is available`() {
+ val viewState = stubContentViewState(onboardingState = OnboardingState.HighlightSuggestions)
+ assertTrue(viewState.isActionButtonsHidden)
+ }
+
+ @Test
+ fun `isActionButtonsHidden should be false when onboarding is unavailable`() {
+ val viewState = stubContentViewState(onboardingState = OnboardingState.Unavailable)
+ assertFalse(viewState.isActionButtonsHidden)
+ }
+
+ @Test
+ fun `isSuggestionsHighlightEffectActive should be true when onboardingState is HighlightSuggestions`() {
+ val viewState = stubContentViewState(onboardingState = OnboardingState.HighlightSuggestions)
+ assertTrue(viewState.isSuggestionsHighlightEffectActive)
+ }
+
+ private fun stubContentViewState(
+ onboardingState: OnboardingState
+ ): StepQuizCodeBlanksViewState.Content =
+ StepQuizCodeBlanksViewState.Content(
+ codeBlocks = emptyList(),
+ suggestions = emptyList(),
+ isDeleteButtonEnabled = false,
+ isSpaceButtonHidden = false,
+ isDecreaseIndentLevelButtonHidden = false,
+ onboardingState = onboardingState
+ )
+}
\ No newline at end of file