diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt index 7a6d9942f7e..6c0917f22b4 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/state/StateFragmentTest.kt @@ -2603,6 +2603,7 @@ class StateFragmentTest { targetTextViewId: Int ) { scrollToViewType(SELECTION_INTERACTION) + // First, check that the option matches what's expected by the test. onView( atPositionOnView( recyclerViewId = R.id.selection_interaction_recyclerview, diff --git a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt index f7e45747c30..dc0ff28e020 100644 --- a/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/state/StateFragmentLocalTest.kt @@ -17,6 +17,7 @@ import androidx.test.espresso.PerformException import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAssertion import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions.scrollToHolder import androidx.test.espresso.contrib.RecyclerViewActions.scrollToPosition @@ -33,6 +34,8 @@ import com.bumptech.glide.GlideBuilder import com.bumptech.glide.load.engine.executor.MockGlideExecutor import com.google.common.truth.Truth.assertThat import dagger.Component +import dagger.Module +import dagger.Provides import kotlinx.coroutines.CoroutineDispatcher import org.hamcrest.BaseMatcher import org.hamcrest.CoreMatchers.allOf @@ -69,11 +72,13 @@ import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewT import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.CONTINUE_NAVIGATION_BUTTON import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.FRACTION_INPUT_INTERACTION import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.NEXT_NAVIGATION_BUTTON +import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.NUMERIC_INPUT_INTERACTION import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.PREVIOUS_RESPONSES_HEADER import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.SELECTION_INTERACTION import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel.ViewType.SUBMIT_ANSWER_BUTTON import org.oppia.android.app.player.state.testing.StateFragmentTestActivity import org.oppia.android.app.recyclerview.RecyclerViewMatcher +import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView import org.oppia.android.app.shim.ViewBindingShimModule import org.oppia.android.app.topic.PracticeTabModule import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule @@ -113,6 +118,7 @@ import org.oppia.android.testing.OppiaTestRule import org.oppia.android.testing.RunOn import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.data.DataProviderTestMonitor +import org.oppia.android.testing.environment.TestEnvironmentConfig import org.oppia.android.testing.espresso.EditTextInputAction import org.oppia.android.testing.espresso.KonfettiViewMatcher.Companion.hasActiveConfetti import org.oppia.android.testing.espresso.KonfettiViewMatcher.Companion.hasExpectedNumberOfActiveSystems @@ -126,7 +132,10 @@ import org.oppia.android.testing.threading.TestDispatcherModule import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule -import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.caching.CacheAssetsLocally +import org.oppia.android.util.caching.LoadImagesFromAssets +import org.oppia.android.util.caching.LoadLessonProtosFromAssets +import org.oppia.android.util.caching.TopicListToCache import org.oppia.android.util.gcsresource.GcsResourceModule import org.oppia.android.util.locale.LocaleProdModule import org.oppia.android.util.logging.LoggerModule @@ -251,7 +260,7 @@ class StateFragmentLocalTest { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() onView(withId(R.id.hint_bulb)).check(matches(not(isDisplayed()))) } @@ -261,7 +270,7 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_wait10seconds_noHintAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) @@ -273,7 +282,7 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_wait30seconds_noHintAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) @@ -285,7 +294,7 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_wait60seconds_hintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(60)) @@ -297,7 +306,7 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_wait60seconds_canViewOneHint() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(60)) openHintsAndSolutionsDialog() @@ -310,7 +319,7 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_wait120seconds_canViewOneHint() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(120)) openHintsAndSolutionsDialog() @@ -324,7 +333,7 @@ class StateFragmentLocalTest { fun testStateFragment_portrait_submitCorrectAnswer_correctTextBannerIsDisplayed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() onView(withId(R.id.congratulations_text_view)) .check(matches(isCompletelyDisplayed())) @@ -336,7 +345,7 @@ class StateFragmentLocalTest { fun testStateFragment_landscape_submitCorrectAnswer_correctTextBannerIsDisplayed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() onView(withId(R.id.congratulations_text_view)) .check(matches(isCompletelyDisplayed())) @@ -348,7 +357,7 @@ class StateFragmentLocalTest { fun testStateFragment_portrait_submitCorrectAnswer_confettiIsActive() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() onView(withId(R.id.congratulations_text_confetti_view)).check(matches(hasActiveConfetti())) } @@ -359,7 +368,7 @@ class StateFragmentLocalTest { fun testStateFragment_landscape_submitCorrectAnswer_confettiIsActive() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() onView(withId(R.id.congratulations_text_confetti_view)).check(matches(hasActiveConfetti())) } @@ -369,10 +378,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_wait60seconds_submitTwoWrongAnswers_canViewOneHint() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(60)) - submitTwoWrongAnswers() + submitTwoWrongAnswersForFractionsState2() openHintsAndSolutionsDialog() onView(withText("Hint 1")).inRoot(isDialog()).check(matches(isDisplayed())) @@ -383,9 +392,9 @@ class StateFragmentLocalTest { fun testStateFragment_submitTwoWrongAnswers_checkPreviousHeaderVisible() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitTwoWrongAnswers() + submitTwoWrongAnswersForFractionsState2() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) testCoroutineDispatchers.runCurrent() @@ -397,9 +406,9 @@ class StateFragmentLocalTest { fun testStateFragment_submitTwoWrongAnswers_checkPreviousHeaderCollapsed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitTwoWrongAnswers() + submitTwoWrongAnswersForFractionsState2() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) testCoroutineDispatchers.runCurrent() @@ -417,9 +426,9 @@ class StateFragmentLocalTest { fun testStateFragment_submitTwoWrongAnswers_expandResponse_checkPreviousHeaderExpanded() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitTwoWrongAnswers() + submitTwoWrongAnswersForFractionsState2() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) testCoroutineDispatchers.runCurrent() @@ -438,9 +447,9 @@ class StateFragmentLocalTest { fun testStateFragment_expandCollapseResponse_checkPreviousHeaderCollapsed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitTwoWrongAnswers() + submitTwoWrongAnswersForFractionsState2() onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(PREVIOUS_RESPONSES_HEADER)) testCoroutineDispatchers.runCurrent() @@ -475,9 +484,9 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_submitInitialWrongAnswer_noHintAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() // Submitting one wrong answer isn't sufficient to show a hint. onView(withId(R.id.hint_bulb)).check(matches(not(isDisplayed()))) @@ -488,9 +497,9 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_submitInitialWrongAnswer_wait10seconds_noHintAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) // Submitting one wrong answer isn't sufficient to show a hint. @@ -502,9 +511,9 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_submitInitialWrongAnswer_wait30seconds_noHintAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) // Submitting one wrong answer isn't sufficient to show a hint. @@ -516,9 +525,9 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_submitTwoWrongAnswers_hintAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitTwoWrongAnswers() + submitTwoWrongAnswersForFractionsState2() // Submitting two wrong answers should make the hint immediately available. onView(withId(R.id.hint_bulb)).check(matches(isDisplayed())) @@ -529,8 +538,8 @@ class StateFragmentLocalTest { fun testStateFragment_submitTwoWrongAnswers_hintAvailable_prevState_hintNotAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - submitTwoWrongAnswers() + playThroughFractionsState1() + submitTwoWrongAnswersForFractionsState2() onView(withId(R.id.hint_bulb)).check(matches(isDisplayed())) // The previous navigation button is next to a submit answer button in this state. onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(SUBMIT_ANSWER_BUTTON)) @@ -545,8 +554,8 @@ class StateFragmentLocalTest { fun testStateFragment_submitTwoWrongAnswers_prevState_currentState_checkDotIconVisible() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - submitTwoWrongAnswers() + playThroughFractionsState1() + submitTwoWrongAnswersForFractionsState2() onView(withId(R.id.dot_hint)).check(matches(isDisplayed())) moveToPreviousAndBackToCurrentStateWithSubmitButton() onView(withId(R.id.dot_hint)).check(matches(isDisplayed())) @@ -557,8 +566,8 @@ class StateFragmentLocalTest { fun testStateFragment_oneUnrevealedHint_prevState_currentState_checkOneUnrevealedHintVisible() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - submitTwoWrongAnswers() + playThroughFractionsState1() + submitTwoWrongAnswersForFractionsState2() openHintsAndSolutionsDialog() onView(withText("Hint 1")).inRoot(isDialog()).check(matches(isDisplayed())) @@ -577,9 +586,9 @@ class StateFragmentLocalTest { fun testStateFragment_revealFirstHint_prevState_currentState_checkFirstHintRevealed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - produceAndViewFirstHint() + produceAndViewFirstHintForFractionState2() moveToPreviousAndBackToCurrentStateWithSubmitButton() openHintsAndSolutionsDialog() onView(withId(R.id.hints_and_solution_recycler_view)) @@ -607,9 +616,9 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_submitTwoWrongAnswersAndWait_canViewOneHint() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitTwoWrongAnswers() + submitTwoWrongAnswersForFractionsState2() openHintsAndSolutionsDialog() onView(withText("Hint 1")).inRoot(isDialog()).check(matches(isDisplayed())) @@ -620,9 +629,9 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_submitThreeWrongAnswers_canViewOneHint() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() - submitThreeWrongAnswersAndWait() + submitThreeWrongAnswersForFractionsState2AndWait() openHintsAndSolutionsDialog() onView(withText("Hint 1")).inRoot(isDialog()).check(matches(isDisplayed())) @@ -634,8 +643,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_newHintIsNoLongerAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - submitTwoWrongAnswersAndWait() + playThroughFractionsState1() + submitTwoWrongAnswersForFractionsState2AndWait() openHintsAndSolutionsDialog() pressRevealHintButton(hintPosition = 0) @@ -650,8 +659,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_wait10seconds_noNewHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) @@ -663,8 +672,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_wait30seconds_newHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) @@ -677,8 +686,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_doNotWait_canViewTwoHints() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() openHintsAndSolutionsDialog() onView(withText("Hint 1")).inRoot(isDialog()).check(matches(isDisplayed())) @@ -692,8 +701,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_wait30seconds_canViewTwoHints() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) openHintsAndSolutionsDialog() @@ -708,8 +717,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_wait60seconds_canViewTwoHints() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(60)) openHintsAndSolutionsDialog() @@ -724,11 +733,11 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_wait60seconds_submitWrongAnswer_canViewTwoHints() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(60)) - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() openHintsAndSolutionsDialog() // After 60 seconds and one wrong answer submission, only two hints should be available. @@ -741,10 +750,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_submitWrongAnswer_noNewHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() // Submitting a single wrong answer after the previous hint won't immediately show another. onView(withId(R.id.dot_hint)).check(matches(not(isDisplayed()))) @@ -755,10 +764,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_submitWrongAnswer_wait10seconds_newHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) // Waiting 10 seconds after submitting a wrong answer should allow another hint to be shown. @@ -770,10 +779,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_submitWrongAnswer_wait10seconds_canViewTwoHints() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) openHintsAndSolutionsDialog() @@ -786,10 +795,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewHint_submitWrongAnswer_wait30seconds_canViewTwoHints() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) openHintsAndSolutionsDialog() @@ -803,8 +812,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewFirstHint_configChange_secondHintIsNotAvailableImmediately() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() onView(isRoot()).perform(orientationLandscape()) testCoroutineDispatchers.runCurrent() @@ -816,8 +825,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewFirstHint_configChange_wait30Seconds_secondHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() onView(isRoot()).perform(orientationLandscape()) testCoroutineDispatchers.runCurrent() @@ -832,7 +841,7 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_newHintAvailable_configChange_newHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(60)) onView(withId(R.id.dot_hint)).check(matches(isDisplayed())) onView(isRoot()).perform(orientationLandscape()) @@ -845,8 +854,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewFirstHint_prevState_wait30seconds_newHintIsNotAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFirstHint() + playThroughFractionsState1() + produceAndViewFirstHintForFractionState2() clickPreviousStateNavigationButton() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) onView(withId(R.id.dot_hint)).check(matches(not(isDisplayed()))) @@ -857,8 +866,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewFourHints_wait10seconds_noNewHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) @@ -870,8 +879,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewFourHints_wait30seconds_newHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) @@ -884,8 +893,8 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewFourHints_wait30seconds_canViewSolution() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) openHintsAndSolutionsDialog() @@ -907,10 +916,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewFourHints_submitWrongAnswer_noNewHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() // Submitting a wrong answer will not immediately reveal the solution. onView(withId(R.id.dot_hint)).check(matches(not(isDisplayed()))) @@ -921,10 +930,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewFourHints_submitWrongAnswer_wait10s_newHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) // Submitting a wrong answer and waiting will reveal the solution. @@ -936,10 +945,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewFourHints_submitWrongAnswer_wait10s_canViewSolution() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) openHintsAndSolutionsDialog() @@ -960,10 +969,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewSolution_clickRevealSolutionButton_showsDialog() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) openHintsAndSolutionsDialog() @@ -989,10 +998,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewRevealSolutionDialog_clickReveal_solutionIsRevealed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { scenario -> startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) openHintsAndSolutionsDialog() @@ -1009,10 +1018,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewRevealSolutionDialog_clickReveal_cannotViewRevealSolution() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { scenario -> startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) openHintsAndSolutionsDialog() @@ -1029,10 +1038,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewRevealSolutionDialog_clickCancel_solutionIsNotRevealed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { scenario -> startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) openHintsAndSolutionsDialog() @@ -1049,10 +1058,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewRevealSolutionDialog_clickCancel_canViewRevealSolution() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { scenario -> startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) openHintsAndSolutionsDialog() @@ -1069,10 +1078,10 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewSolution_noNewHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { scenario -> startPlayingExploration() - playThroughState1() - produceAndViewFourHints() + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() - produceAndViewSolution(scenario, revealedHintCount = 4) + produceAndViewSolutionInFractionsState2(scenario, revealedHintCount = 4) // No hint should be indicated as available after revealing the solution. onView(withId(R.id.dot_hint)).check(matches(not(isDisplayed()))) @@ -1083,9 +1092,9 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewSolution_wait30seconds_noNewHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { scenario -> startPlayingExploration() - playThroughState1() - produceAndViewFourHints() - produceAndViewSolution(scenario, revealedHintCount = 4) + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() + produceAndViewSolutionInFractionsState2(scenario, revealedHintCount = 4) testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(30)) @@ -1098,11 +1107,11 @@ class StateFragmentLocalTest { fun testStateFragment_nextState_viewSolution_submitWrongAnswer_wait10s_noNewHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { scenario -> startPlayingExploration() - playThroughState1() - produceAndViewFourHints() - produceAndViewSolution(scenario, revealedHintCount = 4) + playThroughFractionsState1() + produceAndViewFourHintsInFractionState2() + produceAndViewSolutionInFractionsState2(scenario, revealedHintCount = 4) - submitWrongAnswerToState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) // Submitting a wrong answer should not change anything since the solution's been revealed. @@ -1126,10 +1135,10 @@ class StateFragmentLocalTest { fun testStateFragment_stateWithoutSolution_viewAllHints_wrongAnswerAndWait_noHintIsAvailable() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playUpToFinalTestSecondTry() - produceAndViewThreeHintsInState13() + playUpToFractionsFinalTestSecondTry() + produceAndViewThreeHintsInFractionsState13() - submitWrongAnswerToState13() + submitWrongAnswerToFractionsState13() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) // No hint indicator should be shown since there is no solution for this state. @@ -1137,6 +1146,57 @@ class StateFragmentLocalTest { } } + // TODO(#1050): Add a test for verifying that the solution is correct for non-text & non-fraction + // interactions. + + @Test + fun testStateFragment_stateWithNumericSolution_revealHint_reopenDialog_onlyOneHintShown() { + launchForExploration(TEST_EXPLORATION_ID_2).use { + startPlayingExploration() + playThroughTestState1() + playThroughTestState2() + playThroughTestState3() + playThroughTestState4() + playThroughTestState5() + // Trigger the first hint to show (via two incorrect answers), then reveal it. + produceAndViewNextHint(hintPosition = 0) { + submitNumericInput(text = "1") + submitNumericInput(text = "1") + } + + // Reopen the dialog after showing the hint. + openHintsAndSolutionsDialog() + + // Verify that the first hint is available, but not the solution. + onView(withText("Hint 1")).inRoot(isDialog()).check(matches(isDisplayed())) + onView(withText("Solution")).inRoot(isDialog()).check(doesNotExist()) + } + } + + @Test + fun testStateFragment_stateWithNumericSolution_revealHint_triggerSolution_hintBulbShown() { + launchForExploration(TEST_EXPLORATION_ID_2).use { + startPlayingExploration() + playThroughTestState1() + playThroughTestState2() + playThroughTestState3() + playThroughTestState4() + playThroughTestState5() + // Trigger the first hint to show (via two incorrect answers), then reveal it. + produceAndViewNextHint(hintPosition = 0) { + submitNumericInput(text = "1") + submitNumericInput(text = "1") + } + + // Trigger the solution to show by submitting another incorrect answer & waiting. + submitNumericInput(text = "1") + testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) + + // The new hint indicator should be shown since a solution is now available. + onView(withId(R.id.dot_hint)).check(matches(isDisplayed())) + } + } + @Test @DefineAppLanguageLocaleContext( oppiaLanguageEnumId = ENGLISH_VALUE, @@ -1304,7 +1364,7 @@ class StateFragmentLocalTest { fun testStateFragment_mobilePortrait_finishExploration_endOfSessionConfettiIsDisplayed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughAllStates() + playThroughAllFractionsStates() clickContinueButton() onView(withId(R.id.full_screen_confetti_view)).check(matches(hasActiveConfetti())) @@ -1316,7 +1376,7 @@ class StateFragmentLocalTest { fun testStateFragment_mobileLandscape_finishExploration_endOfSessionConfettiIsDisplayed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughAllStates() + playThroughAllFractionsStates() clickContinueButton() onView(withId(R.id.full_screen_confetti_view)).check(matches(hasActiveConfetti())) @@ -1330,7 +1390,7 @@ class StateFragmentLocalTest { fun testStateFragment_tabletPortrait_finishExploration_endOfSessionConfettiIsDisplayed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughAllStates() + playThroughAllFractionsStates() clickContinueButton() onView(withId(R.id.full_screen_confetti_view)).check(matches(hasActiveConfetti())) @@ -1344,7 +1404,7 @@ class StateFragmentLocalTest { fun testStateFragment_tabletLandscape_finishExploration_endOfSessionConfettiIsDisplayed() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughAllStates() + playThroughAllFractionsStates() clickContinueButton() onView(withId(R.id.full_screen_confetti_view)).check(matches(hasActiveConfetti())) @@ -1356,7 +1416,7 @@ class StateFragmentLocalTest { fun testStateFragment_finishExploration_changePortToLand_endOfSessionConfettiIsDisplayedAgain() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughAllStates() + playThroughAllFractionsStates() clickContinueButton() onView(withId(R.id.full_screen_confetti_view)).check( matches( @@ -1379,7 +1439,7 @@ class StateFragmentLocalTest { fun testStateFragment_finishExploration_changeLandToPort_endOfSessionConfettiIsDisplayedAgain() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughAllStates() + playThroughAllFractionsStates() clickContinueButton() onView(withId(R.id.full_screen_confetti_view)).check( matches( @@ -1401,7 +1461,7 @@ class StateFragmentLocalTest { fun testStateFragment_submitCorrectAnswer_endOfSessionConfettiDoesNotStart() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughState1() + playThroughFractionsState1() onView(withId(R.id.full_screen_confetti_view)).check(matches(not(hasActiveConfetti()))) } @@ -1412,7 +1472,7 @@ class StateFragmentLocalTest { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() // Play through all questions but do not reach the last screen of the exploration. - playThroughAllStates() + playThroughAllFractionsStates() onView(withId(R.id.full_screen_confetti_view)).check(matches(not(hasActiveConfetti()))) } @@ -1422,7 +1482,7 @@ class StateFragmentLocalTest { fun testStateFragment_reachEndOfExplorationTwice_endOfSessionConfettiIsDisplayedOnce() { launchForExploration(FRACTIONS_EXPLORATION_ID_1).use { startPlayingExploration() - playThroughAllStates() + playThroughAllFractionsStates() clickContinueButton() onView(withId(R.id.full_screen_confetti_view)).check(matches(hasActiveConfetti())) onView(withId(R.id.full_screen_confetti_view)).check( @@ -1477,113 +1537,140 @@ class StateFragmentLocalTest { testCoroutineDispatchers.runCurrent() } - private fun playThroughState1() { + private fun playThroughFractionsState1() { onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(SELECTION_INTERACTION)) onView(withSubstring("the pieces must be the same size.")).perform(click()) testCoroutineDispatchers.runCurrent() clickContinueNavigationButton() } - private fun playThroughState2() { + private fun playThroughFractionsState2() { // Correct answer to 'Matthew gets conned' submitFractionAnswer(answerText = "3/4") clickContinueNavigationButton() } - private fun playThroughState3() { + private fun playThroughFractionsState3() { // Correct answer to 'Question 1' submitFractionAnswer(answerText = "4/9") clickContinueNavigationButton() } - private fun playThroughState4() { + private fun playThroughFractionsState4() { // Correct answer to 'Question 2' submitFractionAnswer(answerText = "1/4") clickContinueNavigationButton() } - private fun playThroughState5() { + private fun playThroughFractionsState5() { // Correct answer to 'Question 3' submitFractionAnswer(answerText = "1/8") clickContinueNavigationButton() } - private fun playThroughState6() { + private fun playThroughFractionsState6() { // Correct answer to 'Question 4' submitFractionAnswer(answerText = "1/2") clickContinueNavigationButton() } - private fun playThroughState7() { + private fun playThroughFractionsState7() { // Correct answer to 'Question 5' which redirects the learner to 'Thinking in fractions Q1' submitFractionAnswer(answerText = "2/9") clickContinueNavigationButton() } - private fun playThroughState8() { + private fun playThroughFractionsState8() { // Correct answer to 'Thinking in fractions Q1' submitFractionAnswer(answerText = "7/9") clickContinueNavigationButton() } - private fun playThroughState9() { + private fun playThroughFractionsState9() { // Correct answer to 'Thinking in fractions Q2' submitFractionAnswer(answerText = "4/9") clickContinueNavigationButton() } - private fun playThroughState10() { + private fun playThroughFractionsState10() { // Correct answer to 'Thinking in fractions Q3' submitFractionAnswer(answerText = "5/8") clickContinueNavigationButton() } - private fun playThroughState11() { + private fun playThroughFractionsState11() { // Correct answer to 'Thinking in fractions Q4' which redirects the learner to 'Final Test A' submitFractionAnswer(answerText = "3/4") clickContinueNavigationButton() } - private fun playThroughState12() { + private fun playThroughFractionsState12() { // Correct answer to 'Final Test A' redirects learner to 'Happy ending' submitFractionAnswer(answerText = "2/4") clickContinueNavigationButton() } - private fun playThroughState12WithWrongAnswer() { + private fun playThroughFractionsState12WithWrongAnswer() { // Incorrect answer to 'Final Test A' redirects the learner to 'Final Test A second try' submitFractionAnswer(answerText = "1/9") clickContinueNavigationButton() } - private fun playUpToFinalTestSecondTry() { - playThroughState1() - playThroughState2() - playThroughState3() - playThroughState4() - playThroughState5() - playThroughState6() - playThroughState7() - playThroughState8() - playThroughState9() - playThroughState10() - playThroughState11() - playThroughState12WithWrongAnswer() - } - - private fun playThroughAllStates() { - playThroughState1() - playThroughState2() - playThroughState3() - playThroughState4() - playThroughState5() - playThroughState6() - playThroughState7() - playThroughState8() - playThroughState9() - playThroughState10() - playThroughState11() - playThroughState12() + private fun playUpToFractionsFinalTestSecondTry() { + playThroughFractionsState1() + playThroughFractionsState2() + playThroughFractionsState3() + playThroughFractionsState4() + playThroughFractionsState5() + playThroughFractionsState6() + playThroughFractionsState7() + playThroughFractionsState8() + playThroughFractionsState9() + playThroughFractionsState10() + playThroughFractionsState11() + playThroughFractionsState12WithWrongAnswer() + } + + private fun playThroughAllFractionsStates() { + playThroughFractionsState1() + playThroughFractionsState2() + playThroughFractionsState3() + playThroughFractionsState4() + playThroughFractionsState5() + playThroughFractionsState6() + playThroughFractionsState7() + playThroughFractionsState8() + playThroughFractionsState9() + playThroughFractionsState10() + playThroughFractionsState11() + playThroughFractionsState12() + } + + private fun playThroughTestState1() { + clickContinueButton() + } + + private fun playThroughTestState2() { + submitFractionAnswer(answerText = "1/2") + clickContinueNavigationButton() + } + + private fun playThroughTestState3() { + selectMultipleChoiceOption(optionPosition = 2, expectedOptionText = "Eagle") + clickContinueNavigationButton() + } + + private fun playThroughTestState4() { + selectMultipleChoiceOption(optionPosition = 0, expectedOptionText = "Green") + clickContinueNavigationButton() + } + + private fun playThroughTestState5() { + selectItemSelectionCheckbox(optionPosition = 0, expectedOptionText = "Red") + selectItemSelectionCheckbox(optionPosition = 2, expectedOptionText = "Green") + selectItemSelectionCheckbox(optionPosition = 3, expectedOptionText = "Blue") + clickSubmitAnswerButton() + clickContinueNavigationButton() } private fun clickContinueNavigationButton() { @@ -1600,6 +1687,12 @@ class StateFragmentLocalTest { testCoroutineDispatchers.runCurrent() } + private fun clickSubmitAnswerButton() { + onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(SUBMIT_ANSWER_BUTTON)) + onView(withId(R.id.submit_answer_button)).perform(click()) + testCoroutineDispatchers.runCurrent() + } + private fun clickNextStateNavigationButton() { onView(withId(R.id.next_state_navigation_button)).perform(click()) testCoroutineDispatchers.runCurrent() @@ -1654,58 +1747,117 @@ class StateFragmentLocalTest { testCoroutineDispatchers.runCurrent() } - private fun submitFractionAnswer(answerText: String) { + private fun typeFractionAnswer(answerText: String) { onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(FRACTION_INPUT_INTERACTION)) - onView(withId(R.id.fraction_input_interaction_view)).perform( - editTextInputAction.appendText(answerText) + typeTextIntoInteraction(answerText, interactionViewId = R.id.fraction_input_interaction_view) + } + + private fun submitFractionAnswer(answerText: String) { + typeFractionAnswer(answerText) + clickSubmitAnswerButton() + } + + private fun selectMultipleChoiceOption(optionPosition: Int, expectedOptionText: String) { + clickSelection( + optionPosition, + targetClickViewId = R.id.multiple_choice_radio_button, + expectedText = expectedOptionText, + targetTextViewId = R.id.multiple_choice_content_text_view + ) + } + + private fun selectItemSelectionCheckbox(optionPosition: Int, expectedOptionText: String) { + clickSelection( + optionPosition, + targetClickViewId = R.id.item_selection_checkbox, + expectedText = expectedOptionText, + targetTextViewId = R.id.item_selection_contents_text_view ) + } + + private fun typeNumericInput(text: String) { + onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(NUMERIC_INPUT_INTERACTION)) + typeTextIntoInteraction(text, interactionViewId = R.id.numeric_input_interaction_view) + } + + private fun submitNumericInput(text: String) { + typeNumericInput(text) + clickSubmitAnswerButton() + } + + private fun typeTextIntoInteraction(text: String, interactionViewId: Int) { + onView(withId(interactionViewId)).perform(editTextInputAction.appendText(text)) testCoroutineDispatchers.runCurrent() + } - onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(SUBMIT_ANSWER_BUTTON)) - onView(withId(R.id.submit_answer_button)).perform(click()) + private fun clickSelection( + optionPosition: Int, + targetClickViewId: Int, + expectedText: String, + targetTextViewId: Int + ) { + onView(withId(R.id.state_recycler_view)).perform(scrollToViewType(SELECTION_INTERACTION)) + // First, check that the option matches what's expected by the test. + onView( + atPositionOnView( + recyclerViewId = R.id.selection_interaction_recyclerview, + position = optionPosition, + targetViewId = targetTextViewId + ) + ).check(matches(withText(containsString(expectedText)))) + // Then, click on it. + onView( + atPositionOnView( + recyclerViewId = R.id.selection_interaction_recyclerview, + position = optionPosition, + targetViewId = targetClickViewId + ) + ).perform(click()) testCoroutineDispatchers.runCurrent() } - private fun submitWrongAnswerToState2() { + private fun submitWrongAnswerToFractionsState2() { submitFractionAnswer(answerText = "1/2") } - private fun submitWrongAnswerToState2AndWait() { - submitWrongAnswerToState2() + private fun submitWrongAnswerToFractionsState2AndWait() { + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) } - private fun submitWrongAnswerToState13() { + private fun submitWrongAnswerToFractionsState13() { submitFractionAnswer(answerText = "1/9") } - private fun submitWrongAnswerToState13AndWait() { - submitWrongAnswerToState13() + private fun submitWrongAnswerToFractionsState13AndWait() { + submitWrongAnswerToFractionsState13() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) } - private fun submitTwoWrongAnswers() { - submitWrongAnswerToState2() - submitWrongAnswerToState2() + private fun submitTwoWrongAnswersForFractionsState2() { + submitWrongAnswerToFractionsState2() + submitWrongAnswerToFractionsState2() } - private fun submitTwoWrongAnswersAndWait() { - submitTwoWrongAnswers() + private fun submitTwoWrongAnswersForFractionsState2AndWait() { + submitTwoWrongAnswersForFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) } - private fun submitThreeWrongAnswersAndWait() { - submitWrongAnswerToState2() - submitWrongAnswerToState2() - submitWrongAnswerToState2() + private fun submitThreeWrongAnswersForFractionsState2AndWait() { + submitWrongAnswerToFractionsState2() + submitWrongAnswerToFractionsState2() + submitWrongAnswerToFractionsState2() testCoroutineDispatchers.advanceTimeBy(TimeUnit.SECONDS.toMillis(10)) } - private fun produceAndViewFirstHint() { + private fun produceAndViewFirstHintForFractionState2() { // Two wrong answers need to be submitted for the first hint to show up, so submit an extra one // in advance of the standard show & reveal hint flow. - submitWrongAnswerToState2() - produceAndViewNextHint(hintPosition = 0, submitAnswer = this::submitWrongAnswerToState2AndWait) + submitWrongAnswerToFractionsState2() + produceAndViewNextHint( + hintPosition = 0, submitAnswer = this::submitWrongAnswerToFractionsState2AndWait + ) } /** @@ -1719,27 +1871,39 @@ class StateFragmentLocalTest { closeHintsAndSolutionsDialog() } - private fun produceAndViewThreeHintsInState13() { - submitWrongAnswerToState13() - produceAndViewNextHint(hintPosition = 0, submitAnswer = this::submitWrongAnswerToState13AndWait) - produceAndViewNextHint(hintPosition = 1, submitAnswer = this::submitWrongAnswerToState13AndWait) - produceAndViewNextHint(hintPosition = 2, submitAnswer = this::submitWrongAnswerToState13AndWait) + private fun produceAndViewThreeHintsInFractionsState13() { + submitWrongAnswerToFractionsState13() + produceAndViewNextHint( + hintPosition = 0, submitAnswer = this::submitWrongAnswerToFractionsState13AndWait + ) + produceAndViewNextHint( + hintPosition = 1, submitAnswer = this::submitWrongAnswerToFractionsState13AndWait + ) + produceAndViewNextHint( + hintPosition = 2, submitAnswer = this::submitWrongAnswerToFractionsState13AndWait + ) } - private fun produceAndViewFourHints() { + private fun produceAndViewFourHintsInFractionState2() { // Cause three hints to show, and reveal each of them one at a time (to allow the later hints // to be shown). - produceAndViewFirstHint() - produceAndViewNextHint(hintPosition = 1, submitAnswer = this::submitWrongAnswerToState2AndWait) - produceAndViewNextHint(hintPosition = 2, submitAnswer = this::submitWrongAnswerToState2AndWait) - produceAndViewNextHint(hintPosition = 3, submitAnswer = this::submitWrongAnswerToState2AndWait) + produceAndViewFirstHintForFractionState2() + produceAndViewNextHint( + hintPosition = 1, submitAnswer = this::submitWrongAnswerToFractionsState2AndWait + ) + produceAndViewNextHint( + hintPosition = 2, submitAnswer = this::submitWrongAnswerToFractionsState2AndWait + ) + produceAndViewNextHint( + hintPosition = 3, submitAnswer = this::submitWrongAnswerToFractionsState2AndWait + ) } - private fun produceAndViewSolution( + private fun produceAndViewSolutionInFractionsState2( activityScenario: ActivityScenario, revealedHintCount: Int ) { - submitWrongAnswerToState2AndWait() + submitWrongAnswerToFractionsState2AndWait() openHintsAndSolutionsDialog() pressRevealSolutionButton(revealedHintCount) clickConfirmRevealSolutionButton(activityScenario) @@ -1855,19 +2019,40 @@ class StateFragmentLocalTest { }) } + // TODO(#89): Move this to a common test application component. + @Module + class TestModule { + @Provides + @LoadLessonProtosFromAssets + fun provideLoadLessonProtosFromAssets(testEnvironmentConfig: TestEnvironmentConfig): Boolean = + testEnvironmentConfig.isUsingBazel() + + @Provides + @CacheAssetsLocally + fun provideCacheAssetsLocally(): Boolean = false + + @Provides + @TopicListToCache + fun provideTopicListToCache(): List = listOf() + + @Provides + @LoadImagesFromAssets + fun provideLoadImagesFromAssets(): Boolean = false + } + // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. @Singleton @Component( modules = [ - TestDispatcherModule::class, ApplicationModule::class, RobolectricModule::class, - PlatformParameterModule::class, PlatformParameterSingletonModule::class, - LoggerModule::class, ContinueModule::class, FractionInputModule::class, - ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + TestModule::class, TestDispatcherModule::class, ApplicationModule::class, + RobolectricModule::class, PlatformParameterModule::class, + PlatformParameterSingletonModule::class, LoggerModule::class, ContinueModule::class, + FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, - AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, + AccessibilityTestModule::class, LogStorageModule::class, PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class, ApplicationStartupListenerModule::class, LogUploadWorkerModule::class, diff --git a/domain/BUILD.bazel b/domain/BUILD.bazel index bb80014fd09..e037874fbac 100755 --- a/domain/BUILD.bazel +++ b/domain/BUILD.bazel @@ -50,6 +50,7 @@ DOMAIN_ASSETS = generate_assets_list_from_text_protos( "test_single_interactive_state_exp_with_one_hint_and_solution", "test_single_interactive_state_exp_with_one_hint_and_no_solution", "test_single_interactive_state_exp_with_only_solution", + "test_single_interactive_state_exp_with_solution_missing_answer", ], skills_file_names = [ "skills", diff --git a/domain/src/main/assets/test_exp_id_2.json b/domain/src/main/assets/test_exp_id_2.json index dd5637e5cfb..e108d537190 100644 --- a/domain/src/main/assets/test_exp_id_2.json +++ b/domain/src/main/assets/test_exp_id_2.json @@ -692,8 +692,20 @@ "refresher_exploration_id": "", "missing_prerequisite_skill_id": "" }, - "hints": [], - "solution": null + "hints": [{ + "hint_content": { + "content_id": "hint_1", + "html": "

11 * 11 can be rephrased as 11 * 10, then add 11 at the end.

" + } + }], + "solution": { + "answer_is_exclusive": false, + "correct_answer": "", + "explanation": { + "content_id": "solution", + "html": "

11 times 11 is 121.

" + } + } }, "classifier_model_id": "", "recorded_voiceovers": { @@ -702,11 +714,37 @@ "feedback_3": {}, "feedback_1": {}, "content": {}, - "default_outcome": {} + "default_outcome": {}, + "hint_1": {}, + "solution": {} } }, "written_translations": { "translations_mapping": { + "hint_1": { + "pt": { + "data_format": "html", + "translation": {"translation" : "

11 * 11 pode ser reformulado como 11 * 10 e, em seguida, adicione 11 no final.

"}, + "needs_update": false + }, + "ar": { + "data_format": "html", + "translation": {"translation" : "يمكن إعادة صياغة

11 * 11 على أنها 11 * 10، ثم أضف 11 في النهاية.

"}, + "needs_update": false + } + }, + "solution": { + "pt": { + "data_format": "html", + "translation": {"translation" : "

11 vezes 11 é 121.

"}, + "needs_update": false + }, + "ar": { + "data_format": "html", + "translation": {"translation" : "

11 مرات 11 هو 121.

"}, + "needs_update": false + } + }, "feedback_2": { "pt": { "data_format": "html", diff --git a/domain/src/main/assets/test_exp_id_2.textproto b/domain/src/main/assets/test_exp_id_2.textproto index 58cffe4084a..075f3bb457f 100644 --- a/domain/src/main/assets/test_exp_id_2.textproto +++ b/domain/src/main/assets/test_exp_id_2.textproto @@ -825,10 +825,54 @@ states { value { } } + recorded_voiceovers { + key: "hint_1" + value { + } + } + recorded_voiceovers { + key: "solution" + value { + } + } content { html: "

What is 11 times 11?

" content_id: "content" } + written_translations { + key: "hint_1" + value { + translation_mapping { + key: "pt" + value { + html: "

11 * 11 pode ser reformulado como 11 * 10 e, em seguida, adicione 11 no final.

" + } + } + translation_mapping { + key: "ar" + value { + html: "\331\212\331\205\331\203\331\206 \330\245\330\271\330\247\330\257\330\251 \330\265\331\212\330\247\330\272\330\251

11 * 11 \330\271\331\204\331\211 \330\243\331\206\331\207\330\247 11 * 10\330\214 \330\253\331\205 \330\243\330\266\331\201 11 \331\201\331\212 \330\247\331\204\331\206\331\207\330\247\331\212\330\251.

" + } + } + } + } + written_translations { + key: "solution" + value { + translation_mapping { + key: "pt" + value { + html: "

11 vezes 11 \303\251 121.

" + } + } + translation_mapping { + key: "ar" + value { + html: "

11 \331\205\330\261\330\247\330\252 11 \331\207\331\210 121.

" + } + } + } + } written_translations { key: "feedback_2" value { @@ -970,6 +1014,19 @@ states { rule_type: "IsGreaterThan" } } + solution { + interaction_id: "NumericInput" + explanation { + html: "

11 times 11 is 121.

" + content_id: "solution" + } + } + hint { + hint_content { + html: "

11 * 11 can be rephrased as 11 * 10, then add 11 at the end.

" + content_id: "hint_1" + } + } default_outcome { dest_state_name: "NumberInput" feedback { diff --git a/domain/src/main/assets/test_single_interactive_state_exp_with_solution_missing_answer.json b/domain/src/main/assets/test_single_interactive_state_exp_with_solution_missing_answer.json new file mode 100644 index 00000000000..5ddc99450b0 --- /dev/null +++ b/domain/src/main/assets/test_single_interactive_state_exp_with_solution_missing_answer.json @@ -0,0 +1,112 @@ +{ + "exploration_id": "test_single_interactive_state_exp_with_solution_missing_answer", + "version": 1, + "exploration": { + "init_state_name": "Text", + "states": { + "Text": { + "content": { + "content_id": "content", + "html": "

In which language does Oppia mean 'to learn'?

" + }, + "interaction": { + "id": "TextInput", + "customization_args": { + "rows": { + "value": 1.0 + }, + "placeholder": { + "value": { + "content_id": "ca_placeholder_0", + "unicode_str": "Enter a language" + } + } + }, + "answer_groups": [{ + "rule_specs": [{ + "rule_type": "Equals", + "inputs": { + "x": { + "contentId": "", + "normalizedStrSet": ["finnish"] + } + } + }], + "outcome": { + "dest": "End", + "feedback": { + "content_id": "feedback_1", + "html": "

Correct!

" + }, + "labelled_as_correct": false + } + }], + "default_outcome": { + "dest": "Text", + "feedback": { + "content_id": "default_outcome", + "html": "

Not quite. Try again (or maybe use a search engine).

" + }, + "labelled_as_correct": false + }, + "hints": [], + "solution": { + "answer_is_exclusive": false, + "explanation": { + "content_id": "solution", + "html": "

'Oppia' is translated from Finnish.

" + } + } + }, + "recorded_voiceovers": { + "voiceovers_mapping": { + "feedback_1": {}, + "content": {}, + "default_outcome": {}, + "solution": {} + } + }, + "written_translations": { + "translations_mapping": { + "feedback_1": {}, + "content": {}, + "default_outcome": {}, + "solution": {} + } + } + }, + "End": { + "content": { + "content_id": "content", + "html": "Congratulations, you have finished!" + }, + "param_changes": [], + "interaction": { + "id": "EndExploration", + "customization_args": { + "recommendedExplorationIds": { + "value": [] + } + }, + "answer_groups": [], + "default_outcome": null, + "hints": [], + "solution": null + }, + "recorded_voiceovers": { + "voiceovers_mapping": { + "content": {} + } + }, + "written_translations": { + "translations_mapping": { + "content": {} + } + } + } + }, + "objective": "Test exploration.", + "language_code": "en", + "title": "Prototype exploration with only one solution and no hints" + } +} diff --git a/domain/src/main/assets/test_single_interactive_state_exp_with_solution_missing_answer.textproto b/domain/src/main/assets/test_single_interactive_state_exp_with_solution_missing_answer.textproto new file mode 100644 index 00000000000..1c3bb7d9a8d --- /dev/null +++ b/domain/src/main/assets/test_single_interactive_state_exp_with_solution_missing_answer.textproto @@ -0,0 +1,140 @@ +id: "test_single_interactive_state_exp_with_solution_missing_answer" +states { + key: "Text" + value { + name: "Text" + recorded_voiceovers { + key: "feedback_1" + value { + } + } + recorded_voiceovers { + key: "content" + value { + } + } + recorded_voiceovers { + key: "default_outcome" + value { + } + } + recorded_voiceovers { + key: "solution" + value { + } + } + content { + html: "

In which language does Oppia mean \'to learn\'?

" + content_id: "content" + } + written_translations { + key: "feedback_1" + value { + } + } + written_translations { + key: "content" + value { + } + } + written_translations { + key: "default_outcome" + value { + } + } + written_translations { + key: "solution" + value { + } + } + interaction { + id: "TextInput" + answer_groups { + outcome { + dest_state_name: "End" + feedback { + html: "

Correct!

" + content_id: "feedback_1" + } + } + rule_specs { + input { + key: "x" + value { + translatable_set_of_normalized_string { + content_id: "" + normalized_strings: "finnish" + } + } + } + rule_type: "Equals" + } + } + solution { + interaction_id: "TextInput" + explanation { + html: "

'Oppia' is translated from Finnish.

" + content_id: "solution" + } + } + default_outcome { + dest_state_name: "Text" + feedback { + html: "

Not quite. Try again (or maybe use a search engine).

" + content_id: "default_outcome" + } + } + customization_args { + key: "rows" + value { + signed_int: 1 + } + } + customization_args { + key: "placeholder" + value { + custom_schema_value { + subtitled_html { + html: "Enter a language" + content_id: "ca_placeholder_0" + } + } + } + } + } + } +} +states { + key: "End" + value { + name: "End" + recorded_voiceovers { + key: "content" + value { + } + } + content { + html: "Congratulations, you have finished!" + content_id: "content" + } + written_translations { + key: "content" + value { + } + } + interaction { + id: "EndExploration" + customization_args { + key: "recommendedExplorationIds" + value { + schema_object_list { + } + } + } + } + } +} +init_state_name: "Text" +objective: "Test exploration." +title: "Prototype exploration with only one solution and no hints" +language_code: "en" diff --git a/domain/src/main/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImpl.kt b/domain/src/main/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImpl.kt index 74bea300c55..a4096d930a7 100644 --- a/domain/src/main/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImpl.kt +++ b/domain/src/main/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImpl.kt @@ -12,6 +12,7 @@ import org.oppia.android.app.model.HelpIndex.IndexTypeCase.NEXT_AVAILABLE_HINT_I import org.oppia.android.app.model.HelpIndex.IndexTypeCase.SHOW_SOLUTION import org.oppia.android.app.model.State import org.oppia.android.util.threading.BackgroundDispatcher +import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock import javax.inject.Inject import kotlin.concurrent.withLock @@ -65,7 +66,7 @@ class HintHandlerProdImpl private constructor( private var trackedWrongAnswerCount = 0 private lateinit var pendingState: State - private var hintSequenceNumber = 0 + private var hintSequenceNumber = AtomicInteger(0) private var lastRevealedHintIndex = -1 private var latestAvailableHintIndex = -1 @@ -186,7 +187,7 @@ class HintHandlerProdImpl private constructor( // Cancel any potential pending hints by advancing the sequence number. Note that this isn't // reset to 0 to ensure that all previous hint tasks are cancelled, and new tasks can be // scheduled without overlapping with past sequence numbers. - hintSequenceNumber++ + hintSequenceNumber.incrementAndGet() } private fun maybeScheduleShowHint(wrongAnswerCount: Int = trackedWrongAnswerCount) { @@ -295,17 +296,17 @@ class HintHandlerProdImpl private constructor( // Return the index of the first unrevealed hint, or the length of the list if all have been // revealed. val hintList = pendingState.interaction.hintList - val solution = pendingState.interaction.solution val hasHints = hintList.isNotEmpty() - val hasHelp = hasHints || solution.hasCorrectAnswer() + val hasSolution = pendingState.hasSolution() + val hasHelp = hasHints || hasSolution val lastUnrevealedHintIndex = lastRevealedHintIndex + 1 return if (!hasHelp) { HelpIndex.getDefaultInstance() } else if (hasHints && lastUnrevealedHintIndex < hintList.size) { HelpIndex.newBuilder().setNextAvailableHintIndex(lastUnrevealedHintIndex).build() - } else if (solution.hasCorrectAnswer() && !solutionIsRevealed) { + } else if (hasSolution && !solutionIsRevealed) { HelpIndex.newBuilder().setShowSolution(true).build() } else { HelpIndex.newBuilder().setEverythingRevealed(true).build() @@ -317,7 +318,7 @@ class HintHandlerProdImpl private constructor( * cancelling any previously pending hints initiated by calls to this method. */ private fun scheduleShowHint(delayMs: Long, helpIndexToShow: HelpIndex) { - val targetSequenceNumber = ++hintSequenceNumber + val targetSequenceNumber = hintSequenceNumber.incrementAndGet() backgroundCoroutineScope.launch { delay(delayMs) handlerLock.withLock { @@ -331,12 +332,12 @@ class HintHandlerProdImpl private constructor( * pending hints initiated by calls to [scheduleShowHint]. */ private fun showHintImmediately(helpIndexToShow: HelpIndex) { - showHint(++hintSequenceNumber, helpIndexToShow) + showHint(hintSequenceNumber.incrementAndGet(), helpIndexToShow) } private fun showHint(targetSequenceNumber: Int, nextHelpIndexToShow: HelpIndex) { // Only finish this timer if no other hints were scheduled and no cancellations occurred. - if (targetSequenceNumber == hintSequenceNumber) { + if (targetSequenceNumber == hintSequenceNumber.get()) { val previousHelpIndex = computeCurrentHelpIndex() when (nextHelpIndexToShow.indexTypeCase) { @@ -376,7 +377,7 @@ class HintHandlerProdImpl private constructor( } /** Returns whether this state has a solution to show. */ -private fun State.hasSolution(): Boolean = interaction.solution.hasCorrectAnswer() +private fun State.hasSolution(): Boolean = interaction.hasSolution() /** Returns whether this state has help that the user can see. */ internal fun State.offersHelp(): Boolean = interaction.hintList.isNotEmpty() || hasSolution() diff --git a/domain/src/main/java/org/oppia/android/domain/util/StateRetriever.kt b/domain/src/main/java/org/oppia/android/domain/util/StateRetriever.kt index 16a374e7168..bfe0815af2f 100644 --- a/domain/src/main/java/org/oppia/android/domain/util/StateRetriever.kt +++ b/domain/src/main/java/org/oppia/android/domain/util/StateRetriever.kt @@ -60,36 +60,26 @@ class StateRetriever @Inject constructor() { // Creates an interaction from JSON private fun createInteractionFromJson(interactionJson: JSONObject): Interaction { - return Interaction.newBuilder() - .setId(interactionJson.getStringFromObject("id")) - .addAllAnswerGroups( + return Interaction.newBuilder().apply { + id = interactionJson.getStringFromObject("id") + addAllAnswerGroups( createAnswerGroupsFromJson( interactionJson.getJSONArray("answer_groups"), interactionJson.getStringFromObject("id") ) ) - .setDefaultOutcome( - createOutcomeFromJson( - interactionJson.optJSONObject("default_outcome") - ) - ) - .putAllCustomizationArgs( + defaultOutcome = createOutcomeFromJson(interactionJson.optJSONObject("default_outcome")) + putAllCustomizationArgs( createCustomizationArgsMapFromJson( interactionJson.getJSONObject("customization_args"), interactionJson.getStringFromObject("id") ) ) - .addAllHint( - createListOfHintsFromJson( - interactionJson.getJSONArray("hints") - ) - ) - .setSolution( - createSolutionFromJson( - interactionJson.optJSONObject("solution") - ) - ) - .build() + addAllHint(createListOfHintsFromJson(interactionJson.getJSONArray("hints"))) + + // Only set the solution if one has been defined. + createSolutionFromJson(interactionJson.optJSONObject("solution"))?.let { solution = it } + }.build() } // Creates the list of answer group objects from JSON @@ -159,30 +149,33 @@ class StateRetriever @Inject constructor() { } // Creates a solution object from JSON - private fun createSolutionFromJson(solutionJson: JSONObject?): Solution { - if (solutionJson == null) { - return Solution.getDefaultInstance() + private fun createSolutionFromJson(optionalSolutionJson: JSONObject?): Solution? { + return optionalSolutionJson?.let { solutionJson -> + return Solution.newBuilder().apply { + correctAnswer = createCorrectAnswer(solutionJson) + explanation = parseSubtitledHtml(solutionJson.getJSONObject("explanation")) + answerIsExclusive = solutionJson.getBoolean("answer_is_exclusive") + }.build() } - return Solution.newBuilder().apply { - correctAnswer = createCorrectAnswer(solutionJson) - explanation = parseSubtitledHtml(solutionJson.getJSONObject("explanation")) - answerIsExclusive = solutionJson.getBoolean("answer_is_exclusive") - }.build() } private fun createCorrectAnswer(containerObject: JSONObject): CorrectAnswer { val correctAnswerObject = containerObject.optJSONObject("correct_answer") - return if (correctAnswerObject != null) { - CorrectAnswer.newBuilder() - .setNumerator(correctAnswerObject.getInt("numerator")) - .setDenominator(correctAnswerObject.getInt("denominator")) - .setWholeNumber(correctAnswerObject.getInt("wholeNumber")) - .setIsNegative(correctAnswerObject.getBoolean("isNegative")) - .build() - } else { - CorrectAnswer.newBuilder() - .setCorrectAnswer(containerObject.getStringFromObject("correct_answer")) - .build() + return when { + correctAnswerObject != null -> { + CorrectAnswer.newBuilder() + .setNumerator(correctAnswerObject.getInt("numerator")) + .setDenominator(correctAnswerObject.getInt("denominator")) + .setWholeNumber(correctAnswerObject.getInt("wholeNumber")) + .setIsNegative(correctAnswerObject.getBoolean("isNegative")) + .build() + } + containerObject.optString("correct_answer", /* fallback= */ null) != null -> { + CorrectAnswer.newBuilder() + .setCorrectAnswer(containerObject.getStringFromObject("correct_answer")) + .build() + } + else -> CorrectAnswer.getDefaultInstance() // For incompatible types. } } diff --git a/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImplTest.kt b/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImplTest.kt index de5ab65385a..d24fe070cbe 100644 --- a/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImplTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/hintsandsolution/HintHandlerProdImplTest.kt @@ -87,6 +87,11 @@ class HintHandlerProdImplTest { "test_single_interactive_state_exp_with_hints_and_solution" ) } + private val expWithSolutionMissingCorrectAnswer by lazy { + explorationRetriever.loadExploration( + "test_single_interactive_state_exp_with_solution_missing_answer" + ) + } @Before fun setUp() { @@ -1908,6 +1913,48 @@ class HintHandlerProdImplTest { ) } + @Test + fun testGetCurrentHelpIndex_onlySolution_missingCorrectAnswer_isEmpty() { + val state = expWithSolutionMissingCorrectAnswer.getInitialState() + hintHandler.startWatchingForHintsInNewState(state) + + val helpIndex = hintHandler.getCurrentHelpIndex() + + assertThat(helpIndex).isEqualToDefaultInstance() + } + + @Test + fun testGetCurrentHelpIndex_onlySolution_missingCorrectAnswer_twoWrongAnswers_canShowSolution() { + val state = expWithSolutionMissingCorrectAnswer.getInitialState() + hintHandler.startWatchingForHintsInNewState(state) + hintHandler.handleWrongAnswerSubmission(wrongAnswerCount = 1) + hintHandler.handleWrongAnswerSubmission(wrongAnswerCount = 2) + + val helpIndex = hintHandler.getCurrentHelpIndex() + + assertThat(helpIndex).isEqualTo( + HelpIndex.newBuilder().apply { + showSolution = true + }.build() + ) + } + + @Test + fun testGetCurrentHelpIndex_onlySolution_missingCorrectAnswer_triggeredAndShown_allRevealed() { + val state = expWithSolutionMissingCorrectAnswer.getInitialState() + hintHandler.startWatchingForHintsInNewState(state) + waitFor60Seconds() + hintHandler.viewSolution() + + val helpIndex = hintHandler.getCurrentHelpIndex() + + assertThat(helpIndex).isEqualTo( + HelpIndex.newBuilder().apply { + everythingRevealed = true + }.build() + ) + } + private fun Exploration.getInitialState(): State = statesMap.getValue(initStateName) private fun triggerFirstHint() = waitFor60Seconds()