From de49a312aaefc68672bca572fb79b664ad515d9a Mon Sep 17 00:00:00 2001 From: Apoorv Srivastava <2000apoorv@gmail.com> Date: Mon, 16 Aug 2021 17:44:08 +0530 Subject: [PATCH] nit fixes and added more tests --- .../ExplorationProgressControllerTest.kt | 229 ++++++++++++++++++ model/src/main/proto/exploration.proto | 26 +- .../main/proto/exploration_checkpoint.proto | 1 + 3 files changed, 245 insertions(+), 11 deletions(-) diff --git a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt index 9ceb021184e..f83a18df79f 100644 --- a/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/exploration/ExplorationProgressControllerTest.kt @@ -88,8 +88,10 @@ import org.oppia.android.util.logging.LogLevel import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode import java.io.FileNotFoundException +import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton +import org.oppia.android.app.model.HelpIndex // For context: // https://github.com/oppia/oppia/blob/37285a/extensions/interactions/Continue/directives/oppia-interactive-continue.directive.ts. @@ -1127,6 +1129,233 @@ class ExplorationProgressControllerTest { assertThat(updatedState.state.interaction.solution.solutionIsRevealed).isTrue() } + @Test + fun testHintsAndSolution_noHintVisible_checkHelpIndexIsCorrect() { + subscribeToCurrentStateToAllowExplorationToLoad() + playExploration( + profileId.internalId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2, + shouldSavePartialProgress = true + ) + playThroughPrototypeState1AndMoveToNextState() + + // Verify that the helpIndex.IndexTypeCase is equal to INDEX_TYPE_NOT_SET because no hint + // is visible yet. + verify(mockCurrentStateLiveDataObserver, atLeastOnce()) + .onChanged(currentStateResultCaptor.capture()) + assertThat(currentStateResultCaptor.value.isSuccess()).isTrue() + val currentState = currentStateResultCaptor.value.getOrThrow() + assertThat(currentState.helpIndex.indexTypeCase) + .isEqualTo(HelpIndex.IndexTypeCase.INDEXTYPE_NOT_SET) + } + + @Test + fun testHintsAndSolution_wait60Seconds_unrevealedHintIsVisible_checkHelpIndexIsCorrect() { + subscribeToCurrentStateToAllowExplorationToLoad() + playExploration( + profileId.internalId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2, + shouldSavePartialProgress = true + ) + playThroughPrototypeState1AndMoveToNextState() + // Make the first hint visible by submitting two wrong answers. + testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(60)) + testCoroutineDispatchers.runCurrent() + + // Verify that the helpIndex.IndexTypeCase is equal AVAILABLE_NEXT_HINT_HINT_INDEX because a new + // unrevealed hint is visible. + verify(mockCurrentStateLiveDataObserver, atLeastOnce()) + .onChanged(currentStateResultCaptor.capture()) + assertThat(currentStateResultCaptor.value.isSuccess()).isTrue() + val currentState = currentStateResultCaptor.value.getOrThrow() + assertThat(currentState.state.interaction.hintList[0].hintIsRevealed).isFalse() + assertThat(currentState.helpIndex.indexTypeCase) + .isEqualTo(HelpIndex.IndexTypeCase.AVAILABLE_NEXT_HINT_INDEX) + assertThat(currentState.helpIndex.availableNextHintIndex).isEqualTo(0) + } + + @Test + fun testHintsAndSolution_submitTwoWrongAnswers_unrevealedHintIsVisible_checkHelpIndexIsCorrect() { + subscribeToCurrentStateToAllowExplorationToLoad() + playExploration( + profileId.internalId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2, + shouldSavePartialProgress = true + ) + playThroughPrototypeState1AndMoveToNextState() + // Make the first hint visible by submitting two wrong answers. + submitWrongAnswerForPrototypeState2() + submitWrongAnswerForPrototypeState2() + + // Verify that the helpIndex.IndexTypeCase is equal AVAILABLE_NEXT_HINT_HINT_INDEX because a new + // unrevealed hint is visible. + verify(mockCurrentStateLiveDataObserver, atLeastOnce()) + .onChanged(currentStateResultCaptor.capture()) + assertThat(currentStateResultCaptor.value.isSuccess()).isTrue() + val currentState = currentStateResultCaptor.value.getOrThrow() + assertThat(currentState.state.interaction.hintList[0].hintIsRevealed).isFalse() + assertThat(currentState.helpIndex.indexTypeCase) + .isEqualTo(HelpIndex.IndexTypeCase.AVAILABLE_NEXT_HINT_INDEX) + assertThat(currentState.helpIndex.availableNextHintIndex).isEqualTo(0) + } + + @Test + fun testHintsAndSolution_revealedHintIsVisible_checkHelpIndexIsCorrect() { + subscribeToCurrentStateToAllowExplorationToLoad() + playExploration( + profileId.internalId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2, + shouldSavePartialProgress = true + ) + playThroughPrototypeState1AndMoveToNextState() + submitWrongAnswerForPrototypeState2() + submitWrongAnswerForPrototypeState2() + + val result = explorationProgressController.submitHintIsRevealed( + hintIsRevealed = true, + hintIndex = 0, + ) + result.observeForever(mockAsyncHintObserver) + testCoroutineDispatchers.runCurrent() + + // Verify that the helpIndex.IndexTypeCase is equal LATEST_REVEALED_HINT_INDEX because a new + // revealed hint is visible. + verify(mockCurrentStateLiveDataObserver, atLeastOnce()) + .onChanged(currentStateResultCaptor.capture()) + assertThat(currentStateResultCaptor.value.isSuccess()).isTrue() + val currentState = currentStateResultCaptor.value.getOrThrow() + assertThat(currentState.state.interaction.hintList[0].hintIsRevealed).isTrue() + assertThat(currentState.state.interaction.solution.solutionIsRevealed).isFalse() + assertThat(currentState.helpIndex.indexTypeCase) + .isEqualTo(HelpIndex.IndexTypeCase.LATEST_REVEALED_HINT_INDEX) + assertThat(currentState.helpIndex.latestRevealedHintIndex).isEqualTo(0) + } + + @Test + fun testHintsAndSolution_allHintsVisible_wait30Seconds_solutionVisible_checkHelpIndexIsCorrect() { + subscribeToCurrentStateToAllowExplorationToLoad() + playExploration( + profileId.internalId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2, + shouldSavePartialProgress = true + ) + playThroughPrototypeState1AndMoveToNextState() + submitWrongAnswerForPrototypeState2() + submitWrongAnswerForPrototypeState2() + + val result = explorationProgressController.submitHintIsRevealed( + hintIsRevealed = true, + hintIndex = 0, + ) + result.observeForever(mockAsyncHintObserver) + testCoroutineDispatchers.runCurrent() + + // The solution should be visible after 30 seconds of the last hint being reveled. + testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) + testCoroutineDispatchers.runCurrent() + + // Verify that the helpIndex.IndexTypeCase is equal SHOW_SOLUTION because unrevealed solution is + // visible. + verify(mockCurrentStateLiveDataObserver, atLeastOnce()) + .onChanged(currentStateResultCaptor.capture()) + assertThat(currentStateResultCaptor.value.isSuccess()).isTrue() + val currentState = currentStateResultCaptor.value.getOrThrow() + assertThat(currentState.state.interaction.hintList[0].hintIsRevealed).isTrue() + assertThat(currentState.state.interaction.solution.solutionIsRevealed).isFalse() + assertThat(currentState.helpIndex.indexTypeCase) + .isEqualTo(HelpIndex.IndexTypeCase.SHOW_SOLUTION) + } + + @Test + fun testHintAndSol_hintsVisible_submitWrongAns_wait10Seconds_solVisible_checkHelpIndexIsCorrect() { + subscribeToCurrentStateToAllowExplorationToLoad() + playExploration( + profileId.internalId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2, + shouldSavePartialProgress = true + ) + playThroughPrototypeState1AndMoveToNextState() + submitWrongAnswerForPrototypeState2() + submitWrongAnswerForPrototypeState2() + + val result = explorationProgressController.submitHintIsRevealed( + hintIsRevealed = true, + hintIndex = 0, + ) + result.observeForever(mockAsyncHintObserver) + testCoroutineDispatchers.runCurrent() + + submitWrongAnswerForPrototypeState2() + // The solution should be visible after 10 seconds becuase one wrong answer was submitted. + testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) + testCoroutineDispatchers.runCurrent() + + // Verify that the helpIndex.IndexTypeCase is equal SHOW_SOLUTION because unrevealed solution is + // visible. + verify(mockCurrentStateLiveDataObserver, atLeastOnce()) + .onChanged(currentStateResultCaptor.capture()) + assertThat(currentStateResultCaptor.value.isSuccess()).isTrue() + val currentState = currentStateResultCaptor.value.getOrThrow() + assertThat(currentState.state.interaction.hintList[0].hintIsRevealed).isTrue() + assertThat(currentState.state.interaction.solution.solutionIsRevealed).isFalse() + assertThat(currentState.helpIndex.indexTypeCase) + .isEqualTo(HelpIndex.IndexTypeCase.SHOW_SOLUTION) + } + + + @Test + fun testHintsAndSolution_revealedSolutionIsVisible_checkHelpIndexIsCorrect() { + subscribeToCurrentStateToAllowExplorationToLoad() + playExploration( + profileId.internalId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2, + shouldSavePartialProgress = true + ) + playThroughPrototypeState1AndMoveToNextState() + submitWrongAnswerForPrototypeState2() + submitWrongAnswerForPrototypeState2() + + val hintResult = explorationProgressController.submitHintIsRevealed( + hintIsRevealed = true, + hintIndex = 0, + ) + hintResult.observeForever(mockAsyncHintObserver) + testCoroutineDispatchers.runCurrent() + + // The solution should be visible after 30 seconds of the last hint being reveled. + testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) + testCoroutineDispatchers.runCurrent() + + val solutionResult = explorationProgressController.submitSolutionIsRevealed() + solutionResult.observeForever(mockAsyncSolutionObserver) + testCoroutineDispatchers.runCurrent() + + // Verify that the helpIndex.IndexTypeCase is equal EVERYTHING_IS_REVEALED because a new the + // solution has been revealed. + verify(mockCurrentStateLiveDataObserver, atLeastOnce()) + .onChanged(currentStateResultCaptor.capture()) + assertThat(currentStateResultCaptor.value.isSuccess()).isTrue() + val currentState = currentStateResultCaptor.value.getOrThrow() + assertThat(currentState.state.interaction.hintList[0].hintIsRevealed).isTrue() + assertThat(currentState.state.interaction.solution.solutionIsRevealed).isTrue() + assertThat(currentState.helpIndex.indexTypeCase) + .isEqualTo(HelpIndex.IndexTypeCase.EVERYTHING_REVEALED) + } + @Test fun testSubmitAnswer_forTextInput_wrongAnswer_afterAllHintsAreExhausted_showSolution() { subscribeToCurrentStateToAllowExplorationToLoad() diff --git a/model/src/main/proto/exploration.proto b/model/src/main/proto/exploration.proto index 77929e62aa2..930d294b2b2 100644 --- a/model/src/main/proto/exploration.proto +++ b/model/src/main/proto/exploration.proto @@ -333,27 +333,31 @@ message AnswerOutcome { // to properly account for variable numbers of hints, for cases when only a solution or no solution // exists, and for when there are no hints or solutions. message HelpIndex { + // Deprecating HINT_INDEX because it is now split in two, latest_revealed_hint_index and + // available_next_hint_index. + reserved 1; + // This type is uninitialized in cases when no index is currently available (either because a hint // or solution hasn't yet been triggered, or because there are none to trigger). oneof index_type { - // Indicates this help index corresponds to the index of the next available hint within the hint - // list of a state. - int32 available_next_hint_index = 1; - - // Indicates this help index corresponds to the index of the last revealed hint within the hint - // list of a state. - int32 latest_revealed_hint_index = 2; - // Indicates this help index corresponds to the solution of a state. The boolean value here has // no importance and is always 'true'. - bool show_solution = 3; + bool show_solution = 2; // Indicates that everything available has been revealed. Note that this is different than the // case when there are no hints or solutions to trigger, even though the resulting behavior may // be different. This case specifically indicates that all hints and the solution, if present, // have been revealed and there's no other help to provide. The boolean value here has no // importance and is always 'true'. - bool everything_revealed = 4; + bool everything_revealed = 3; + + // Indicates this help index corresponds to the index of the next available hint within the hint + // list of a state. + int32 available_next_hint_index = 4; + + // Indicates this help index corresponds to the index of the last revealed hint within the hint + // list of a state. + int32 latest_revealed_hint_index = 5; } } @@ -367,7 +371,7 @@ message HintState { int32 hint_sequence_number = 2; // Delay for scheduling a new task to show more help to the learner. - int64 delay_to_show_next_hint_and_solution = 4; + int64 delay_to_show_next_hint_and_solution = 3; } // Different states in which exploration checkpoint can exist. diff --git a/model/src/main/proto/exploration_checkpoint.proto b/model/src/main/proto/exploration_checkpoint.proto index 651ffb48994..44cc0a09e03 100644 --- a/model/src/main/proto/exploration_checkpoint.proto +++ b/model/src/main/proto/exploration_checkpoint.proto @@ -47,6 +47,7 @@ message ExplorationCheckpoint { // The timestamp in milliseconds of when the checkpoint was saved for the first time. int64 timestamp_of_first_checkpoint = 9; + // The saved help index for the exploration. HelpIndex help_index = 10; }