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