diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 357dfd7b5..46b86d224 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -158,6 +158,7 @@ MagicNumber:StepDelegate.kt$StepDelegate$25 MagicNumber:StepDelegate.kt$StepDelegate$5 MagicNumber:StepDelegate.kt$StepDelegate$50 + MagicNumber:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$2 MagicNumber:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$47580L MagicNumber:StudyPlanActivityAdapterDelegate.kt$StudyPlanActivityAdapterDelegate.ViewHolder$100f MagicNumber:SubscriptionSyncLoading.kt$0.5f @@ -205,7 +206,6 @@ ModifierReused:LeaderboardPlaceInfo.kt$Row( modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = placeNumber.toString(), style = MaterialTheme.typography.body2, color = colorResource(id = R.color.color_on_surface_alpha_60), modifier = Modifier.align(Alignment.CenterVertically) ) if (placeNumber in 1..3) { Image( painter = painterResource( id = when (placeNumber) { 1 -> org.hyperskill.app.android.R.drawable.ic_leaderboard_first_place 2 -> org.hyperskill.app.android.R.drawable.ic_leaderboard_second_place 3 -> org.hyperskill.app.android.R.drawable.ic_leaderboard_third_place else -> error("Place icon should not be visible for the place number $placeNumber") } ), contentDescription = null, modifier = modifier .requiredSize(24.dp) .align(Alignment.CenterVertically) ) } } 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 diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index 3d14111a9..0fd2617ca 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -584,6 +584,8 @@ 2CEEE03328916A3D00282849 /* ProblemOfDayViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEEE03228916A3D00282849 /* ProblemOfDayViewModel.swift */; }; 2CEEE03528916A6800282849 /* ProblemOfDayOutputProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEEE03428916A6800282849 /* ProblemOfDayOutputProtocol.swift */; }; 2CEEE03728917F1100282849 /* TimeIntervalExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEEE03628917F1100282849 /* TimeIntervalExtensions.swift */; }; + 2CEFEBE22C8AD43F0069567E /* StepQuizCodeBlanksElifStatementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEFEBE12C8AD43F0069567E /* StepQuizCodeBlanksElifStatementView.swift */; }; + 2CEFEBE42C8AD5280069567E /* StepQuizCodeBlanksElseStatementView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CEFEBE32C8AD5280069567E /* StepQuizCodeBlanksElseStatementView.swift */; }; 2CF0B4E629F9CEAF009C2A2D /* StudyPlanSectionActivitiesList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF0B4E529F9CEAF009C2A2D /* StudyPlanSectionActivitiesList.swift */; }; 2CF2DA3A27EC5B2D0055426D /* Assembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF2DA3927EC5B2D0055426D /* Assembly.swift */; }; 2CF34F912C2E8EAE0054477E /* CommentsContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CF34F902C2E8EAE0054477E /* CommentsContentView.swift */; }; @@ -1390,6 +1392,8 @@ 2CEEE03228916A3D00282849 /* ProblemOfDayViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemOfDayViewModel.swift; sourceTree = ""; }; 2CEEE03428916A6800282849 /* ProblemOfDayOutputProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemOfDayOutputProtocol.swift; sourceTree = ""; }; 2CEEE03628917F1100282849 /* TimeIntervalExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeIntervalExtensions.swift; sourceTree = ""; }; + 2CEFEBE12C8AD43F0069567E /* StepQuizCodeBlanksElifStatementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksElifStatementView.swift; sourceTree = ""; }; + 2CEFEBE32C8AD5280069567E /* StepQuizCodeBlanksElseStatementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StepQuizCodeBlanksElseStatementView.swift; sourceTree = ""; }; 2CF0B4E529F9CEAF009C2A2D /* StudyPlanSectionActivitiesList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StudyPlanSectionActivitiesList.swift; sourceTree = ""; }; 2CF2DA3927EC5B2D0055426D /* Assembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Assembly.swift; sourceTree = ""; }; 2CF34F902C2E8EAE0054477E /* CommentsContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsContentView.swift; sourceTree = ""; }; @@ -3639,6 +3643,8 @@ 2CBEE4C52C87003A004486E8 /* Conditions */ = { isa = PBXGroup; children = ( + 2CEFEBE12C8AD43F0069567E /* StepQuizCodeBlanksElifStatementView.swift */, + 2CEFEBE32C8AD5280069567E /* StepQuizCodeBlanksElseStatementView.swift */, 2CBEE4C62C870059004486E8 /* StepQuizCodeBlanksIfStatementView.swift */, ); path = Conditions; @@ -5235,6 +5241,7 @@ 2C0DB90728644F2C001EA35E /* CodeEditorView.swift in Sources */, 2CDA98452944590800ADE539 /* ProfileStatisticsView.swift in Sources */, 2C8DD40E2AFB907000FD5359 /* ShareStreakAction.swift in Sources */, + 2CEFEBE22C8AD43F0069567E /* StepQuizCodeBlanksElifStatementView.swift in Sources */, 2C7CB6822ADFDB45006F78DA /* UIFont+SizeOfString.swift in Sources */, 2CC4AAF1280DB513002276A0 /* WebOAuthService.swift in Sources */, 2CF2DA3A27EC5B2D0055426D /* Assembly.swift in Sources */, @@ -5583,6 +5590,7 @@ 2C93AF2529B34FE6004639E0 /* StepQuizPyCharmAssembly.swift in Sources */, 2CDA98412944512D00ADE539 /* ProfileSkeletonView.swift in Sources */, 2CEDE70729965B4D0032D399 /* RestartApplicationLocalNotification.swift in Sources */, + 2CEFEBE42C8AD5280069567E /* StepQuizCodeBlanksElseStatementView.swift in Sources */, 2CACBCBC2B7A12F1006D3AB2 /* UsersInterviewWidgetView.swift in Sources */, 2CDA98432944524D00ADE539 /* HomeSkeletonView.swift in Sources */, 2C9D493D29F07015000599AB /* StudyPlanSectionErrorView.swift in Sources */, diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Conditions/StepQuizCodeBlanksElifStatementView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Conditions/StepQuizCodeBlanksElifStatementView.swift new file mode 100644 index 000000000..8f4c66aec --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Conditions/StepQuizCodeBlanksElifStatementView.swift @@ -0,0 +1,73 @@ +import shared +import SwiftUI + +struct StepQuizCodeBlanksElifStatementView: View { + let elifStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemElifStatement + + let onChildTap: (StepQuizCodeBlanksViewStateCodeBlockChildItem) -> Void + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .center, spacing: LayoutInsets.smallInset) { + Text("elif") + .font(StepQuizCodeBlanksAppearance.blankFont) + .foregroundColor(StepQuizCodeBlanksAppearance.blankTextColor) + + ForEach(elifStatementItem.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 { + StepQuizCodeBlanksElifStatementView( + elifStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemElifStatement( + id: 0, + indentLevel: 0, + children: [ + StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: nil) + ] + ), + onChildTap: { _ in } + ) + + StepQuizCodeBlanksElifStatementView( + elifStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemElifStatement( + id: 0, + indentLevel: 0, + children: [ + StepQuizCodeBlanksViewStateCodeBlockChildItem(id: 0, isActive: true, value: "x") + ] + ), + onChildTap: { _ in } + ) + + StepQuizCodeBlanksElifStatementView( + elifStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemElifStatement( + 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/CodeBlocks/Conditions/StepQuizCodeBlanksElseStatementView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Conditions/StepQuizCodeBlanksElseStatementView.swift new file mode 100644 index 000000000..91a75e90e --- /dev/null +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/Conditions/StepQuizCodeBlanksElseStatementView.swift @@ -0,0 +1,45 @@ +import shared +import SwiftUI + +struct StepQuizCodeBlanksElseStatementView: View { + let elseStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemElseStatement + + var body: some View { + Text("else:") + .font(StepQuizCodeBlanksAppearance.blankFont) + .foregroundColor(StepQuizCodeBlanksAppearance.blankTextColor) + .padding(.horizontal, LayoutInsets.defaultInset) + .padding(.vertical, LayoutInsets.smallInset) + .frame(minHeight: StepQuizCodeBlanksCodeBlockChildTextView.Appearance.minHeight) + .background(Color(ColorPalette.violet400Alpha7)) + .addBorder( + color: elseStatementItem.isActive ? StepQuizCodeBlanksAppearance.activeBorderColor : .clear, + width: elseStatementItem.isActive ? 1 : 0, + cornerRadius: StepQuizCodeBlanksAppearance.cornerRadius + ) + .padding(.horizontal) + .animation(.default, value: elseStatementItem.isActive) + } +} + +#if DEBUG +#Preview { + StepQuizCodeBlanksElseStatementView( + elseStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemElseStatement( + id: 0, + indentLevel: 0, + isActive: true + ) + ) +} + +#Preview { + StepQuizCodeBlanksElseStatementView( + elseStatementItem: StepQuizCodeBlanksViewStateCodeBlockItemElseStatement( + id: 0, + indentLevel: 0, + isActive: false + ) + ) +} +#endif diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/StepQuizCodeBlanksCodeBlocksView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/StepQuizCodeBlanksCodeBlocksView.swift index 79f8d394f..9b5e4c634 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/StepQuizCodeBlanksCodeBlocksView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/StepQuizSubmodules/StepQuizCodeBlanks/Views/CodeBlocks/StepQuizCodeBlanksCodeBlocksView.swift @@ -78,6 +78,15 @@ struct StepQuizCodeBlanksCodeBlocksView: View { ifStatementItem: ifStatementItem, onChildTap: onChildTap ) + case .elifStatement(let elifStatementItem): + StepQuizCodeBlanksElifStatementView( + elifStatementItem: elifStatementItem, + onChildTap: onChildTap + ) + case .elseStatement(let elseStatementItem): + StepQuizCodeBlanksElseStatementView( + elseStatementItem: elseStatementItem + ) } } } 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 241028bbd..1266f9783 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 @@ -27,9 +27,15 @@ sealed class CodeBlock { internal fun areAllChildrenUnselected(): Boolean = children.all { it is CodeBlockChild.SelectSuggestion && it.selectedSuggestion == null } + internal fun areAllChildrenSelected(): Boolean = + children.all { it is CodeBlockChild.SelectSuggestion && it.selectedSuggestion != null } + internal fun hasAnySelectedChild(): Boolean = children.any { it is CodeBlockChild.SelectSuggestion && it.selectedSuggestion != null } + internal fun hasAnyUnselectedChild(): Boolean = + children.any { it is CodeBlockChild.SelectSuggestion && it.selectedSuggestion == null } + internal data class Blank( override val isActive: Boolean, override val indentLevel: Int = 0, @@ -38,7 +44,7 @@ sealed class CodeBlock { override val children: List = emptyList() override val analyticRepresentation: String - get() = "Blank(isActive=$isActive, suggestions=$suggestions)" + get() = "Blank(isActive=$isActive, indentLevel=$indentLevel, suggestions=$suggestions)" override fun toReplyString(): String = "" } @@ -52,7 +58,7 @@ sealed class CodeBlock { override val suggestions: List = emptyList() override val analyticRepresentation: String = - "Print(children=$children)" + "Print(indentLevel=$indentLevel, children=$children)" override fun toReplyString(): String = buildString { @@ -63,7 +69,7 @@ sealed class CodeBlock { } override fun toString(): String = - "Print(children=$children)" + "Print(indentLevel=$indentLevel, children=$children)" } internal data class Variable( @@ -81,7 +87,7 @@ sealed class CodeBlock { override val suggestions: List = emptyList() override val analyticRepresentation: String - get() = "Variable(children=$children)" + get() = "Variable(indentLevel=$indentLevel, children=$children)" override fun toReplyString(): String = buildString { @@ -92,7 +98,7 @@ sealed class CodeBlock { } override fun toString(): String = - "Variable(children=$children)" + "Variable(indentLevel=$indentLevel, children=$children)" } internal data class IfStatement( @@ -104,7 +110,7 @@ sealed class CodeBlock { override val suggestions: List = emptyList() override val analyticRepresentation: String - get() = "IfStatement(children=$children)" + get() = "IfStatement(indentLevel=$indentLevel, children=$children)" override fun toReplyString(): String = buildString { @@ -115,7 +121,46 @@ sealed class CodeBlock { } override fun toString(): String = - "IfStatement(children=$children)" + "IfStatement(indentLevel=$indentLevel, children=$children)" + } + + internal data class ElifStatement( + 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() = "ElifStatement(indentLevel=$indentLevel, children=$children)" + + override fun toReplyString(): String = + buildString { + append("elif ") + append(joinChildrenToReplyString(children)) + append(":") + } + + override fun toString(): String = + "ElifStatement(indentLevel=$indentLevel, children=$children)" + } + + internal data class ElseStatement( + override val isActive: Boolean, + override val indentLevel: Int = 0 + ) : CodeBlock() { + override val suggestions: List = emptyList() + + override val children: List = emptyList() + + override val analyticRepresentation: String + get() = "ElseStatement(isActive=$isActive, indentLevel=$indentLevel)" + + override fun toReplyString(): String = "else:" + + override fun toString(): String = + "ElseStatement(isActive=$isActive, indentLevel=$indentLevel)" } } @@ -144,10 +189,12 @@ internal fun CodeBlock.Companion.joinChildrenToReplyString(children: List): CodeBlock = when (this) { - is CodeBlock.Blank -> this + is CodeBlock.Blank, + is CodeBlock.ElseStatement -> this is CodeBlock.Print -> copy(children = children.cast()) is CodeBlock.Variable -> copy(children = children.cast()) is CodeBlock.IfStatement -> copy(children = children.cast()) + is CodeBlock.ElifStatement -> copy(children = children.cast()) } internal fun CodeBlock.updatedIndentLevel(indentLevel: Int): CodeBlock = @@ -156,4 +203,6 @@ internal fun CodeBlock.updatedIndentLevel(indentLevel: Int): CodeBlock = is CodeBlock.Print -> copy(indentLevel = indentLevel) is CodeBlock.Variable -> copy(indentLevel = indentLevel) is CodeBlock.IfStatement -> copy(indentLevel = indentLevel) + is CodeBlock.ElifStatement -> copy(indentLevel = indentLevel) + is CodeBlock.ElseStatement -> 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 0d95eaa5d..9ff56fa42 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 @@ -26,6 +26,20 @@ sealed class Suggestion { "IfStatement(text='$text')" } + data object ElifStatement : Suggestion() { + override val text: String = "elif" + + override val analyticRepresentation: String = + "ElifStatement(text='$text')" + } + + data object ElseStatement : Suggestion() { + override val text: String = "else" + + override val analyticRepresentation: String = + "ElseStatement(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 8cc1fe4e3..11857df8f 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 @@ -37,6 +37,9 @@ object StepQuizCodeBlanksFeature { internal val codeBlanksVariablesSuggestions: List = step.codeBlanksVariablesSuggestions() + internal val codeBlanksVariablesAndStringsSuggestions: List = + codeBlanksVariablesSuggestions + codeBlanksStringsSuggestions + internal val codeBlanksOperationsSuggestions: List = step.codeBlanksOperationsSuggestions() } 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 31f62faa4..be8532bdc 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 @@ -21,8 +21,8 @@ import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksF import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.Message import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.OnboardingState import org.hyperskill.app.step_quiz_code_blanks.presentation.StepQuizCodeBlanksFeature.State -import ru.nobird.app.core.model.cast import ru.nobird.app.core.model.mutate +import ru.nobird.app.core.model.slice import ru.nobird.app.presentation.redux.reducer.StateReducer private typealias StepQuizCodeBlanksReducerResult = Pair> @@ -89,8 +89,7 @@ class StepQuizCodeBlanksReducer( children = listOf( CodeBlockChild.SelectSuggestion( isActive = true, - suggestions = state.codeBlanksVariablesSuggestions + - state.codeBlanksStringsSuggestions, + suggestions = state.codeBlanksVariablesAndStringsSuggestions, selectedSuggestion = null ) ) @@ -117,16 +116,33 @@ class StepQuizCodeBlanksReducer( children = listOf( CodeBlockChild.SelectSuggestion( isActive = true, - suggestions = state.codeBlanksVariablesSuggestions + - state.codeBlanksStringsSuggestions, + suggestions = state.codeBlanksVariablesAndStringsSuggestions, selectedSuggestion = null ) ) ) - else -> activeCodeBlock + Suggestion.ElifStatement -> + CodeBlock.ElifStatement( + indentLevel = activeCodeBlock.indentLevel, + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = state.codeBlanksVariablesAndStringsSuggestions, + selectedSuggestion = null + ) + ) + ) + Suggestion.ElseStatement -> + CodeBlock.ElseStatement( + isActive = false, + indentLevel = activeCodeBlock.indentLevel + ) + is Suggestion.ConstantString -> activeCodeBlock } + is CodeBlock.Print, - is CodeBlock.IfStatement -> { + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement -> { activeCodeBlock.activeChildIndex()?.let { activeChildIndex -> val activeChild = activeCodeBlock.children[activeChildIndex] as CodeBlockChild.SelectSuggestion val newChildren = activeCodeBlock.children @@ -138,10 +154,10 @@ class StepQuizCodeBlanksReducer( ) ) } - .cast>() activeCodeBlock.updatedChildren(newChildren) } ?: activeCodeBlock } + is CodeBlock.Variable -> { activeCodeBlock.activeChildIndex()?.let { activeChildIndex -> activeCodeBlock.copy( @@ -168,13 +184,35 @@ class StepQuizCodeBlanksReducer( ) } ?: activeCodeBlock } + + is CodeBlock.ElseStatement -> activeCodeBlock } - val newCodeBlocks = state.codeBlocks.mutate { set(activeCodeBlockIndex, newCodeBlock) } + val newCodeBlocks = state.codeBlocks.mutate { + set(activeCodeBlockIndex, newCodeBlock) + + if (newCodeBlock is CodeBlock.ElseStatement && activeCodeBlock !== newCodeBlock) { + val blankInsertIndex = activeCodeBlockIndex + 1 + val blankIndentLevel = newCodeBlock.indentLevel + 1 + add( + blankInsertIndex, + createBlankCodeBlock( + isActive = true, + indentLevel = blankIndentLevel, + suggestions = getSuggestionsForBlankCodeBlock( + index = blankInsertIndex, + indentLevel = blankIndentLevel, + codeBlocks = this, + isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable + ) + ) + ) + } + } val isFulfilledOnboardingPrintCodeBlock = state.onboardingState is OnboardingState.HighlightSuggestions && - activeCodeBlock is CodeBlock.Print && activeCodeBlock.children.any { it.selectedSuggestion == null } && - newCodeBlock is CodeBlock.Print && newCodeBlock.children.all { it.selectedSuggestion != null } + activeCodeBlock is CodeBlock.Print && activeCodeBlock.hasAnyUnselectedChild() && + newCodeBlock is CodeBlock.Print && newCodeBlock.areAllChildrenSelected() val (onboardingState, onboardingActions) = if (isFulfilledOnboardingPrintCodeBlock) { OnboardingState.HighlightCallToActionButton to @@ -250,7 +288,8 @@ class StepQuizCodeBlanksReducer( val newChildren = when (targetCodeBlock) { is CodeBlock.Print, is CodeBlock.Variable, - is CodeBlock.IfStatement -> { + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement -> { targetCodeBlock.children.mapIndexed { index, child -> require(child is CodeBlockChild.SelectSuggestion) if (index == message.codeBlockChildItem.id) { @@ -260,7 +299,9 @@ class StepQuizCodeBlanksReducer( } } } - else -> null + null, + is CodeBlock.Blank, + is CodeBlock.ElseStatement -> null } val newCodeBlocks = state.codeBlocks.mutate { @@ -317,17 +358,27 @@ class StepQuizCodeBlanksReducer( } removeAt(activeCodeBlockIndex) } - val replaceActiveCodeWithBlank = { + val replaceActiveCodeBlockWithBlank = { set( activeCodeBlockIndex, createBlankCodeBlock( isActive = true, indentLevel = activeCodeBlock.indentLevel, - isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable + suggestions = getSuggestionsForBlankCodeBlock( + index = activeCodeBlockIndex, + indentLevel = activeCodeBlock.indentLevel, + codeBlocks = this, + isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable + ) ) ) } + val isNextCodeBlockHasSameIndentLevelOrTrue = state.codeBlocks + .getOrNull(activeCodeBlockIndex + 1) + ?.let { it.indentLevel == activeCodeBlock.indentLevel } + ?: true + when (activeCodeBlock) { is CodeBlock.Blank -> { if (state.codeBlocks.size > 1) { @@ -370,7 +421,7 @@ class StepQuizCodeBlanksReducer( removeActiveCodeBlockAndSetNextActive() else -> - replaceActiveCodeWithBlank() + replaceActiveCodeBlockWithBlank() } } is CodeBlock.Variable -> { @@ -409,51 +460,63 @@ class StepQuizCodeBlanksReducer( if (state.codeBlocks.size > 1) { removeActiveCodeBlockAndSetNextActive() } else { - replaceActiveCodeWithBlank() + replaceActiveCodeBlockWithBlank() } } } - is CodeBlock.IfStatement -> { + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement -> { val activeChildIndex = activeCodeBlock.activeChildIndex() ?: return@mutate - val activeChild = activeCodeBlock.children[activeChildIndex] - - val nextCodeBlock = state.codeBlocks.getOrNull(activeCodeBlockIndex + 1) + val activeChild = activeCodeBlock.children[activeChildIndex] as CodeBlockChild.SelectSuggestion when { - activeChild.selectedSuggestion != null -> + activeChild.selectedSuggestion != null -> { + val newChildren = activeCodeBlock.children.mutate { + set( + activeChildIndex, + activeChild.copy(selectedSuggestion = null) + ) + } set( activeCodeBlockIndex, - activeCodeBlock.copy( - children = activeCodeBlock.children.mutate { - set( - activeChildIndex, - activeChild.copy(selectedSuggestion = null) - ) - } - ) + activeCodeBlock.updatedChildren(newChildren) ) + } - activeChildIndex > 0 -> + activeChildIndex > 0 -> { + val newChildren = activeCodeBlock.children.mutate { + val previousChildIndex = activeChildIndex - 1 + val previousChild = this[previousChildIndex] as CodeBlockChild.SelectSuggestion + set( + previousChildIndex, + previousChild.copy(isActive = true) + ) + + removeAt(activeChildIndex) + } set( activeCodeBlockIndex, - activeCodeBlock.copy( - children = activeCodeBlock.children.mutate { - set( - activeChildIndex - 1, - this[activeChildIndex - 1].copy(isActive = true) - ) - removeAt(activeChildIndex) - } - ) + activeCodeBlock.updatedChildren(newChildren) ) + } (activeChildIndex == 0 || activeCodeBlock.areAllChildrenUnselected()) && - (nextCodeBlock?.let { it.indentLevel == activeCodeBlock.indentLevel } ?: true) -> + isNextCodeBlockHasSameIndentLevelOrTrue -> { if (state.codeBlocks.size > 1) { removeActiveCodeBlockAndSetNextActive() } else { - replaceActiveCodeWithBlank() + replaceActiveCodeBlockWithBlank() } + } + } + } + is CodeBlock.ElseStatement -> { + if (isNextCodeBlockHasSameIndentLevelOrTrue) { + if (state.codeBlocks.size > 1) { + removeActiveCodeBlockAndSetNextActive() + } else { + replaceActiveCodeBlockWithBlank() + } } } } @@ -484,7 +547,9 @@ class StepQuizCodeBlanksReducer( return if (activeCodeBlock != null) { val indentLevel = when (activeCodeBlock) { - is CodeBlock.IfStatement -> activeCodeBlock.indentLevel + 1 + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement, + is CodeBlock.ElseStatement -> activeCodeBlock.indentLevel + 1 else -> activeCodeBlock.indentLevel } @@ -493,15 +558,23 @@ class StepQuizCodeBlanksReducer( activeCodeBlockIndex, setCodeBlockIsActive(codeBlock = state.codeBlocks[activeCodeBlockIndex], isActive = false) ) + + val insertIndex = activeCodeBlockIndex + 1 add( - activeCodeBlockIndex + 1, + insertIndex, createBlankCodeBlock( isActive = true, indentLevel = indentLevel, - isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable + suggestions = getSuggestionsForBlankCodeBlock( + index = insertIndex, + indentLevel = indentLevel, + codeBlocks = this, + isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable + ) ) ) } + state.copy(codeBlocks = newCodeBlocks) to actions } else { state to actions @@ -534,7 +607,8 @@ class StepQuizCodeBlanksReducer( val newChildren = when (activeCodeBlock) { is CodeBlock.Print, is CodeBlock.Variable, - is CodeBlock.IfStatement -> { + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement -> { activeCodeBlock.activeChildIndex()?.let { activeChildIndex -> val activeChild = activeCodeBlock.children[activeChildIndex] as CodeBlockChild.SelectSuggestion @@ -561,15 +635,14 @@ class StepQuizCodeBlanksReducer( selectedSuggestion = null ) - activeCodeBlock.children - .mutate { - set(activeChildIndex, activeChild.copy(isActive = false)) - add(activeChildIndex + 1, newChild) - } - .cast>() + activeCodeBlock.children.mutate { + set(activeChildIndex, activeChild.copy(isActive = false)) + add(activeChildIndex + 1, newChild) + } } } - else -> null + is CodeBlock.Blank, + is CodeBlock.ElseStatement -> null } val newCodeBlocks = state.codeBlocks.mutate { @@ -612,7 +685,18 @@ class StepQuizCodeBlanksReducer( codeBlocks = state.codeBlocks.mutate { set( activeCodeBlockIndex, - activeCodeBlock.updatedIndentLevel(newIndentLevel) + when (activeCodeBlock) { + is CodeBlock.Blank -> activeCodeBlock.copy( + indentLevel = newIndentLevel, + suggestions = getSuggestionsForBlankCodeBlock( + index = activeCodeBlockIndex, + indentLevel = newIndentLevel, + codeBlocks = this, + isVariableSuggestionAvailable = state.isVariableSuggestionsAvailable + ) + ) + else -> activeCodeBlock.updatedIndentLevel(newIndentLevel) + } ) } ) to actions @@ -621,9 +705,11 @@ class StepQuizCodeBlanksReducer( private fun setCodeBlockIsActive(codeBlock: CodeBlock, isActive: Boolean): CodeBlock = when (codeBlock) { is CodeBlock.Blank -> codeBlock.copy(isActive = isActive) + is CodeBlock.ElseStatement -> codeBlock.copy(isActive = isActive) is CodeBlock.Print, is CodeBlock.Variable, - is CodeBlock.IfStatement -> { + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement -> { if (isActive) { if (codeBlock.activeChild() != null) { codeBlock @@ -651,17 +737,51 @@ class StepQuizCodeBlanksReducer( private fun createBlankCodeBlock( isActive: Boolean, indentLevel: Int, - isVariableSuggestionAvailable: Boolean + suggestions: List ): CodeBlock.Blank = CodeBlock.Blank( isActive = isActive, indentLevel = indentLevel, - suggestions = if (isVariableSuggestionAvailable) { + suggestions = suggestions + ) + + private fun getSuggestionsForBlankCodeBlock( + index: Int = -1, + indentLevel: Int = 0, + codeBlocks: List = emptyList(), + isVariableSuggestionAvailable: Boolean + ): List = + when { + areElifAndElseStatementsSuggestionsAvailable(index, indentLevel, codeBlocks) -> + listOf(Suggestion.Print, Suggestion.Variable, Suggestion.ElifStatement, Suggestion.ElseStatement) + + isVariableSuggestionAvailable -> listOf(Suggestion.Print, Suggestion.Variable, Suggestion.IfStatement) - } else { + + else -> listOf(Suggestion.Print) - } - ) + } + + internal fun areElifAndElseStatementsSuggestionsAvailable( + index: Int, + indentLevel: Int, + codeBlocks: List + ): Boolean { + if (index < 2 || codeBlocks.isEmpty()) { + return false + } + + val previousCodeBlock = codeBlocks + .slice(to = index) + .reversed() + .firstOrNull { it.indentLevel == indentLevel } + + return when (previousCodeBlock) { + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement -> true + else -> false + } + } private fun createInitialCodeBlocks(step: Step): List = if (step.id == 47580L) { @@ -714,7 +834,9 @@ class StepQuizCodeBlanksReducer( createBlankCodeBlock( isActive = true, indentLevel = 0, - isVariableSuggestionAvailable = StepQuizCodeBlanksFeature.isVariableSuggestionsAvailable(step) + suggestions = getSuggestionsForBlankCodeBlock( + isVariableSuggestionAvailable = StepQuizCodeBlanksFeature.isVariableSuggestionsAvailable(step) + ) ) ) } else { @@ -722,7 +844,9 @@ class StepQuizCodeBlanksReducer( createBlankCodeBlock( isActive = true, indentLevel = 0, - isVariableSuggestionAvailable = StepQuizCodeBlanksFeature.isVariableSuggestionsAvailable(step) + suggestions = getSuggestionsForBlankCodeBlock( + 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 899f8f17d..fb7f1d6b9 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 @@ -23,10 +23,13 @@ object StepQuizCodeBlanksViewStateMapper { val suggestions = when (activeCodeBlock) { - is CodeBlock.Blank -> activeCodeBlock.suggestions + is CodeBlock.Blank -> + activeCodeBlock.suggestions + is CodeBlock.Print, is CodeBlock.Variable, - is CodeBlock.IfStatement -> + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement -> (activeCodeBlock.activeChild() as? CodeBlockChild.SelectSuggestion)?.let { if (it.selectedSuggestion == null) { it.suggestions @@ -34,7 +37,9 @@ object StepQuizCodeBlanksViewStateMapper { emptyList() } } - null -> emptyList() + + null, + is CodeBlock.ElseStatement -> emptyList() } ?: emptyList() val isDeleteButtonEnabled = @@ -55,13 +60,16 @@ object StepQuizCodeBlanksViewStateMapper { } } ?: false } - is CodeBlock.IfStatement -> { + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement -> { activeCodeBlock.activeChildIndex()?.let { activeChildIndex -> + val activeChild = activeCodeBlock.children[activeChildIndex] as CodeBlockChild.SelectSuggestion + when { activeChildIndex > 0 -> true - activeCodeBlock.children[activeChildIndex].selectedSuggestion != null -> + activeChild.selectedSuggestion != null -> true else -> @@ -70,13 +78,17 @@ object StepQuizCodeBlanksViewStateMapper { } } ?: false } + is CodeBlock.ElseStatement -> + codeBlocks.getOrNull(activeCodeBlockIndex + 1) + ?.let { it.indentLevel == activeCodeBlock.indentLevel } ?: true null -> false } val isSpaceButtonHidden = if (state.codeBlanksOperationsSuggestions.isNotEmpty()) { when (activeCodeBlock) { is CodeBlock.Print, - is CodeBlock.IfStatement -> { + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement -> { val activeChild = activeCodeBlock.activeChild() as? CodeBlockChild.SelectSuggestion activeChild?.selectedSuggestion == null } @@ -88,17 +100,26 @@ object StepQuizCodeBlanksViewStateMapper { true } } - else -> true + null, + is CodeBlock.Blank, + is CodeBlock.ElseStatement -> true } } else { true } + val isPreviousCodeBlockCondition = + when (activeCodeBlockIndex?.let { state.codeBlocks.getOrNull(it - 1) }) { + is CodeBlock.IfStatement, + is CodeBlock.ElifStatement, + is CodeBlock.ElseStatement -> true + else -> false + } val isDecreaseIndentLevelButtonHidden = when { activeCodeBlock == null -> true activeCodeBlock.indentLevel < 1 -> true - state.codeBlocks.getOrNull(activeCodeBlockIndex - 1) is CodeBlock.IfStatement -> true + isPreviousCodeBlockCondition -> true else -> false } @@ -141,6 +162,18 @@ object StepQuizCodeBlanksViewStateMapper { indentLevel = codeBlock.indentLevel, children = codeBlock.children.mapIndexed(::mapCodeBlockChild) ) + is CodeBlock.ElifStatement -> + StepQuizCodeBlanksViewState.CodeBlockItem.ElifStatement( + id = index, + indentLevel = codeBlock.indentLevel, + children = codeBlock.children.mapIndexed(::mapCodeBlockChild) + ) + is CodeBlock.ElseStatement -> + StepQuizCodeBlanksViewState.CodeBlockItem.ElseStatement( + id = index, + indentLevel = codeBlock.indentLevel, + isActive = codeBlock.isActive + ) } private fun 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 0cddcf03b..2bf809df5 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 @@ -59,6 +59,20 @@ sealed interface StepQuizCodeBlanksViewState { override val indentLevel: Int = 0, override val children: List ) : CodeBlockItem + + data class ElifStatement( + override val id: Int, + override val indentLevel: Int = 0, + override val children: List + ) : CodeBlockItem + + data class ElseStatement( + override val id: Int, + override val indentLevel: Int = 0, + val isActive: Boolean + ) : CodeBlockItem { + override val children: List = emptyList() + } } data class CodeBlockChildItem( 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 index feea44ebf..9c864bf95 100644 --- 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 @@ -53,6 +53,43 @@ class StepQuizCodeBlanksReducerCodeBlockChildClickedTest { assertContainsCodeBlockChildClickedAnalyticEvent(actions) } + @Test + fun `CodeBlockChildClicked should not update state if target code block is Blank`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Blank( + isActive = false, + suggestions = emptyList() + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = false), + 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 not update state if target code block is ElseStatement`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf(CodeBlock.ElseStatement(isActive = false)) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.ElseStatement(id = 0, isActive = false), + 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( @@ -153,6 +190,86 @@ class StepQuizCodeBlanksReducerCodeBlockChildClickedTest { assertContainsCodeBlockChildClickedAnalyticEvent(actions) } + @Test + fun `CodeBlockChildClicked should update state to activate the clicked IfStatement child`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.IfStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.IfStatement(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.IfStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsCodeBlockChildClickedAnalyticEvent(actions) + } + + @Test + fun `CodeBlockChildClicked should update state to activate the clicked ElifStatement child`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.ElifStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockChildClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.ElifStatement(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.ElifStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsCodeBlockChildClickedAnalyticEvent(actions) + } + private fun assertContainsCodeBlockChildClickedAnalyticEvent(actions: Set) { assertTrue { actions.any { 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 index 7a88b7b36..84deb40e1 100644 --- 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 @@ -26,6 +26,36 @@ class StepQuizCodeBlanksReducerCodeBlockClickedTest { assertTrue(actions.isEmpty()) } + @Test + fun `CodeBlockClicked should not update state if no target code block`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 1, isActive = true) + ) + val (state, actions) = reducer.reduce(initialState, message) + + assertEquals(initialState, state) + assertContainsCodeBlockClickedAnalyticEvent(actions) + } + + @Test + fun `CodeBlockClicked should not update state if target code block is active`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 0, isActive = true) + ) + val (state, actions) = reducer.reduce(initialState, message) + + assertEquals(initialState, state) + assertContainsCodeBlockClickedAnalyticEvent(actions) + } + @Test fun `CodeBlockClicked should update active Print code block`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( @@ -64,7 +94,32 @@ class StepQuizCodeBlanksReducerCodeBlockClickedTest { } @Test - fun `CodeBlockClicked should update active Variable code block`() { + fun `CodeBlockClicked should update active ElseStatement code block`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Blank(isActive = false, suggestions = emptyList()), + CodeBlock.ElseStatement(isActive = true) + ) + ) + + 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.ElseStatement(isActive = false) + ) + ) + + assertEquals(expectedState, state) + assertContainsCodeBlockClickedAnalyticEvent(actions) + } + + @Test + fun `CodeBlockClicked should update active Variable code block to not active`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( codeBlocks = listOf( CodeBlock.Print( @@ -118,6 +173,61 @@ class StepQuizCodeBlanksReducerCodeBlockClickedTest { assertContainsCodeBlockClickedAnalyticEvent(actions) } + @Test + fun `CodeBlockClicked should update not active Variable code block to active`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + 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 + ) + ) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.CodeBlockClicked( + codeBlockItem = StepQuizCodeBlanksViewState.CodeBlockItem.Blank(id = 1, isActive = false) + ) + val (state, actions) = reducer.reduce(initialState, message) + + val expectedState = initialState.copy( + 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 + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsCodeBlockClickedAnalyticEvent(actions) + } + private fun assertContainsCodeBlockClickedAnalyticEvent(actions: Set) { assertTrue { actions.any { 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 index 63c0f39d7..9e16f08b9 100644 --- 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 @@ -6,12 +6,26 @@ 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.domain.model.Suggestion 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 state is not Content`() { + val initialState = StepQuizCodeBlanksFeature.State.Idle + + val (state, actions) = reducer.reduce( + initialState, + StepQuizCodeBlanksFeature.Message.DecreaseIndentLevelButtonClicked + ) + + assertEquals(initialState, state) + assertTrue(actions.isEmpty()) + } + @Test fun `DecreaseIndentLevelButtonClicked should not update state if no active code block`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( @@ -54,7 +68,44 @@ class StepQuizCodeBlanksReducerDecreaseIndentLevelButtonClickedTest { ) val expectedState = initialState.copy( - codeBlocks = listOf(CodeBlock.Blank(isActive = true, indentLevel = 0, suggestions = emptyList())) + codeBlocks = listOf( + CodeBlock.Blank( + isActive = true, + indentLevel = 0, + suggestions = listOf(Suggestion.Print) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsDecreaseIndentLevelAnalyticEvent(actions) + } + + @Test + fun `DecreaseIndentLevelButtonClicked should decrease indent level by 1 and update suggestions for Blank`() { + 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 = listOf(Suggestion.Print) + ) + ) ) assertEquals(expectedState, state) @@ -78,7 +129,7 @@ class StepQuizCodeBlanksReducerDecreaseIndentLevelButtonClickedTest { val expectedState = initialState.copy( codeBlocks = listOf( CodeBlock.Blank(isActive = false, indentLevel = 3, suggestions = emptyList()), - CodeBlock.Blank(isActive = true, indentLevel = 1, suggestions = emptyList()) + CodeBlock.Blank(isActive = true, indentLevel = 1, suggestions = listOf(Suggestion.Print)) ) ) 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 index 2ab0d37fe..c01e8a93e 100644 --- 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 @@ -62,43 +62,6 @@ class StepQuizCodeBlanksReducerDeleteButtonClickedTest { 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( @@ -402,6 +365,84 @@ class StepQuizCodeBlanksReducerDeleteButtonClickedTest { } } + @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 remove child for Print code block`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ), + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.Print( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + @Test fun `DeleteButtonClicked should replace single Print code block with Blank`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( @@ -428,6 +469,94 @@ class StepQuizCodeBlanksReducerDeleteButtonClickedTest { assertContainsDeleteButtonClickedAnalyticEvent(actions) } + @Test + fun `DeleteButtonClicked should clear suggestion if active Variable code block has selected suggestion`() { + 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 = suggestion + ) + ) + ) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should remove child for Variable code block`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ), + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.Variable( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + @Test fun `DeleteButtonClicked should replace single Variable code block with Blank`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( @@ -459,6 +588,336 @@ class StepQuizCodeBlanksReducerDeleteButtonClickedTest { assertContainsDeleteButtonClickedAnalyticEvent(actions) } + @Test + fun `DeleteButtonClicked should remove Variable code block and set previous active`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Blank(isActive = false, suggestions = emptyList()), + 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 = emptyList())) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should remove Variable code block and set next active`() { + 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 + ) + ) + ), + CodeBlock.Blank(isActive = false, suggestions = emptyList()) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + val expectedState = initialState.copy( + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should clear suggestion if active IfStatement code block has selected suggestion`() { + val suggestion = Suggestion.ConstantString("suggestion") + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.IfStatement( + 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.IfStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = listOf(suggestion), + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should remove child for IfStatement code block`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.IfStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ), + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.IfStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = Suggestion.ConstantString("suggestion") + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should replace single IfStatement code block with Blank`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.IfStatement( + 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) + } + + /* ktlint-disable */ + @Test + fun `DeleteButtonClicked should not replace single IfStatement code block with Blank when next code block has different indent level`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.IfStatement( + indentLevel = 0, + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ), + CodeBlock.Blank(indentLevel = 1, isActive = false, suggestions = emptyList()) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + assertEquals(initialState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should remove IfStatement code block and set previous active`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Blank(isActive = false, suggestions = emptyList()), + CodeBlock.IfStatement( + 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 = emptyList())) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should remove IfStatement code block and set next active`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.IfStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ), + CodeBlockChild.SelectSuggestion( + isActive = false, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ), + CodeBlock.Blank(isActive = false, suggestions = emptyList()) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + val expectedState = initialState.copy( + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + /* ktlint-disable */ + @Test + fun `DeleteButtonClicked should not replace single ElseStatement code block with Blank when next code block has different indent level`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.ElseStatement( + indentLevel = 0, + isActive = true + ), + CodeBlock.Blank(indentLevel = 1, isActive = false, suggestions = emptyList()) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + assertEquals(initialState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should replace single ElseStatement code block with Blank`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf(CodeBlock.ElseStatement(isActive = true)) + ) + + 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 remove ElseStatement code block and set previous active`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Blank(isActive = false, suggestions = emptyList()), + CodeBlock.ElseStatement(isActive = true) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + val expectedState = initialState.copy( + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + + @Test + fun `DeleteButtonClicked should remove ElseStatement code block and set next active`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.ElseStatement(isActive = true), + CodeBlock.Blank(isActive = false, suggestions = emptyList()) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.DeleteButtonClicked) + + val expectedState = initialState.copy( + codeBlocks = listOf(CodeBlock.Blank(isActive = true, suggestions = emptyList())) + ) + + assertEquals(expectedState, state) + assertContainsDeleteButtonClickedAnalyticEvent(actions) + } + private fun assertContainsDeleteButtonClickedAnalyticEvent(actions: Set) { assertTrue { actions.any { diff --git a/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerElifAndElseStatementsSuggestionsAvailabilityTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerElifAndElseStatementsSuggestionsAvailabilityTest.kt new file mode 100644 index 000000000..a9ff044f7 --- /dev/null +++ b/shared/src/commonTest/kotlin/org/hyperskill/step_quiz_code_blanks/presentation/StepQuizCodeBlanksReducerElifAndElseStatementsSuggestionsAvailabilityTest.kt @@ -0,0 +1,74 @@ +package org.hyperskill.step_quiz_code_blanks.presentation + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +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.presentation.StepQuizCodeBlanksReducer + +class StepQuizCodeBlanksReducerElifAndElseStatementsSuggestionsAvailabilityTest { + private val reducer = StepQuizCodeBlanksReducer(StepRoute.Learn.Step(1, null)) + + @Test + fun `Should return false if index is less than 2`() { + val result = + reducer.areElifAndElseStatementsSuggestionsAvailable(index = 1, indentLevel = 0, codeBlocks = emptyList()) + assertFalse(result) + } + + @Test + fun `Should return false if codeBlocks is empty`() { + val result = + reducer.areElifAndElseStatementsSuggestionsAvailable(index = 2, indentLevel = 0, codeBlocks = emptyList()) + assertFalse(result) + } + + @Test + fun `Should return false if no previous code block at same indent level`() { + val codeBlocks = listOf( + CodeBlock.IfStatement(indentLevel = 0, children = emptyList()), + CodeBlock.Print(indentLevel = 1, children = emptyList()) + ) + val result = + reducer.areElifAndElseStatementsSuggestionsAvailable(index = 2, indentLevel = 1, codeBlocks = codeBlocks) + assertFalse(result) + } + + @Test + fun `Should return true if previous code block is IfStatement at same indent level`() { + val codeBlocks = listOf( + CodeBlock.IfStatement(indentLevel = 0, children = emptyList()), + CodeBlock.Print(indentLevel = 1, children = emptyList()) + ) + val result = + reducer.areElifAndElseStatementsSuggestionsAvailable(index = 2, indentLevel = 0, codeBlocks = codeBlocks) + assertTrue(result) + } + + @Test + fun `Should return true if previous code block is IfStatement at same indent level nested`() { + val codeBlocks = listOf( + CodeBlock.IfStatement(indentLevel = 0, children = emptyList()), + CodeBlock.Print(indentLevel = 1, children = emptyList()), + CodeBlock.IfStatement(indentLevel = 1, children = emptyList()), + CodeBlock.Print(indentLevel = 2, children = emptyList()) + ) + val result = + reducer.areElifAndElseStatementsSuggestionsAvailable(index = 4, indentLevel = 1, codeBlocks = codeBlocks) + assertTrue(result) + } + + @Test + fun `Should return true if previous code block is ElifStatement at same indent level`() { + val codeBlocks = listOf( + CodeBlock.IfStatement(indentLevel = 0, children = emptyList()), + CodeBlock.Print(indentLevel = 1, children = emptyList()), + CodeBlock.ElifStatement(indentLevel = 0, children = emptyList()), + CodeBlock.Print(indentLevel = 1, children = emptyList()) + ) + val result = + reducer.areElifAndElseStatementsSuggestionsAvailable(index = 4, indentLevel = 0, codeBlocks = codeBlocks) + assertTrue(result) + } +} \ 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 index 1f52b1479..b25d8cd19 100644 --- 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 @@ -24,7 +24,7 @@ class StepQuizCodeBlanksReducerEnterButtonClickedTest { } @Test - fun `EnterButtonClicked should log analytic event and not update state if no active code block`() { + fun `EnterButtonClicked should not update state if no active code block`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( codeBlocks = listOf( CodeBlock.Blank( @@ -41,7 +41,7 @@ class StepQuizCodeBlanksReducerEnterButtonClickedTest { } @Test - fun `EnterButtonClicked should log analytic event and add new active Blank block if active code block exists`() { + fun `EnterButtonClicked should append new active Blank block if active code block exists`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( codeBlocks = listOf( CodeBlock.Blank( @@ -103,6 +103,41 @@ class StepQuizCodeBlanksReducerEnterButtonClickedTest { assertContainsEnterButtonClickedAnalyticEvent(actions) } + @Test + fun `EnterButtonClicked should add Blank with next indentLevel if active code block is condition`() { + val codeBlocks = listOf( + CodeBlock.IfStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ), + CodeBlock.ElifStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = emptyList(), + selectedSuggestion = null + ) + ) + ), + CodeBlock.ElseStatement(isActive = true) + ) + + codeBlocks.forEach { codeBlock -> + val initialState = StepQuizCodeBlanksFeature.State.Content.stub(codeBlocks = listOf(codeBlock)) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.EnterButtonClicked) + + assertTrue(state is StepQuizCodeBlanksFeature.State.Content) + assertEquals(1, state.codeBlocks[1].indentLevel) + assertContainsEnterButtonClickedAnalyticEvent(actions) + } + } + private fun assertContainsEnterButtonClickedAnalyticEvent(actions: Set) { assertTrue { actions.any { 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 index b1295d945..3c8166d55 100644 --- 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 @@ -27,7 +27,7 @@ class StepQuizCodeBlanksReducerSpaceButtonClickedTest { } @Test - fun `SpaceButtonClicked should not update state if active Print block has no active child`() { + fun `SpaceButtonClicked should not update state if no active code block`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( codeBlocks = listOf( CodeBlock.Print( @@ -48,6 +48,35 @@ class StepQuizCodeBlanksReducerSpaceButtonClickedTest { assertContainsSpaceButtonClickedAnalyticEvent(actions) } + @Test + fun `SpaceButtonClicked should not update state if active code block is Blank`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Blank( + isActive = true, + suggestions = emptyList() + ) + ) + ) + + val (state, actions) = reducer.reduce(initialState, StepQuizCodeBlanksFeature.Message.SpaceButtonClicked) + + assertEquals(initialState, state) + assertContainsSpaceButtonClickedAnalyticEvent(actions) + } + + @Test + fun `SpaceButtonClicked should not update state if active code block is ElseStatement`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf(CodeBlock.ElseStatement(isActive = true)) + ) + + 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( 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 index 5eedd6de7..5e3970aff 100644 --- 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 @@ -61,7 +61,7 @@ class StepQuizCodeBlanksReducerSuggestionClickedTest { } @Test - fun `SuggestionClicked should update active Blank code block to Print if suggestion exists`() { + fun `SuggestionClicked should update Blank to Print`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( codeBlocks = listOf( CodeBlock.Blank( @@ -93,7 +93,7 @@ class StepQuizCodeBlanksReducerSuggestionClickedTest { } @Test - fun `SuggestionClicked should update active Blank code block to Variable if suggestion exists`() { + fun `SuggestionClicked should update Blank to Variable`() { val initialState = StepQuizCodeBlanksFeature.State.Content.stub( codeBlocks = listOf( CodeBlock.Blank( @@ -129,6 +129,102 @@ class StepQuizCodeBlanksReducerSuggestionClickedTest { assertContainsSuggestionClickedAnalyticEvent(actions) } + @Test + fun `SuggestionClicked should update Blank to IfStatement`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Blank( + isActive = true, + suggestions = listOf(Suggestion.Print, Suggestion.IfStatement) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.IfStatement) + val (state, actions) = reducer.reduce(initialState, message) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.IfStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = initialState.codeBlanksVariablesAndStringsSuggestions, + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsSuggestionClickedAnalyticEvent(actions) + } + + @Test + fun `SuggestionClicked should update Blank to ElifStatement`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Blank( + isActive = true, + suggestions = listOf(Suggestion.Print, Suggestion.ElifStatement) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.ElifStatement) + val (state, actions) = reducer.reduce(initialState, message) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.ElifStatement( + children = listOf( + CodeBlockChild.SelectSuggestion( + isActive = true, + suggestions = initialState.codeBlanksVariablesAndStringsSuggestions, + selectedSuggestion = null + ) + ) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsSuggestionClickedAnalyticEvent(actions) + } + + @Test + fun `SuggestionClicked should update Blank to ElseStatement and add Blank with next indentLevel`() { + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.Blank( + isActive = true, + suggestions = listOf(Suggestion.Print, Suggestion.ElseStatement) + ) + ) + ) + + val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(Suggestion.ElseStatement) + val (state, actions) = reducer.reduce(initialState, message) + + val expectedState = initialState.copy( + codeBlocks = listOf( + CodeBlock.ElseStatement( + isActive = false, + indentLevel = 0 + ), + CodeBlock.Blank( + isActive = true, + indentLevel = 1, + suggestions = listOf(Suggestion.Print) + ) + ) + ) + + assertEquals(expectedState, state) + assertContainsSuggestionClickedAnalyticEvent(actions) + } + @Test fun `SuggestionClicked should update Print code block with selected suggestion`() { val suggestion = Suggestion.ConstantString("suggestion") @@ -154,6 +250,56 @@ class StepQuizCodeBlanksReducerSuggestionClickedTest { assertContainsSuggestionClickedAnalyticEvent(actions) } + @Test + fun `SuggestionClicked should update IfStatement code block with selected suggestion`() { + val suggestion = Suggestion.ConstantString("suggestion") + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.IfStatement( + 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.IfStatement).children[0].selectedSuggestion) + assertContainsSuggestionClickedAnalyticEvent(actions) + } + + @Test + fun `SuggestionClicked should update ElifStatement code block with selected suggestion`() { + val suggestion = Suggestion.ConstantString("suggestion") + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf( + CodeBlock.ElifStatement( + 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.ElifStatement).children[0].selectedSuggestion) + assertContainsSuggestionClickedAnalyticEvent(actions) + } + @Test fun `SuggestionClicked should update Variable code block with selected suggestion for name`() { val suggestion = Suggestion.ConstantString("suggestion") @@ -252,6 +398,20 @@ class StepQuizCodeBlanksReducerSuggestionClickedTest { assertContainsSuggestionClickedAnalyticEvent(actions) } + @Test + fun `SuggestionClicked should not update ElseStatement code block with selected suggestion`() { + val suggestion = Suggestion.ConstantString("suggestion") + val initialState = StepQuizCodeBlanksFeature.State.Content.stub( + codeBlocks = listOf(CodeBlock.ElseStatement(isActive = true)) + ) + + val message = StepQuizCodeBlanksFeature.Message.SuggestionClicked(suggestion) + val (state, actions) = reducer.reduce(initialState, message) + + assertEquals(initialState, state) + assertContainsSuggestionClickedAnalyticEvent(actions) + } + private fun assertContainsSuggestionClickedAnalyticEvent(actions: Set) { assertTrue { actions.any {