From 24e8b1039c3f5fef320b3021fbd40c3f15df359f Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:21:14 +0300 Subject: [PATCH 01/17] Remove observer after gating result is returned --- .../ExplorationActivityPresenter.kt | 68 ++++++++++--------- .../player/state/StateFragmentPresenter.kt | 23 ++++--- .../SurveyWelcomeDialogFragmentPresenter.kt | 1 + 3 files changed, 53 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index e2247b2367a..2779f4f326e 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -9,6 +9,7 @@ import androidx.appcompat.widget.Toolbar import androidx.core.view.doOnPreDraw import androidx.databinding.DataBindingUtil import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer import androidx.lifecycle.Transformations import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope @@ -528,42 +529,47 @@ class ExplorationActivityPresenter @Inject constructor( } private fun maybeShowSurveyDialog(profileId: ProfileId, topicId: String) { - surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData() - .observe( - activity - ) { gatingResult -> - when (gatingResult) { - is AsyncResult.Pending -> { - oppiaLogger.d("ExplorationActivity", "A gating decision is pending") - } - is AsyncResult.Failure -> { - oppiaLogger.e( - "ExplorationActivity", - "Failed to retrieve gating decision", - gatingResult.error - ) - backPressActivitySelector() - } - is AsyncResult.Success -> { - if (gatingResult.value) { - val dialogFragment = - SurveyWelcomeDialogFragment.newInstance( - profileId, - topicId, - explorationId, - SURVEY_QUESTIONS - ) - val transaction = activity.supportFragmentManager.beginTransaction() - transaction - .add(dialogFragment, TAG_SURVEY_WELCOME_DIALOG) - .addToBackStack(null) - .commit() - } else { + val liveData = surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData() + liveData.observe( + activity, + object : Observer> { + override fun onChanged(gatingResult: AsyncResult?) { + when (gatingResult) { + is AsyncResult.Pending -> { + oppiaLogger.d("ExplorationActivity", "A gating decision is pending") + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "ExplorationActivity", + "Failed to retrieve gating decision", + gatingResult.error + ) backPressActivitySelector() } + is AsyncResult.Success -> { + if (gatingResult.value) { + val dialogFragment = + SurveyWelcomeDialogFragment.newInstance( + profileId, + topicId, + explorationId, + SURVEY_QUESTIONS + ) + val transaction = activity.supportFragmentManager.beginTransaction() + transaction + .add(dialogFragment, TAG_SURVEY_WELCOME_DIALOG) + .addToBackStack(null) + .commit() + } else { + backPressActivitySelector() + } + // Changes to underlying DataProviders will update the gating result. + liveData.removeObserver(this) + } } } } + ) } companion object { diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index 9aabc25f075..aecb966d6d3 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -189,7 +189,6 @@ class StateFragmentPresenter @Inject constructor( fun onReturnToTopicButtonClicked() { hideKeyboard() markExplorationCompleted() - maybeShowSurveyDialog(profileId, topicId) } private fun showOrHideAudioByState(state: State) { @@ -455,13 +454,17 @@ class StateFragmentPresenter @Inject constructor( fun getExplorationCheckpointState() = explorationCheckpointState private fun markExplorationCompleted() { - storyProgressController.recordCompletedChapter( + val markStoryCompletedLivedata = storyProgressController.recordCompletedChapter( profileId, topicId, storyId, explorationId, oppiaClock.getCurrentTimeMs() - ) + ).toLiveData() + + // Only check gating result when the previous operation has completed because gating depends on + // result of saving the time spent in the exploration, at the end of the exploration. + markStoryCompletedLivedata.observe(activity, { maybeShowSurveyDialog(profileId, topicId) }) } private fun showHintsAndSolutions(helpIndex: HelpIndex, isCurrentStatePendingState: Boolean) { @@ -535,10 +538,11 @@ class StateFragmentPresenter @Inject constructor( } private fun maybeShowSurveyDialog(profileId: ProfileId, topicId: String) { - surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData() - .observe( - activity, - { gatingResult -> + val liveData = surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData() + liveData.observe( + activity, + object : Observer> { + override fun onChanged(gatingResult: AsyncResult?) { when (gatingResult) { is AsyncResult.Pending -> { oppiaLogger.d("StateFragment", "A gating decision is pending") @@ -569,10 +573,13 @@ class StateFragmentPresenter @Inject constructor( (activity as StopStatePlayingSessionWithSavedProgressListener) .deleteCurrentProgressAndStopSession(isCompletion = true) } + // Changes to underlying DataProviders will update the gating result. + liveData.removeObserver(this) } } } - ) + } + ) } /** diff --git a/app/src/main/java/org/oppia/android/app/survey/SurveyWelcomeDialogFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/survey/SurveyWelcomeDialogFragmentPresenter.kt index bc24dfacd60..0255528e2fe 100644 --- a/app/src/main/java/org/oppia/android/app/survey/SurveyWelcomeDialogFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/survey/SurveyWelcomeDialogFragmentPresenter.kt @@ -57,6 +57,7 @@ class SurveyWelcomeDialogFragmentPresenter @Inject constructor( } profileManagementController.updateSurveyLastShownTimestamp(profileId) + logSurveyPopUpShownEvent(explorationId, topicId, profileId) return binding.root From a3e4a97a8883c1ff735c065bc6192af725154849 Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Mon, 11 Mar 2024 17:21:36 +0300 Subject: [PATCH 02/17] Minor update to syntax --- .../org/oppia/android/domain/survey/SurveyGatingController.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt b/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt index fcb790a0245..d37e3afa32c 100644 --- a/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt +++ b/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt @@ -66,7 +66,7 @@ class SurveyGatingController @Inject constructor( val currentTimeStamp = oppiaClock.getCurrentTimeMs() val showNextTimestamp = lastShownTimestampMs + gracePeriodMillis - return currentTimeStamp > showNextTimestamp || currentTimeStamp == showNextTimestamp + return currentTimeStamp >= showNextTimestamp } private fun retrieveSurveyLastShownDate(profileId: ProfileId) = From 1233d2b4200d44b901bf0c7d98428f24318f90ba Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:09:26 +0300 Subject: [PATCH 03/17] fix flaky test --- .../oppia/android/domain/survey/SurveyController.kt | 13 ++++--------- .../domain/survey/SurveyProgressController.kt | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/survey/SurveyController.kt b/domain/src/main/java/org/oppia/android/domain/survey/SurveyController.kt index 13ee41e96c9..5310de7c656 100644 --- a/domain/src/main/java/org/oppia/android/domain/survey/SurveyController.kt +++ b/domain/src/main/java/org/oppia/android/domain/survey/SurveyController.kt @@ -5,10 +5,10 @@ import org.oppia.android.app.model.Survey import org.oppia.android.app.model.SurveyQuestion import org.oppia.android.app.model.SurveyQuestionName import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController +import org.oppia.android.domain.oppialogger.survey.SurveyEventsLogger import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders -import org.oppia.android.util.data.DataProviders.Companion.combineWith import org.oppia.android.util.data.DataProviders.Companion.transform import java.util.UUID import javax.inject.Inject @@ -28,7 +28,8 @@ private const val CREATE_QUESTIONS_LIST_PROVIDER_ID = "create_questions_list_pro class SurveyController @Inject constructor( private val dataProviders: DataProviders, private val surveyProgressController: SurveyProgressController, - private val exceptionsController: ExceptionsController + private val exceptionsController: ExceptionsController, + private val surveyEventsLogger: SurveyEventsLogger ) { private val surveyId = UUID.randomUUID().toString() @@ -54,13 +55,7 @@ class SurveyController @Inject constructor( survey.mandatoryQuestionsList + survey.optionalQuestion } else survey.mandatoryQuestionsList } - - val beginSessionDataProvider = - surveyProgressController.beginSurveySession(surveyId, profileId, questionsListDataProvider) - - beginSessionDataProvider.combineWith( - createSurveyDataProvider, START_SURVEY_SESSION_PROVIDER_ID - ) { sessionResult, _ -> sessionResult } + surveyProgressController.beginSurveySession(surveyId, profileId, questionsListDataProvider) } catch (e: Exception) { exceptionsController.logNonFatalException(e) dataProviders.createInMemoryDataProviderAsync(START_SURVEY_SESSION_PROVIDER_ID) { diff --git a/domain/src/main/java/org/oppia/android/domain/survey/SurveyProgressController.kt b/domain/src/main/java/org/oppia/android/domain/survey/SurveyProgressController.kt index b1c412eca01..d997dcce25c 100644 --- a/domain/src/main/java/org/oppia/android/domain/survey/SurveyProgressController.kt +++ b/domain/src/main/java/org/oppia/android/domain/survey/SurveyProgressController.kt @@ -382,8 +382,8 @@ class SurveyProgressController @Inject constructor( if (selectedAnswer.questionName == SurveyQuestionName.NPS) { // compute the feedback question before navigating to it progress.questionGraph.computeFeedbackQuestion( - currentQuestionId + 1, - selectedAnswer.npsScore + index = currentQuestionId + 1, + npsScore = selectedAnswer.npsScore ) } From d750e5691a1ade8316c465463578df1a436d529c Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Wed, 13 Mar 2024 19:21:26 +0300 Subject: [PATCH 04/17] Add survey popup tests --- .../StateFragmentTestActivityPresenter.kt | 50 +++- .../app/player/state/StateFragmentTest.kt | 216 +++++++++++++++++- 2 files changed, 263 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt index 312e5477daa..e7d5e540fef 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt @@ -10,10 +10,14 @@ import org.oppia.android.app.model.ProfileId import org.oppia.android.app.player.exploration.HintsAndSolutionExplorationManagerFragment import org.oppia.android.app.player.exploration.TAG_HINTS_AND_SOLUTION_EXPLORATION_MANAGER import org.oppia.android.app.player.state.StateFragment +import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionWithSavedProgressListener +import org.oppia.android.app.survey.SurveyWelcomeDialogFragment +import org.oppia.android.app.survey.TAG_SURVEY_WELCOME_DIALOG import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.StateFragmentTestActivityBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger +import org.oppia.android.domain.survey.SurveyGatingController import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2 import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 @@ -28,8 +32,9 @@ private const val TEST_ACTIVITY_TAG = "TestActivity" class StateFragmentTestActivityPresenter @Inject constructor( private val activity: AppCompatActivity, private val explorationDataController: ExplorationDataController, + private val surveyGatingController: SurveyGatingController, private val oppiaLogger: OppiaLogger, - private val viewModelProvider: ViewModelProvider + private val viewModelProvider: ViewModelProvider, ) { private var profileId: Int = 1 @@ -112,6 +117,46 @@ class StateFragmentTestActivityPresenter @Inject constructor( ) } + private fun maybeShowSurveyDialog(profileId: ProfileId, topicId: String) { + val liveData = surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData() + liveData.observe( + activity, + object : Observer> { + override fun onChanged(gatingResult: AsyncResult?) { + when (gatingResult) { + is AsyncResult.Pending -> { + oppiaLogger.d("StateFragmentTest", "A gating decision is pending") + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "StateFragmentTest", + "Failed to retrieve gating decision", + gatingResult.error + ) + (activity as StopStatePlayingSessionWithSavedProgressListener) + .deleteCurrentProgressAndStopSession(isCompletion = true) + } + is AsyncResult.Success -> { + if (gatingResult.value) { + val surveyPopup = SurveyWelcomeDialogFragment.newInstance( + profileId, + topicId, + explorationId, + listOf() + ) + activity.supportFragmentManager.beginTransaction() + .add(R.id.state_fragment_placeholder, surveyPopup, TAG_SURVEY_WELCOME_DIALOG) + .commitNow() + } + // Changes to underlying DataProviders will update the gating result. + liveData.removeObserver(this) + } + } + } + } + ) + } + /** * Initializes fragments that depend on ephemeral state (which isn't valid to do until the play * session is fully started). @@ -142,6 +187,9 @@ class StateFragmentTestActivityPresenter @Inject constructor( private fun finishExploration(isCompletion: Boolean) { explorationDataController.stopPlayingExploration(isCompletion) + val userProfileId = ProfileId.newBuilder().setInternalId(profileId).build() + maybeShowSurveyDialog(userProfileId, topicId) + getStateFragment()?.let { fragment -> activity.supportFragmentManager.beginTransaction().remove(fragment).commitNow() } 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 24193e64a33..af23c595231 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 @@ -28,6 +28,7 @@ import androidx.test.espresso.contrib.RecyclerViewActions.scrollToHolder import androidx.test.espresso.intent.Intents import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.Visibility.GONE +import androidx.test.espresso.matcher.ViewMatchers.Visibility.VISIBLE import androidx.test.espresso.matcher.ViewMatchers.hasChildCount import androidx.test.espresso.matcher.ViewMatchers.isClickable import androidx.test.espresso.matcher.ViewMatchers.isDisplayed @@ -168,6 +169,7 @@ import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.CoroutineExecutorService import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClock import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule @@ -216,12 +218,12 @@ class StateFragmentTest { @Inject lateinit var testGlideImageLoader: TestGlideImageLoader @Inject lateinit var profileManagementController: ProfileManagementController @Inject lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger + @Inject lateinit var oppiaClock: FakeOppiaClock private val profileId = ProfileId.newBuilder().apply { internalId = 1 }.build() @After fun tearDown() { - testCoroutineDispatchers.unregisterIdlingResource() Intents.release() } @@ -4227,6 +4229,181 @@ class StateFragmentTest { } } + @Test + fun testFinishChapter_lateNight_isPastGracePeriod_minimumAggregateTimeMet_noSurveyPopup() { + setUpTestWithSurveyFeatureOn() + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(LATE_NIGHT_UTC_TIMESTAMP_MILLIS) + + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = true).use { + startPlayingExploration() + + playThroughPrototypeExploration() + + oppiaClock.setCurrentTimeMs(LATE_NIGHT_UTC_TIMESTAMP_MILLIS + SESSION_LENGTH_LONG) + + clickReturnToTopicButton() + + // Check that the fragment is removed. + // In production, the activity is finished and TopicActivity is navigated to, but since this + // test runs in a test activity, once the test completes, the fragment is removed and the + // placeholders are displayed instead. + onView(withId(R.id.play_test_exploration_button)).check( + matches( + withEffectiveVisibility( + VISIBLE + ) + ) + ) + } + } + + @Test + fun testFinishChapter_earlyMorning_isPastGracePeriod_minimumAggregateTimeMet_noSurveyPopup() { + setUpTestWithSurveyFeatureOn() + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(EARLY_MORNING_UTC_TIMESTAMP_MILLIS) + + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = true).use { + startPlayingExploration() + + playThroughPrototypeExploration() + + oppiaClock.setCurrentTimeMs(EARLY_MORNING_UTC_TIMESTAMP_MILLIS + SESSION_LENGTH_LONG) + + clickReturnToTopicButton() + + // Check that the fragment is removed. + // In production, the activity is finished and TopicActivity is navigated to, but since this + // test runs in a test activity, once the test completes, the fragment is removed and the + // placeholders are displayed instead. + onView(withId(R.id.play_test_exploration_button)).check( + matches( + withEffectiveVisibility( + VISIBLE + ) + ) + ) + } + } + + @Test + fun testFinishChapter_midMorning_isPastGracePeriod_minimumAggregateTimeMet_surveyPopupShown() { + setUpTestWithSurveyFeatureOn() + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(MID_MORNING_UTC_TIMESTAMP_MILLIS) + + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = true).use { + startPlayingExploration() + + playThroughPrototypeExploration() + + oppiaClock.setCurrentTimeMs(MID_MORNING_UTC_TIMESTAMP_MILLIS + SESSION_LENGTH_LONG) + + clickReturnToTopicButton() + + onView(withId(R.id.survey_onboarding_title_text)) + .check( + matches( + allOf( + withText(R.string.survey_onboarding_title_text), + isDisplayed() + ) + ) + ) + } + } + + @Test + fun testFinishChapter_afternoon_isPastGracePeriod_minimumAggregateTimeMet_surveyPopupShown() { + setUpTestWithSurveyFeatureOn() + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(AFTERNOON_UTC_TIMESTAMP_MILLIS) + + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = true).use { + startPlayingExploration() + + playThroughPrototypeExploration() + + oppiaClock.setCurrentTimeMs(AFTERNOON_UTC_TIMESTAMP_MILLIS + SESSION_LENGTH_LONG) + + clickReturnToTopicButton() + + onView(withId(R.id.survey_onboarding_title_text)) + .check( + matches( + allOf( + withText(R.string.survey_onboarding_title_text), + isDisplayed() + ) + ) + ) + } + } + + @Test + fun testFinishChapter_evening_isPastGracePeriod_minimumAggregateTimeMet_surveyPopupShown() { + setUpTestWithSurveyFeatureOn() + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(EVENING_UTC_TIMESTAMP_MILLIS) + + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = true).use { + startPlayingExploration() + + playThroughPrototypeExploration() + + oppiaClock.setCurrentTimeMs(EVENING_UTC_TIMESTAMP_MILLIS + SESSION_LENGTH_LONG) + + clickReturnToTopicButton() + + onView(withId(R.id.survey_onboarding_title_text)) + .check( + matches( + allOf( + withText(R.string.survey_onboarding_title_text), + isDisplayed() + ) + ) + ) + } + } + + @Test + fun testFinishChapter_surveyFeatureOff_allGatingConditionsMet_noSurveyPopup() { + // Survey Gating conditions are: isPastGracePeriod, has achieved minimum aggregate exploration + // time of 5min in a topic, and is within the hours of 9am and 10pm in the user's local time. + + // The default surveyLastShownTimestamp is set to the beginning of epoch which will always be + // more than the grace period days in the past, so no need to explicitly define + // surveyLastShownTimestamp for computing the grace period. + + setUpTestWithSurveyFeatureOff() + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(AFTERNOON_UTC_TIMESTAMP_MILLIS) + + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = true).use { + startPlayingExploration() + + playThroughPrototypeExploration() + + oppiaClock.setCurrentTimeMs(EVENING_UTC_TIMESTAMP_MILLIS + SESSION_LENGTH_LONG) + + clickReturnToTopicButton() + + // Check that the fragment is removed. + // In production, the activity is finished and TopicActivity is navigated to, but since this + // test runs in a test activity, once the test completes, the fragment is removed and the + // placeholders are displayed instead. + onView(withId(R.id.play_test_exploration_button)).check( + matches( + withEffectiveVisibility( + VISIBLE + ) + ) + ) + } + } + private fun addShadowMediaPlayerException(dataSource: Any, exception: Exception) { val classLoader = StateFragmentTest::class.java.classLoader!! val shadowMediaPlayerClass = classLoader.loadClass("org.robolectric.shadows.ShadowMediaPlayer") @@ -4776,11 +4953,23 @@ class StateFragmentTest { setUpTest() } + private fun setUpTestWithSurveyFeatureOn() { + TestPlatformParameterModule.forceEnableNpsSurvey(true) + setUpTest() + } + + private fun setUpTestWithSurveyFeatureOff() { + TestPlatformParameterModule.forceEnableNpsSurvey(false) + setUpTest() + } + private fun setUpTest() { Intents.init() setUpTestApplicationComponent() testCoroutineDispatchers.registerIdlingResource() - profileTestHelper.initializeProfiles() + profileTestHelper.initializeProfiles().also { + testCoroutineDispatchers.unregisterIdlingResource() + } // Initialize Glide such that all of its executors use the same shared dispatcher pool as the // rest of Oppia so that thread execution can be synchronized via Oppia's test coroutine @@ -5021,4 +5210,27 @@ class StateFragmentTest { override fun getApplicationInjector(): ApplicationInjector = component } + + private companion object { + // Date & time: Wed Apr 24 2019 08:22:03 GMT. + private const val EARLY_MORNING_UTC_TIMESTAMP_MILLIS = 1556094123000 + + // Date & time: Wed Apr 24 2019 10:30:12 GMT. + private const val MID_MORNING_UTC_TIMESTAMP_MILLIS = 1556101812000 + + // Date & time: Tue Apr 23 2019 14:22:00 GMT. + private const val AFTERNOON_UTC_TIMESTAMP_MILLIS = 1556029320000 + + // Date & time: Tue Apr 23 2019 21:26:12 GMT. + private const val EVENING_UTC_TIMESTAMP_MILLIS = 1556054772000 + + // Date & time: Tue Apr 23 2019 23:22:00 GMT. + private const val LATE_NIGHT_UTC_TIMESTAMP_MILLIS = 1556061720000 + + // Exploration play through time less than the required 5 min + private const val SESSION_LENGTH_SHORT = 120000L + + // Exploration play through time greater than the required 5 min + private const val SESSION_LENGTH_LONG = 360000L + } } From 152d15709dd1d086440b191f5bdc2fb6b767b2b8 Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Wed, 13 Mar 2024 19:53:13 +0300 Subject: [PATCH 05/17] Disable robolectric runs as drag and drop interaction won't work, as documented in https://github.com/oppia/oppia-android/issues/1612 --- .../org/oppia/android/app/player/state/StateFragmentTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) 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 af23c595231..9a65ddf4fb1 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 @@ -4230,6 +4230,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. fun testFinishChapter_lateNight_isPastGracePeriod_minimumAggregateTimeMet_noSurveyPopup() { setUpTestWithSurveyFeatureOn() oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) @@ -4259,6 +4260,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. fun testFinishChapter_earlyMorning_isPastGracePeriod_minimumAggregateTimeMet_noSurveyPopup() { setUpTestWithSurveyFeatureOn() oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) @@ -4288,6 +4290,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. fun testFinishChapter_midMorning_isPastGracePeriod_minimumAggregateTimeMet_surveyPopupShown() { setUpTestWithSurveyFeatureOn() oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) @@ -4315,6 +4318,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. fun testFinishChapter_afternoon_isPastGracePeriod_minimumAggregateTimeMet_surveyPopupShown() { setUpTestWithSurveyFeatureOn() oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) @@ -4342,6 +4346,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. fun testFinishChapter_evening_isPastGracePeriod_minimumAggregateTimeMet_surveyPopupShown() { setUpTestWithSurveyFeatureOn() oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) @@ -4369,6 +4374,7 @@ class StateFragmentTest { } @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. fun testFinishChapter_surveyFeatureOff_allGatingConditionsMet_noSurveyPopup() { // Survey Gating conditions are: isPastGracePeriod, has achieved minimum aggregate exploration // time of 5min in a topic, and is within the hours of 9am and 10pm in the user's local time. From a257c56146fb2911bed47d84be99454d1e6e2db5 Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Fri, 22 Mar 2024 01:11:44 +0300 Subject: [PATCH 06/17] Additional tests for the SurveyGatingController --- .../survey/SurveyGatingControllerTest.kt | 140 +++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/domain/src/test/java/org/oppia/android/domain/survey/SurveyGatingControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/survey/SurveyGatingControllerTest.kt index df46b018821..b757364aab3 100644 --- a/domain/src/test/java/org/oppia/android/domain/survey/SurveyGatingControllerTest.kt +++ b/domain/src/test/java/org/oppia/android/domain/survey/SurveyGatingControllerTest.kt @@ -49,6 +49,7 @@ import javax.inject.Singleton private const val SESSION_LENGTH_SHORT = 120000L private const val SESSION_LENGTH_LONG = 360000L +private const val SESSION_LENGTH_MINIMUM = 300000L /** Tests for [SurveyGatingController]. */ @RunWith(AndroidJUnit4::class) @@ -120,6 +121,22 @@ class SurveyGatingControllerTest { @Test fun testGating_lateNight_isPastGracePeriod_minimumAggregateTimeMet_returnsFalse() { + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(LATE_NIGHT_UTC_TIMESTAMP_MILLIS) + // The default surveyLastShownTimestamp is set to the beginning of epoch which will always be + // more than the grace period days in the past, so no need to explicitly define + // surveyLastShownTimestamp here. + startAndEndExplorationSession(SESSION_LENGTH_MINIMUM, PROFILE_ID_0, TEST_TOPIC_ID_0) + + val gatingProvider = surveyGatingController.maybeShowSurvey(PROFILE_ID_0, TEST_TOPIC_ID_0) + + val result = monitorFactory.waitForNextSuccessfulResult(gatingProvider) + + assertThat(result).isFalse() + } + + @Test + fun testGating_lateNight_isPastGracePeriod_minimumAggregateTimeExceeded_returnsFalse() { oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) oppiaClock.setCurrentTimeMs(LATE_NIGHT_UTC_TIMESTAMP_MILLIS) // The default surveyLastShownTimestamp is set to the beginning of epoch which will always be @@ -168,6 +185,22 @@ class SurveyGatingControllerTest { @Test fun testGating_earlyMorning_isPastGracePeriod_minimumAggregateTimeMet_returnsFalse() { + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(EARLY_MORNING_UTC_TIMESTAMP_MILLIS) + // The default surveyLastShownTimestamp is set to the beginning of epoch which will always be + // more than the grace period days in the past, so no need to explicitly define + // surveyLastShownTimestamp here. + startAndEndExplorationSession(SESSION_LENGTH_MINIMUM, PROFILE_ID_0, TEST_TOPIC_ID_0) + + val gatingProvider = surveyGatingController.maybeShowSurvey(PROFILE_ID_0, TEST_TOPIC_ID_0) + + val result = monitorFactory.waitForNextSuccessfulResult(gatingProvider) + + assertThat(result).isFalse() + } + + @Test + fun testGating_earlyMorning_isPastGracePeriod_minimumAggregateTimeExceeded_returnsFalse() { oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) oppiaClock.setCurrentTimeMs(EARLY_MORNING_UTC_TIMESTAMP_MILLIS) // The default surveyLastShownTimestamp is set to the beginning of epoch which will always be @@ -200,6 +233,22 @@ class SurveyGatingControllerTest { @Test fun testGating_midMorning_stillWithinGracePeriod_minimumAggregateTimeMet_returnsFalse() { + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(MID_MORNING_UTC_TIMESTAMP_MILLIS) + monitorFactory.ensureDataProviderExecutes( + profileManagementController.updateSurveyLastShownTimestamp(PROFILE_ID_0) + ) + startAndEndExplorationSession(SESSION_LENGTH_MINIMUM, PROFILE_ID_0, TEST_TOPIC_ID_0) + + val gatingProvider = surveyGatingController.maybeShowSurvey(PROFILE_ID_0, TEST_TOPIC_ID_0) + + val result = monitorFactory.waitForNextSuccessfulResult(gatingProvider) + + assertThat(result).isFalse() + } + + @Test + fun testGating_midMorning_stillWithinGracePeriod_minimumAggregateTimeExceeded_returnsFalse() { oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) oppiaClock.setCurrentTimeMs(MID_MORNING_UTC_TIMESTAMP_MILLIS) monitorFactory.ensureDataProviderExecutes( @@ -232,6 +281,25 @@ class SurveyGatingControllerTest { @Test fun testGating_midMorning_isPastGracePeriod_minimumAggregateTimeMet_returnsTrue() { + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + startAndEndExplorationSession(SESSION_LENGTH_MINIMUM, PROFILE_ID_0, TEST_TOPIC_ID_0) + + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(MID_MORNING_UTC_TIMESTAMP_MILLIS) + + // The default surveyLastShownTimestamp is set to the beginning of epoch which will always be + // more than the grace period days in the past, so no need to explicitly define + // surveyLastShownTimestamp here. + + val gatingProvider = surveyGatingController.maybeShowSurvey(PROFILE_ID_0, TEST_TOPIC_ID_0) + + val result = monitorFactory.waitForNextSuccessfulResult(gatingProvider) + + assertThat(result).isTrue() + } + + @Test + fun testGating_midMorning_isPastGracePeriod_minimumAggregateTimeExceeded_returnsTrue() { oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) startAndEndExplorationSession(SESSION_LENGTH_LONG, PROFILE_ID_0, TEST_TOPIC_ID_0) @@ -266,7 +334,23 @@ class SurveyGatingControllerTest { } @Test - fun testGating_afternoon_stillWithinGracePeriod__minimumAggregateTimeMet_returnsFalse() { + fun testGating_afternoon_stillWithinGracePeriod_minimumAggregateTimeMet_returnsFalse() { + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(AFTERNOON_UTC_TIMESTAMP_MILLIS) + monitorFactory.ensureDataProviderExecutes( + profileManagementController.updateSurveyLastShownTimestamp(PROFILE_ID_0) + ) + startAndEndExplorationSession(SESSION_LENGTH_MINIMUM, PROFILE_ID_0, TEST_TOPIC_ID_0) + + val gatingProvider = surveyGatingController.maybeShowSurvey(PROFILE_ID_0, TEST_TOPIC_ID_0) + + val result = monitorFactory.waitForNextSuccessfulResult(gatingProvider) + + assertThat(result).isFalse() + } + + @Test + fun testGating_afternoon_stillWithinGracePeriod_minimumAggregateTimeExceeded_returnsFalse() { oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) oppiaClock.setCurrentTimeMs(AFTERNOON_UTC_TIMESTAMP_MILLIS) monitorFactory.ensureDataProviderExecutes( @@ -301,6 +385,25 @@ class SurveyGatingControllerTest { @Test fun testGating_afternoon_isPastGracePeriod_minimumAggregateTimeMet_returnsTrue() { + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + startAndEndExplorationSession(SESSION_LENGTH_MINIMUM, PROFILE_ID_0, TEST_TOPIC_ID_0) + + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(AFTERNOON_UTC_TIMESTAMP_MILLIS) + + // The default surveyLastShownTimestamp is set to the beginning of epoch which will always be + // more than the grace period days in the past, so no need to explicitly define + // surveyLastShownTimestamp here. + + val gatingProvider = surveyGatingController.maybeShowSurvey(PROFILE_ID_0, TEST_TOPIC_ID_0) + + val result = monitorFactory.waitForNextSuccessfulResult(gatingProvider) + + assertThat(result).isTrue() + } + + @Test + fun testGating_afternoon_isPastGracePeriod_minimumAggregateTimeExceeded_returnsTrue() { oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) startAndEndExplorationSession(SESSION_LENGTH_LONG, PROFILE_ID_0, TEST_TOPIC_ID_0) @@ -336,6 +439,22 @@ class SurveyGatingControllerTest { @Test fun testGating_evening_stillWithinGracePeriod_minimumAggregateTimeMet_returnsFalse() { + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(EVENING_UTC_TIMESTAMP_MILLIS) + monitorFactory.ensureDataProviderExecutes( + profileManagementController.updateSurveyLastShownTimestamp(PROFILE_ID_0) + ) + startAndEndExplorationSession(SESSION_LENGTH_MINIMUM, PROFILE_ID_0, TEST_TOPIC_ID_0) + + val gatingProvider = surveyGatingController.maybeShowSurvey(PROFILE_ID_0, TEST_TOPIC_ID_0) + + val result = monitorFactory.waitForNextSuccessfulResult(gatingProvider) + + assertThat(result).isFalse() + } + + @Test + fun testGating_evening_stillWithinGracePeriod_minimumAggregateTimeExceeded_returnsFalse() { oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) oppiaClock.setCurrentTimeMs(EVENING_UTC_TIMESTAMP_MILLIS) monitorFactory.ensureDataProviderExecutes( @@ -370,6 +489,25 @@ class SurveyGatingControllerTest { @Test fun testGating_evening_isPastGracePeriod_minimumAggregateTimeMet_returnsTrue() { + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + startAndEndExplorationSession(SESSION_LENGTH_MINIMUM, PROFILE_ID_0, TEST_TOPIC_ID_0) + + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(EVENING_UTC_TIMESTAMP_MILLIS) + + // The default surveyLastShownTimestamp is set to the beginning of epoch which will always be + // more than the grace period days in the past, so no need to explicitly define + // surveyLastShownTimestamp here. + + val gatingProvider = surveyGatingController.maybeShowSurvey(PROFILE_ID_0, TEST_TOPIC_ID_0) + + val result = monitorFactory.waitForNextSuccessfulResult(gatingProvider) + + assertThat(result).isTrue() + } + + @Test + fun testGating_evening_isPastGracePeriod_minimumAggregateTimeExceeded_returnsTrue() { oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) startAndEndExplorationSession(SESSION_LENGTH_LONG, PROFILE_ID_0, TEST_TOPIC_ID_0) From a1f440b49a182c8e4c88e3530a57f00990a67080 Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Fri, 22 Mar 2024 01:17:19 +0300 Subject: [PATCH 07/17] Remove unused parameter --- .../java/org/oppia/android/domain/survey/SurveyController.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/domain/src/main/java/org/oppia/android/domain/survey/SurveyController.kt b/domain/src/main/java/org/oppia/android/domain/survey/SurveyController.kt index 5310de7c656..ad76139194a 100644 --- a/domain/src/main/java/org/oppia/android/domain/survey/SurveyController.kt +++ b/domain/src/main/java/org/oppia/android/domain/survey/SurveyController.kt @@ -5,7 +5,6 @@ import org.oppia.android.app.model.Survey import org.oppia.android.app.model.SurveyQuestion import org.oppia.android.app.model.SurveyQuestionName import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController -import org.oppia.android.domain.oppialogger.survey.SurveyEventsLogger import org.oppia.android.util.data.AsyncResult import org.oppia.android.util.data.DataProvider import org.oppia.android.util.data.DataProviders @@ -28,8 +27,7 @@ private const val CREATE_QUESTIONS_LIST_PROVIDER_ID = "create_questions_list_pro class SurveyController @Inject constructor( private val dataProviders: DataProviders, private val surveyProgressController: SurveyProgressController, - private val exceptionsController: ExceptionsController, - private val surveyEventsLogger: SurveyEventsLogger + private val exceptionsController: ExceptionsController ) { private val surveyId = UUID.randomUUID().toString() From f4f85f6f9e11909a84bb2f5944f06075474a22ad Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Sat, 23 Mar 2024 04:37:53 +0300 Subject: [PATCH 08/17] Improve tests --- .../ExplorationActivityPresenter.kt | 6 +- .../player/state/StateFragmentPresenter.kt | 5 +- .../StateFragmentTestActivityPresenter.kt | 53 +------------ .../exploration/ExplorationActivityTest.kt | 77 ++++++++++++++++++- .../app/player/state/StateFragmentTest.kt | 34 ++++++++ 5 files changed, 116 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index 2779f4f326e..ee5ce12deec 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -298,11 +298,7 @@ class ExplorationActivityPresenter @Inject constructor( } is AsyncResult.Success -> { oppiaLogger.d("ExplorationActivity", "Successfully stopped exploration") - if (isCompletion) { - maybeShowSurveyDialog(profileId, topicId) - } else { - backPressActivitySelector() - } + maybeShowSurveyDialog(profileId, topicId) } } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index aecb966d6d3..c8450fe3c98 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -569,12 +569,13 @@ class StateFragmentPresenter @Inject constructor( transaction .add(dialogFragment, TAG_SURVEY_WELCOME_DIALOG) .commitNow() + + // Changes to underlying DataProviders will update the gating result. + liveData.removeObserver(this) } else { (activity as StopStatePlayingSessionWithSavedProgressListener) .deleteCurrentProgressAndStopSession(isCompletion = true) } - // Changes to underlying DataProviders will update the gating result. - liveData.removeObserver(this) } } } diff --git a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt index e7d5e540fef..482451ae6b7 100644 --- a/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/testing/StateFragmentTestActivityPresenter.kt @@ -3,21 +3,16 @@ package org.oppia.android.app.player.state.testing import android.widget.Button import androidx.appcompat.app.AppCompatActivity import androidx.databinding.DataBindingUtil -import androidx.lifecycle.Observer import org.oppia.android.R import org.oppia.android.app.activity.ActivityScope import org.oppia.android.app.model.ProfileId import org.oppia.android.app.player.exploration.HintsAndSolutionExplorationManagerFragment import org.oppia.android.app.player.exploration.TAG_HINTS_AND_SOLUTION_EXPLORATION_MANAGER import org.oppia.android.app.player.state.StateFragment -import org.oppia.android.app.player.stopplaying.StopStatePlayingSessionWithSavedProgressListener -import org.oppia.android.app.survey.SurveyWelcomeDialogFragment -import org.oppia.android.app.survey.TAG_SURVEY_WELCOME_DIALOG import org.oppia.android.app.viewmodel.ViewModelProvider import org.oppia.android.databinding.StateFragmentTestActivityBinding import org.oppia.android.domain.exploration.ExplorationDataController import org.oppia.android.domain.oppialogger.OppiaLogger -import org.oppia.android.domain.survey.SurveyGatingController import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2 import org.oppia.android.domain.topic.TEST_STORY_ID_0 import org.oppia.android.domain.topic.TEST_TOPIC_ID_0 @@ -32,9 +27,8 @@ private const val TEST_ACTIVITY_TAG = "TestActivity" class StateFragmentTestActivityPresenter @Inject constructor( private val activity: AppCompatActivity, private val explorationDataController: ExplorationDataController, - private val surveyGatingController: SurveyGatingController, private val oppiaLogger: OppiaLogger, - private val viewModelProvider: ViewModelProvider, + private val viewModelProvider: ViewModelProvider ) { private var profileId: Int = 1 @@ -103,7 +97,7 @@ class StateFragmentTestActivityPresenter @Inject constructor( } startPlayingProvider.toLiveData().observe( activity, - Observer> { result -> + { result -> when (result) { is AsyncResult.Pending -> oppiaLogger.d(TEST_ACTIVITY_TAG, "Loading exploration") is AsyncResult.Failure -> @@ -117,46 +111,6 @@ class StateFragmentTestActivityPresenter @Inject constructor( ) } - private fun maybeShowSurveyDialog(profileId: ProfileId, topicId: String) { - val liveData = surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData() - liveData.observe( - activity, - object : Observer> { - override fun onChanged(gatingResult: AsyncResult?) { - when (gatingResult) { - is AsyncResult.Pending -> { - oppiaLogger.d("StateFragmentTest", "A gating decision is pending") - } - is AsyncResult.Failure -> { - oppiaLogger.e( - "StateFragmentTest", - "Failed to retrieve gating decision", - gatingResult.error - ) - (activity as StopStatePlayingSessionWithSavedProgressListener) - .deleteCurrentProgressAndStopSession(isCompletion = true) - } - is AsyncResult.Success -> { - if (gatingResult.value) { - val surveyPopup = SurveyWelcomeDialogFragment.newInstance( - profileId, - topicId, - explorationId, - listOf() - ) - activity.supportFragmentManager.beginTransaction() - .add(R.id.state_fragment_placeholder, surveyPopup, TAG_SURVEY_WELCOME_DIALOG) - .commitNow() - } - // Changes to underlying DataProviders will update the gating result. - liveData.removeObserver(this) - } - } - } - } - ) - } - /** * Initializes fragments that depend on ephemeral state (which isn't valid to do until the play * session is fully started). @@ -187,9 +141,6 @@ class StateFragmentTestActivityPresenter @Inject constructor( private fun finishExploration(isCompletion: Boolean) { explorationDataController.stopPlayingExploration(isCompletion) - val userProfileId = ProfileId.newBuilder().setInternalId(profileId).build() - maybeShowSurveyDialog(userProfileId, topicId) - getStateFragment()?.let { fragment -> activity.supportFragmentManager.beginTransaction().remove(fragment).commitNow() } diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index 800916c4391..075a1dff49b 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -224,6 +224,8 @@ class ExplorationActivityTest { private val internalProfileId: Int = 0 + private val afternoonUtcTimestampMillis = 1556101812000 + @Before fun setUp() { Intents.init() @@ -231,11 +233,11 @@ class ExplorationActivityTest { testCoroutineDispatchers.registerIdlingResource() profileTestHelper.initializeProfiles() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + testCoroutineDispatchers.unregisterIdlingResource() } @After fun tearDown() { - testCoroutineDispatchers.unregisterIdlingResource() Intents.release() } @@ -2313,6 +2315,79 @@ class ExplorationActivityTest { } } + @Test + fun testExplorationActivity_closeExploration_surveyGatingCriteriaMet_showsSurveyPopup() { + markAllSpotlightsSeen() + + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis) + + launch( + createExplorationActivityIntent( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2, + shouldSavePartialProgress = false + ) + ).use { + explorationDataController.startPlayingNewExploration( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + testCoroutineDispatchers.runCurrent() + + fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis + 360_000L) + + onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)).perform(click()) + onView(withText(R.string.stop_exploration_dialog_leave_button)).inRoot(isDialog()) + .perform(click()) + onView(withText(R.string.stop_exploration_dialog_leave_button)).inRoot(isDialog()) + testCoroutineDispatchers.runCurrent() + + onView(withText(R.string.survey_onboarding_title_text)).inRoot(isDialog()) + .check(matches(isDisplayed())) + onView(withText(R.string.survey_onboarding_message_text)).inRoot(isDialog()) + .check(matches(isDisplayed())) + } + } + + @Test + fun testExplorationActivity_closeExploration_surveyGatingCriteriaNotMet_noSurveyPopup() { + markAllSpotlightsSeen() + + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis) + + launch( + createExplorationActivityIntent( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2, + shouldSavePartialProgress = false + ) + ).use { + explorationDataController.startPlayingNewExploration( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + testCoroutineDispatchers.runCurrent() + + // Time not advanced to simulate minimum aggregate learning time not achieved. + + onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)).perform(click()) + onView(withText(R.string.stop_exploration_dialog_leave_button)).inRoot(isDialog()) + .perform(click()) + + onView(withText(R.string.survey_onboarding_title_text)).check(doesNotExist()) + } + } + private fun markSpotlightSeen(feature: Spotlight.FeatureCase) { val profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() spotlightStateController.markSpotlightViewed(profileId, feature) 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 9a65ddf4fb1..11c9ccea3cf 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 @@ -4373,6 +4373,40 @@ class StateFragmentTest { } } + @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. + fun testFinishChapter_allGatingConditionsMet_surveyDismissed_popupDoesNotShowAgain() { + setUpTestWithSurveyFeatureOn() + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(EVENING_UTC_TIMESTAMP_MILLIS) + + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = true).use { + startPlayingExploration() + + playThroughPrototypeExploration() + + oppiaClock.setCurrentTimeMs(EVENING_UTC_TIMESTAMP_MILLIS + SESSION_LENGTH_LONG) + + clickReturnToTopicButton() + + onView(withId(R.id.maybe_later_button)) + .perform(click()) + testCoroutineDispatchers.runCurrent() + + // Check that the fragment is removed. + // When the survey popup is shown, the lastShownDateProvider is updated with current time, + // consequently updating the combined gating data provider. Recomputation of the gating result + // should not re-trigger the survey. + onView(withId(R.id.play_test_exploration_button)).check( + matches( + withEffectiveVisibility( + VISIBLE + ) + ) + ) + } + } + @Test @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. fun testFinishChapter_surveyFeatureOff_allGatingConditionsMet_noSurveyPopup() { From 848fc1dd5d1a04b79aec392c15ec08c59f7d4d2b Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:18:18 +0300 Subject: [PATCH 09/17] Fix failing test --- .../exploration/ExplorationActivityTest.kt | 81 ++++++++++++------- 1 file changed, 50 insertions(+), 31 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index 075a1dff49b..d8ae71fe698 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -50,7 +50,6 @@ import org.hamcrest.Description import org.hamcrest.Matcher import org.hamcrest.Matchers.not import org.junit.After -import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -79,7 +78,6 @@ import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionMo import org.oppia.android.app.player.state.itemviewmodel.StateItemViewModel import org.oppia.android.app.recyclerview.RecyclerViewMatcher.Companion.atPositionOnView import org.oppia.android.app.shim.ViewBindingShimModule -import org.oppia.android.app.testing.ExplorationInjectionActivity import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule import org.oppia.android.app.utility.EspressoTestsMatchers.withDrawable import org.oppia.android.app.utility.OrientationChangeAction.Companion.orientationLandscape @@ -226,16 +224,6 @@ class ExplorationActivityTest { private val afternoonUtcTimestampMillis = 1556101812000 - @Before - fun setUp() { - Intents.init() - setUpTestApplicationComponent() - testCoroutineDispatchers.registerIdlingResource() - profileTestHelper.initializeProfiles() - fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) - testCoroutineDispatchers.unregisterIdlingResource() - } - @After fun tearDown() { Intents.release() @@ -245,25 +233,6 @@ class ExplorationActivityTest { ApplicationProvider.getApplicationContext().inject(this) } - private fun getApplicationDependencies( - internalProfileId: Int, - topicId: String, - storyId: String, - explorationId: String - ) { - launch(ExplorationInjectionActivity::class.java).use { - it.onActivity { activity -> - explorationDataController = activity.explorationDataController - explorationDataController.startPlayingNewExploration( - internalProfileId, - topicId, - storyId, - explorationId - ) - } - } - } - // TODO(#388): Fill in remaining tests for this activity. @get:Rule var explorationActivityTestRule: ActivityTestRule = ActivityTestRule( @@ -287,6 +256,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_hasCorrectActivityLabel() { + setUpTest() markAllSpotlightsSeen() explorationActivityTestRule.launchActivity( createExplorationActivityIntent( @@ -330,6 +300,7 @@ class ExplorationActivityTest { @Test fun testExploration_toolbarTitle_readerOff_marqueeInRtl_isDisplayedCorrectly() { + setUpTest() markAllSpotlightsSeen() fakeAccessibilityService.setScreenReaderEnabled(false) explorationActivityTestRule.launchActivity( @@ -353,6 +324,7 @@ class ExplorationActivityTest { @Test fun testExploration_toolbarTitle_readerOn_marqueeInRtl_isDisplayedCorrectly() { + setUpTest() markAllSpotlightsSeen() fakeAccessibilityService.setScreenReaderEnabled(true) explorationActivityTestRule.launchActivity( @@ -376,6 +348,7 @@ class ExplorationActivityTest { @Test fun testExploration_toolbarTitle_readerOff_marqueeInLtr_isDisplayedCorrectly() { + setUpTest() markAllSpotlightsSeen() fakeAccessibilityService.setScreenReaderEnabled(false) explorationActivityTestRule.launchActivity( @@ -399,6 +372,7 @@ class ExplorationActivityTest { @Test fun testExploration_toolbarTitle_readerOn_marqueeInLtr_isDisplayedCorrectly() { + setUpTest() markAllSpotlightsSeen() fakeAccessibilityService.setScreenReaderEnabled(true) explorationActivityTestRule.launchActivity( @@ -721,6 +695,7 @@ class ExplorationActivityTest { @Test fun testExploration_clickAudioIcon_contentDescription_changesCorrectly() { + setUpTest() markAllSpotlightsSeen() setUpAudioForFractionLesson() launch( @@ -822,6 +797,7 @@ class ExplorationActivityTest { @Test fun testAudioWithNoConnection_openRatioExploration_clickAudioIcon_checkOpensNoConnectionDialog() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -851,6 +827,7 @@ class ExplorationActivityTest { @Test fun testAudioWithCellular_openRatioExploration_clickAudioIcon_checkOpensCellularAudioDialog() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -880,6 +857,7 @@ class ExplorationActivityTest { @Test fun testAudioCellular_ratioExp_audioIcon_configChange_opensCellularAudioDialog() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -910,6 +888,7 @@ class ExplorationActivityTest { @Test fun testAudioCellular_ratioExp_audioIcon_clickNegative_audioFragmentIsHidden() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -949,6 +928,7 @@ class ExplorationActivityTest { @Test fun testAudioCellular_ratioExp_audioIcon_clickPositive_checkAudioFragmentIsVisible() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -995,6 +975,7 @@ class ExplorationActivityTest { @Test fun testAudioCellular_ratioExp_check_negative_audioIcon_audioFragHiddenDialogNotDisplay() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -1038,6 +1019,7 @@ class ExplorationActivityTest { @Test fun testAudioCellular_ratioExp_checkPositive_audioIconTwice_audioFragVisDialogNotDisplay() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -1082,6 +1064,7 @@ class ExplorationActivityTest { @Test fun testAudioWifi_ratioExp_audioIcon_audioFragHasDefaultLangAndAutoPlays() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -1121,6 +1104,7 @@ class ExplorationActivityTest { @Test fun testAudioWifi_fractionsExp_changeLang_next_langIsHinglish() { + setUpTest() markAllSpotlightsSeen() setUpAudioForFractionLesson() launch( @@ -1183,6 +1167,7 @@ class ExplorationActivityTest { @Test @RunOn(TestPlatform.ESPRESSO) fun testAudioWifi_ratioExp_continueInteraction_audioButton_submitAns_feedbackAudioPlays() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -1228,6 +1213,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_loadExplorationFragment_hasDummyString() { + setUpTest() markAllSpotlightsSeen() launch( createExplorationActivityIntent( @@ -1272,6 +1258,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_onToolbarClosePressed_showsUnsavedExplorationDialog() { + setUpTest() markAllSpotlightsSeen() setUpAudioForFractionLesson() launch( @@ -1301,6 +1288,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_loadingAudio_progressbarIsDisplayed() { + setUpTest() markAllSpotlightsSeen() setUpAudio() launch( @@ -1339,6 +1327,7 @@ class ExplorationActivityTest { // TODO(#89): Check this test case too. It works in pair with below test cases. @Test fun testExpActivity_showUnsavedExpDialog_cancel_dismissesDialog() { + setUpTest() markAllSpotlightsSeen() setUpAudioForFractionLesson() explorationActivityTestRule.launchActivity( @@ -1394,6 +1383,7 @@ class ExplorationActivityTest { @Test fun testExpActivity_showUnsavedExpDialog_cancel_checkOldestProgressIsSaved() { + setUpTest() markAllSpotlightsSeen() explorationCheckpointTestHelper.saveCheckpointForRatiosStory0Exploration0( profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build(), @@ -1430,6 +1420,7 @@ class ExplorationActivityTest { // TODO(#89): Check this test case too. It works in pair with test cases ignored above. @Test fun testExpActivity_showUnsavedExpDialog_leave_checkOldestProgressIsSaved() { + setUpTest() markAllSpotlightsSeen() explorationCheckpointTestHelper.saveCheckpointForRatiosStory0Exploration0( profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build(), @@ -1525,6 +1516,7 @@ class ExplorationActivityTest { // TODO(#89): Check this test case too. It works in pair with test cases ignored above. @Test fun testExpActivity_progressSaved_onBackPress_checkNoProgressDeleted() { + setUpTest() markAllSpotlightsSeen() explorationCheckpointTestHelper.saveCheckpointForRatiosStory0Exploration0( profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build(), @@ -1597,6 +1589,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_databaseFull_onToolbarClosePressed_showsProgressDatabaseFullDialog() { + setUpTest() markAllSpotlightsSeen() explorationCheckpointTestHelper.saveCheckpointForRatiosStory0Exploration0( profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build(), @@ -1634,6 +1627,7 @@ class ExplorationActivityTest { // TODO(#89): Check this test case too. It works in pair with below test cases. @Test fun testExplorationActivity_showProgressDatabaseFullDialog_backToLesson_checkDialogDismisses() { + setUpTest() markAllSpotlightsSeen() explorationCheckpointTestHelper.saveCheckpointForRatiosStory0Exploration0( profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build(), @@ -1996,6 +1990,7 @@ class ExplorationActivityTest { @Test @RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3858): Enable for Espresso. fun testExpActivity_englishContentLang_showHint_explanationInEnglish() { + setUpTest() markAllSpotlightsSeen() updateContentLanguage( ProfileId.newBuilder().apply { internalId = internalProfileId }.build(), @@ -2036,6 +2031,7 @@ class ExplorationActivityTest { @Test @RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3858): Enable for Espresso. fun testExpActivity_showHint_hasCorrectContentDescription() { + setUpTest() markAllSpotlightsSeen() launch( createExplorationActivityIntent( @@ -2081,6 +2077,7 @@ class ExplorationActivityTest { @Test @RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3858): Enable for Espresso. fun testExpActivity_showHint_checkExpandListIconWithScreenReader_isClickable() { + setUpTest() markAllSpotlightsSeen() launch( createExplorationActivityIntent( @@ -2117,6 +2114,7 @@ class ExplorationActivityTest { @Test @RunOn(TestPlatform.ROBOLECTRIC) // TODO(#3858): Enable for Espresso. fun testExpActivity_showHint_checkExpandListIconWithoutScreenReader_isNotClickable() { + setUpTest() markAllSpotlightsSeen() launch( createExplorationActivityIntent( @@ -2154,6 +2152,7 @@ class ExplorationActivityTest { @Test @RunOn(TestPlatform.ROBOLECTRIC, buildEnvironments = [BuildEnvironment.BAZEL]) fun testExpActivity_profileWithArabicContentLang_showHint_explanationInArabic() { + setUpTest() markAllSpotlightsSeen() updateContentLanguage( ProfileId.newBuilder().apply { internalId = internalProfileId }.build(), @@ -2194,6 +2193,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_initialise_openBottomSheet_showsBottomSheet() { + setUpTest() markAllSpotlightsSeen() launch( createExplorationActivityIntent( @@ -2220,6 +2220,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_openBottomsheet_selectHelpInBottomsheet_opensHelpActivity() { + setUpTest() markAllSpotlightsSeen() launch( createExplorationActivityIntent( @@ -2254,6 +2255,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_openBottomsheet_selectOptionsInBottomsheet_opensOptionsActivity() { + setUpTest() markAllSpotlightsSeen() launch( createExplorationActivityIntent( @@ -2288,6 +2290,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_openBottomsheet_selectCloseOption_bottomSheetCloses() { + setUpTest() markAllSpotlightsSeen() launch( createExplorationActivityIntent( @@ -2317,6 +2320,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_closeExploration_surveyGatingCriteriaMet_showsSurveyPopup() { + setUpTestWithSurveyFeatureOn() markAllSpotlightsSeen() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) @@ -2356,6 +2360,7 @@ class ExplorationActivityTest { @Test fun testExplorationActivity_closeExploration_surveyGatingCriteriaNotMet_noSurveyPopup() { + setUpTest() markAllSpotlightsSeen() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) @@ -2388,6 +2393,20 @@ class ExplorationActivityTest { } } + private fun setUpTestWithSurveyFeatureOn() { + TestPlatformParameterModule.forceEnableNpsSurvey(true) + setUpTest() + } + + private fun setUpTest() { + Intents.init() + setUpTestApplicationComponent() + testCoroutineDispatchers.registerIdlingResource() + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) + profileTestHelper.initializeProfiles() + testCoroutineDispatchers.unregisterIdlingResource() + } + private fun markSpotlightSeen(feature: Spotlight.FeatureCase) { val profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() spotlightStateController.markSpotlightViewed(profileId, feature) From 6376d6510a5418ce27121460814845014b963b5a Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:16:54 +0300 Subject: [PATCH 10/17] Add tests for updating underlying gating dataproviders --- .../ExplorationActivityPresenter.kt | 5 +- .../exploration/ExplorationActivityTest.kt | 76 ++++----- .../ExplorationActivityLocalTest.kt | 146 ++++++++++++++++-- .../domain/survey/SurveyGatingController.kt | 4 +- 4 files changed, 169 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt index ee5ce12deec..e75247720b1 100644 --- a/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/exploration/ExplorationActivityPresenter.kt @@ -556,11 +556,12 @@ class ExplorationActivityPresenter @Inject constructor( .add(dialogFragment, TAG_SURVEY_WELCOME_DIALOG) .addToBackStack(null) .commit() + + // Changes to underlying DataProviders will update the gating result. + liveData.removeObserver(this) } else { backPressActivitySelector() } - // Changes to underlying DataProviders will update the gating result. - liveData.removeObserver(this) } } } diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index 075a1dff49b..894c0f2aa11 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -112,6 +112,7 @@ import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterM import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.question.QuestionModule import org.oppia.android.domain.spotlight.SpotlightStateController import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_0 @@ -222,9 +223,10 @@ class ExplorationActivityTest { @Inject lateinit var fakeAccessibilityService: FakeAccessibilityService - private val internalProfileId: Int = 0 + @Inject + lateinit var profileManagementController: ProfileManagementController - private val afternoonUtcTimestampMillis = 1556101812000 + private val internalProfileId: Int = 0 @Before fun setUp() { @@ -2316,11 +2318,13 @@ class ExplorationActivityTest { } @Test - fun testExplorationActivity_closeExploration_surveyGatingCriteriaMet_showsSurveyPopup() { - markAllSpotlightsSeen() - + @Ignore("Test fails to initialize test feature flag before start due to class config.") + fun testExplorationActivity_updateGatingProvider_surveyGatingCriteriaMetEarlier_doesntUpdateUI() { + TestPlatformParameterModule.forceEnableNpsSurvey(true) fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) - fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis) + fakeOppiaClock.setCurrentTimeMs(1556101812000) + + markAllSpotlightsSeen() launch( createExplorationActivityIntent( @@ -2328,7 +2332,7 @@ class ExplorationActivityTest { TEST_TOPIC_ID_0, TEST_STORY_ID_0, TEST_EXPLORATION_ID_2, - shouldSavePartialProgress = false + false ) ).use { explorationDataController.startPlayingNewExploration( @@ -2339,52 +2343,30 @@ class ExplorationActivityTest { ) testCoroutineDispatchers.runCurrent() - fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis + 360_000L) - - onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)).perform(click()) - onView(withText(R.string.stop_exploration_dialog_leave_button)).inRoot(isDialog()) - .perform(click()) - onView(withText(R.string.stop_exploration_dialog_leave_button)).inRoot(isDialog()) - testCoroutineDispatchers.runCurrent() + fakeOppiaClock.setCurrentTimeMs(1556101812000 + 360_000L) - onView(withText(R.string.survey_onboarding_title_text)).inRoot(isDialog()) - .check(matches(isDisplayed())) - onView(withText(R.string.survey_onboarding_message_text)).inRoot(isDialog()) - .check(matches(isDisplayed())) - } - } - - @Test - fun testExplorationActivity_closeExploration_surveyGatingCriteriaNotMet_noSurveyPopup() { - markAllSpotlightsSeen() - - fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) - fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis) - - launch( - createExplorationActivityIntent( - internalProfileId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2, - shouldSavePartialProgress = false - ) - ).use { - explorationDataController.startPlayingNewExploration( - internalProfileId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2 + // Update the SurveyLastShownTimestamp to trigger an update in the data provider and notify + // subscribers of an update. + profileManagementController.updateSurveyLastShownTimestamp( + ProfileId.newBuilder().setInternalId(internalProfileId).build() ) testCoroutineDispatchers.runCurrent() - // Time not advanced to simulate minimum aggregate learning time not achieved. - - onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)).perform(click()) - onView(withText(R.string.stop_exploration_dialog_leave_button)).inRoot(isDialog()) + onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)) .perform(click()) + onView(withText(R.string.stop_exploration_dialog_leave_button)) + .inRoot(isDialog()) + .perform(click()) + onView(withText(R.string.stop_exploration_dialog_leave_button)) + .inRoot(isDialog()) + testCoroutineDispatchers.runCurrent() - onView(withText(R.string.survey_onboarding_title_text)).check(doesNotExist()) + onView(withText(R.string.survey_onboarding_title_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + onView(withText(R.string.survey_onboarding_message_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) } } diff --git a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt index edc6c0a91e3..817d643e8a5 100644 --- a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt @@ -5,14 +5,23 @@ import android.content.Intent import androidx.appcompat.app.AppCompatActivity import androidx.test.core.app.ActivityScenario.launch import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.Component +import javax.inject.Inject +import javax.inject.Singleton import org.junit.After -import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.oppia.android.R import org.oppia.android.app.activity.ActivityComponent import org.oppia.android.app.activity.ActivityComponentFactory import org.oppia.android.app.activity.route.ActivityRouterModule @@ -28,6 +37,7 @@ import org.oppia.android.app.model.EventLog import org.oppia.android.app.model.EventLog.Context.ActivityContextCase.OPEN_EXPLORATION_ACTIVITY import org.oppia.android.app.model.ExplorationActivityParams import org.oppia.android.app.model.ProfileId +import org.oppia.android.app.model.Spotlight import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule import org.oppia.android.app.shim.IntentFactoryShimModule import org.oppia.android.app.shim.ViewBindingShimModule @@ -61,9 +71,10 @@ import org.oppia.android.domain.oppialogger.analytics.ApplicationLifecycleModule import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterModule import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule -import org.oppia.android.domain.platformparameter.PlatformParameterModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.question.QuestionModule +import org.oppia.android.domain.spotlight.SpotlightStateController import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule import org.oppia.android.domain.topic.TEST_EXPLORATION_ID_2 import org.oppia.android.domain.topic.TEST_STORY_ID_0 @@ -73,9 +84,11 @@ import org.oppia.android.testing.FakeAnalyticsEventLogger import org.oppia.android.testing.TestLogReportingModule import org.oppia.android.testing.firebase.TestAuthenticationModule import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +import org.oppia.android.testing.platformparameter.TestPlatformParameterModule import org.oppia.android.testing.robolectric.RobolectricModule import org.oppia.android.testing.threading.TestCoroutineDispatchers import org.oppia.android.testing.threading.TestDispatcherModule +import org.oppia.android.testing.time.FakeOppiaClock import org.oppia.android.testing.time.FakeOppiaClockModule import org.oppia.android.util.accessibility.AccessibilityTestModule import org.oppia.android.util.caching.AssetModule @@ -94,8 +107,6 @@ import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode -import javax.inject.Inject -import javax.inject.Singleton @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) @@ -113,15 +124,16 @@ class ExplorationActivityLocalTest { @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject + lateinit var fakeOppiaClock: FakeOppiaClock + + @Inject + lateinit var spotlightStateController: SpotlightStateController + private lateinit var networkConnectionUtil: NetworkConnectionUtil private lateinit var explorationDataController: ExplorationDataController private val internalProfileId: Int = 0 - - @Before - fun setUp() { - setUpTestApplicationComponent() - testCoroutineDispatchers.registerIdlingResource() - } + private val afternoonUtcTimestampMillis = 1556101812000 @After fun tearDown() { @@ -130,6 +142,7 @@ class ExplorationActivityLocalTest { @Test fun testExploration_onLaunch_logsEvent() { + setUpTestApplicationComponent() getApplicationDependencies( internalProfileId, TEST_TOPIC_ID_0, @@ -155,6 +168,116 @@ class ExplorationActivityLocalTest { } } + @Test + fun testExplorationActivity_closeExploration_surveyGatingCriteriaMet_showsSurveyPopup() { + setUpTestWithNpsEnabled() + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis) + + getApplicationDependencies( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + markAllSpotlightsSeen() + + launch( + createExplorationActivityIntent( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + ).use { + explorationDataController.startPlayingNewExploration( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + testCoroutineDispatchers.runCurrent() + + fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis + 360_000L) + + onView(ViewMatchers.withContentDescription(R.string.nav_app_bar_navigate_up_description)) + .perform(click()) + onView(withText(R.string.stop_exploration_dialog_leave_button)) + .inRoot(isDialog()) + .perform(click()) + onView(withText(R.string.stop_exploration_dialog_leave_button)) + .inRoot(isDialog()) + testCoroutineDispatchers.runCurrent() + + onView(withText(R.string.survey_onboarding_title_text)) + .inRoot(isDialog()) + .check(matches(ViewMatchers.isDisplayed())) + onView(withText(R.string.survey_onboarding_message_text)) + .inRoot(isDialog()) + .check(matches(ViewMatchers.isDisplayed())) + } + } + + @Test + fun testExplorationActivity_closeExploration_surveyGatingCriteriaNotMet_noSurveyPopup() { + setUpTestWithNpsEnabled() + getApplicationDependencies( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + + markAllSpotlightsSeen() + + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis) + + launch( + createExplorationActivityIntent( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + ).use { + explorationDataController.startPlayingNewExploration( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + testCoroutineDispatchers.runCurrent() + + // Time not advanced to simulate minimum aggregate learning time not achieved. + onView(ViewMatchers.withContentDescription(R.string.nav_app_bar_navigate_up_description)) + .perform(click()) + onView(withText(R.string.stop_exploration_dialog_leave_button)) + .inRoot(isDialog()) + .perform(click()) + + onView(withText(R.string.survey_onboarding_title_text)) + .check(ViewAssertions.doesNotExist()) + } + } + + private fun setUpTestWithNpsEnabled() { + TestPlatformParameterModule.forceEnableNpsSurvey(true) + setUpTestApplicationComponent() + } + + private fun markAllSpotlightsSeen() { + markSpotlightSeen(Spotlight.FeatureCase.LESSONS_BACK_BUTTON) + markSpotlightSeen(Spotlight.FeatureCase.VOICEOVER_PLAY_ICON) + markSpotlightSeen(Spotlight.FeatureCase.VOICEOVER_LANGUAGE_ICON) + } + + private fun markSpotlightSeen(feature: Spotlight.FeatureCase) { + val profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() + spotlightStateController.markSpotlightViewed(profileId, feature) + testCoroutineDispatchers.runCurrent() + } + private fun getApplicationDependencies( internalProfileId: Int, topicId: String, @@ -194,6 +317,7 @@ class ExplorationActivityLocalTest { private fun setUpTestApplicationComponent() { ApplicationProvider.getApplicationContext().inject(this) + testCoroutineDispatchers.registerIdlingResource() } // TODO(#59): Figure out a way to reuse modules instead of needing to re-declare them. @@ -201,7 +325,7 @@ class ExplorationActivityLocalTest { @Component( modules = [ TestDispatcherModule::class, ApplicationModule::class, RobolectricModule::class, - PlatformParameterModule::class, PlatformParameterSingletonModule::class, + TestPlatformParameterModule::class, PlatformParameterSingletonModule::class, LoggerModule::class, ContinueModule::class, FractionInputModule::class, ItemSelectionInputModule::class, MultipleChoiceInputModule::class, NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, diff --git a/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt b/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt index d37e3afa32c..466be9ccf97 100644 --- a/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt +++ b/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt @@ -44,8 +44,8 @@ class SurveyGatingController @Inject constructor( fun maybeShowSurvey(profileId: ProfileId, topicId: String): DataProvider { val lastShownDateProvider = retrieveSurveyLastShownDate(profileId) val learningTimeProvider = retrieveAggregateLearningTime(profileId, topicId) - return lastShownDateProvider.combineWith( - learningTimeProvider, GATING_RESULT_PROVIDER_ID + return learningTimeProvider.combineWith( + lastShownDateProvider, GATING_RESULT_PROVIDER_ID ) { lastShownTimestampMs, learningTimeMs -> isSurveyGracePeriodEnded(lastShownTimestampMs) && hasReachedMinimumTopicLearningThreshold(learningTimeMs) && From b8ef774d686fc762089a76949ad57c8e3fb2c397 Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:20:56 +0300 Subject: [PATCH 11/17] Add tests for updating underlying gating dataproviders --- .../app/player/exploration/ExplorationActivityLocalTest.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt index 817d643e8a5..c718341e511 100644 --- a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt @@ -15,8 +15,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import dagger.Component -import javax.inject.Inject -import javax.inject.Singleton import org.junit.After import org.junit.Rule import org.junit.Test @@ -72,7 +70,6 @@ import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterM import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule -import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.question.QuestionModule import org.oppia.android.domain.spotlight.SpotlightStateController import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule @@ -107,6 +104,8 @@ import org.oppia.android.util.parser.image.GlideImageLoaderModule import org.oppia.android.util.parser.image.ImageParsingModule import org.robolectric.annotation.Config import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton @RunWith(AndroidJUnit4::class) @LooperMode(LooperMode.Mode.PAUSED) From 1f4b57ed9078a04d7246e3587f69ffd938bef020 Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Thu, 4 Apr 2024 02:51:18 +0300 Subject: [PATCH 12/17] Fix failing tests --- .../player/state/StateFragmentPresenter.kt | 4 +- .../exploration/ExplorationActivityTest.kt | 53 ------------ .../app/player/state/StateFragmentTest.kt | 81 +++++++++++++++---- .../ExplorationActivityLocalTest.kt | 5 +- .../domain/survey/SurveyGatingController.kt | 4 +- 5 files changed, 73 insertions(+), 74 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index c8450fe3c98..f15709e70b4 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -464,7 +464,7 @@ class StateFragmentPresenter @Inject constructor( // Only check gating result when the previous operation has completed because gating depends on // result of saving the time spent in the exploration, at the end of the exploration. - markStoryCompletedLivedata.observe(activity, { maybeShowSurveyDialog(profileId, topicId) }) + markStoryCompletedLivedata.observe(fragment, { maybeShowSurveyDialog(profileId, topicId) }) } private fun showHintsAndSolutions(helpIndex: HelpIndex, isCurrentStatePendingState: Boolean) { @@ -571,7 +571,7 @@ class StateFragmentPresenter @Inject constructor( .commitNow() // Changes to underlying DataProviders will update the gating result. - liveData.removeObserver(this) + // liveData.removeObserver(this) } else { (activity as StopStatePlayingSessionWithSavedProgressListener) .deleteCurrentProgressAndStopSession(isCompletion = true) diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index 4f49c75baca..45acb596d07 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -2319,59 +2319,6 @@ class ExplorationActivityTest { } } - @Test - @Ignore("Test fails to initialize test feature flag before start due to class config.") - fun testExplorationActivity_updateGatingProvider_surveyGatingCriteriaMetEarlier_doesntUpdateUI() { - TestPlatformParameterModule.forceEnableNpsSurvey(true) - fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) - fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis) - - markAllSpotlightsSeen() - - launch( - createExplorationActivityIntent( - internalProfileId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2, - shouldSavePartialProgress = false - ) - ).use { - explorationDataController.startPlayingNewExploration( - internalProfileId, - TEST_TOPIC_ID_0, - TEST_STORY_ID_0, - TEST_EXPLORATION_ID_2 - ) - testCoroutineDispatchers.runCurrent() - - fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis + 360_000L) - - // Update the SurveyLastShownTimestamp to trigger an update in the data provider and notify - // subscribers of an update. - profileManagementController.updateSurveyLastShownTimestamp( - ProfileId.newBuilder().setInternalId(internalProfileId).build() - ) - testCoroutineDispatchers.runCurrent() - - onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)) - .perform(click()) - onView(withText(R.string.stop_exploration_dialog_leave_button)) - .inRoot(isDialog()) - .perform(click()) - onView(withText(R.string.stop_exploration_dialog_leave_button)) - .inRoot(isDialog()) - testCoroutineDispatchers.runCurrent() - - onView(withText(R.string.survey_onboarding_title_text)) - .inRoot(isDialog()) - .check(matches(isDisplayed())) - onView(withText(R.string.survey_onboarding_message_text)) - .inRoot(isDialog()) - .check(matches(isDisplayed())) - } - } - private fun markSpotlightSeen(feature: Spotlight.FeatureCase) { val profileId = ProfileId.newBuilder().setInternalId(internalProfileId).build() spotlightStateController.markSpotlightViewed(profileId, feature) 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 11c9ccea3cf..49ea8f88f8b 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 @@ -204,21 +204,35 @@ import javax.inject.Singleton // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("FunctionName", "SameParameterValue") class StateFragmentTest { - @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() - @get:Rule val oppiaTestRule = OppiaTestRule() - - @Inject lateinit var profileTestHelper: ProfileTestHelper - @Inject lateinit var context: Context - @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - @Inject lateinit var editTextInputAction: EditTextInputAction - @field:[Inject BackgroundDispatcher] lateinit var backgroundDispatcher: CoroutineDispatcher - @Inject lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper - @Inject lateinit var translationController: TranslationController - @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Inject lateinit var testGlideImageLoader: TestGlideImageLoader - @Inject lateinit var profileManagementController: ProfileManagementController - @Inject lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger - @Inject lateinit var oppiaClock: FakeOppiaClock + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + @get:Rule + val oppiaTestRule = OppiaTestRule() + + @Inject + lateinit var profileTestHelper: ProfileTestHelper + @Inject + lateinit var context: Context + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject + lateinit var editTextInputAction: EditTextInputAction + @field:[Inject BackgroundDispatcher] + lateinit var backgroundDispatcher: CoroutineDispatcher + @Inject + lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper + @Inject + lateinit var translationController: TranslationController + @Inject + lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject + lateinit var testGlideImageLoader: TestGlideImageLoader + @Inject + lateinit var profileManagementController: ProfileManagementController + @Inject + lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger + @Inject + lateinit var oppiaClock: FakeOppiaClock private val profileId = ProfileId.newBuilder().apply { internalId = 1 }.build() @@ -4444,6 +4458,43 @@ class StateFragmentTest { } } + @Test + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. + fun testFinishChapter_updateGatingProvider_surveyGatingCriteriaMetEarlier_doesntUpdateUI() { + setUpTestWithSurveyFeatureOn() + oppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + oppiaClock.setCurrentTimeMs(AFTERNOON_UTC_TIMESTAMP_MILLIS) + + launchForExploration(TEST_EXPLORATION_ID_2, shouldSavePartialProgress = true).use { + startPlayingExploration() + + playThroughPrototypeExploration() + + oppiaClock.setCurrentTimeMs(AFTERNOON_UTC_TIMESTAMP_MILLIS + SESSION_LENGTH_LONG) + + clickReturnToTopicButton() + + onView(withText(R.string.survey_onboarding_title_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + onView(withText(R.string.survey_onboarding_message_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + + // Update the SurveyLastShownTimestamp to trigger an update in the data provider and notify + // subscribers of an update. + profileManagementController.updateSurveyLastShownTimestamp(profileId) + testCoroutineDispatchers.runCurrent() + + onView(withText(R.string.survey_onboarding_title_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + onView(withText(R.string.survey_onboarding_message_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + } + } + private fun addShadowMediaPlayerException(dataSource: Any, exception: Exception) { val classLoader = StateFragmentTest::class.java.classLoader!! val shadowMediaPlayerClass = classLoader.loadClass("org.robolectric.shadows.ShadowMediaPlayer") diff --git a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt index c718341e511..338f6ccee04 100644 --- a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt @@ -11,6 +11,7 @@ import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -210,10 +211,10 @@ class ExplorationActivityLocalTest { onView(withText(R.string.survey_onboarding_title_text)) .inRoot(isDialog()) - .check(matches(ViewMatchers.isDisplayed())) + .check(matches(isDisplayed())) onView(withText(R.string.survey_onboarding_message_text)) .inRoot(isDialog()) - .check(matches(ViewMatchers.isDisplayed())) + .check(matches(isDisplayed())) } } diff --git a/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt b/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt index 466be9ccf97..d37e3afa32c 100644 --- a/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt +++ b/domain/src/main/java/org/oppia/android/domain/survey/SurveyGatingController.kt @@ -44,8 +44,8 @@ class SurveyGatingController @Inject constructor( fun maybeShowSurvey(profileId: ProfileId, topicId: String): DataProvider { val lastShownDateProvider = retrieveSurveyLastShownDate(profileId) val learningTimeProvider = retrieveAggregateLearningTime(profileId, topicId) - return learningTimeProvider.combineWith( - lastShownDateProvider, GATING_RESULT_PROVIDER_ID + return lastShownDateProvider.combineWith( + learningTimeProvider, GATING_RESULT_PROVIDER_ID ) { lastShownTimestampMs, learningTimeMs -> isSurveyGracePeriodEnded(lastShownTimestampMs) && hasReachedMinimumTopicLearningThreshold(learningTimeMs) && From f2914e2d267da9169beb6b66c768d1ec6b2c963e Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Thu, 4 Apr 2024 04:26:23 +0300 Subject: [PATCH 13/17] Add test for data provider update behaviour --- .../ExplorationActivityLocalTest.kt | 72 ++++++++++++++++++- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt index 338f6ccee04..720d559a182 100644 --- a/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt +++ b/app/src/test/java/org/oppia/android/app/player/exploration/ExplorationActivityLocalTest.kt @@ -10,8 +10,8 @@ import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.RootMatchers.isDialog -import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat @@ -71,6 +71,7 @@ import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterM import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule +import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.question.QuestionModule import org.oppia.android.domain.spotlight.SpotlightStateController import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule @@ -130,6 +131,9 @@ class ExplorationActivityLocalTest { @Inject lateinit var spotlightStateController: SpotlightStateController + @Inject + lateinit var profileManagementController: ProfileManagementController + private lateinit var networkConnectionUtil: NetworkConnectionUtil private lateinit var explorationDataController: ExplorationDataController private val internalProfileId: Int = 0 @@ -200,7 +204,7 @@ class ExplorationActivityLocalTest { fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis + 360_000L) - onView(ViewMatchers.withContentDescription(R.string.nav_app_bar_navigate_up_description)) + onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)) .perform(click()) onView(withText(R.string.stop_exploration_dialog_leave_button)) .inRoot(isDialog()) @@ -250,7 +254,7 @@ class ExplorationActivityLocalTest { testCoroutineDispatchers.runCurrent() // Time not advanced to simulate minimum aggregate learning time not achieved. - onView(ViewMatchers.withContentDescription(R.string.nav_app_bar_navigate_up_description)) + onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)) .perform(click()) onView(withText(R.string.stop_exploration_dialog_leave_button)) .inRoot(isDialog()) @@ -261,6 +265,68 @@ class ExplorationActivityLocalTest { } } + @Test + fun testExplorationActivity_updateGatingProvider_surveyGatingCriteriaMet_keepsSurveyDialog() { + setUpTestWithNpsEnabled() + fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_FIXED_FAKE_TIME) + fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis) + + getApplicationDependencies( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + markAllSpotlightsSeen() + + launch( + createExplorationActivityIntent( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + ).use { + explorationDataController.startPlayingNewExploration( + internalProfileId, + TEST_TOPIC_ID_0, + TEST_STORY_ID_0, + TEST_EXPLORATION_ID_2 + ) + testCoroutineDispatchers.runCurrent() + + fakeOppiaClock.setCurrentTimeMs(afternoonUtcTimestampMillis + 360_000L) + + onView(withContentDescription(R.string.nav_app_bar_navigate_up_description)) + .perform(click()) + onView(withText(R.string.stop_exploration_dialog_leave_button)) + .inRoot(isDialog()) + .perform(click()) + + testCoroutineDispatchers.runCurrent() + + onView(withText(R.string.survey_onboarding_title_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + onView(withText(R.string.survey_onboarding_message_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + + // Update the SurveyLastShownTimestamp to trigger an update in the data provider and notify + // subscribers of an update. + profileManagementController.updateSurveyLastShownTimestamp( + ProfileId.newBuilder().setInternalId(internalProfileId).build() + ) + + onView(withText(R.string.survey_onboarding_title_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + onView(withText(R.string.survey_onboarding_message_text)) + .inRoot(isDialog()) + .check(matches(isDisplayed())) + } + } + private fun setUpTestWithNpsEnabled() { TestPlatformParameterModule.forceEnableNpsSurvey(true) setUpTestApplicationComponent() From 0c3ce2fd34bffcfdae93562aab6a671a514db337 Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Fri, 5 Apr 2024 22:20:40 +0300 Subject: [PATCH 14/17] Clean up PR --- .../player/state/StateFragmentPresenter.kt | 61 +++++++++---------- .../app/player/state/StateFragmentTest.kt | 36 ++++------- 2 files changed, 40 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index f15709e70b4..8de5dce9ffc 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -541,42 +541,37 @@ class StateFragmentPresenter @Inject constructor( val liveData = surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData() liveData.observe( activity, - object : Observer> { - override fun onChanged(gatingResult: AsyncResult?) { - when (gatingResult) { - is AsyncResult.Pending -> { - oppiaLogger.d("StateFragment", "A gating decision is pending") - } - is AsyncResult.Failure -> { - oppiaLogger.e( - "StateFragment", - "Failed to retrieve gating decision", - gatingResult.error - ) + { gatingResult -> + when (gatingResult) { + is AsyncResult.Pending -> { + oppiaLogger.d("StateFragment", "A gating decision is pending") + } + is AsyncResult.Failure -> { + oppiaLogger.e( + "StateFragment", + "Failed to retrieve gating decision", + gatingResult.error + ) + (activity as StopStatePlayingSessionWithSavedProgressListener) + .deleteCurrentProgressAndStopSession(isCompletion = true) + } + is AsyncResult.Success -> { + if (gatingResult.value) { + val dialogFragment = + SurveyWelcomeDialogFragment.newInstance( + profileId, + topicId, + explorationId, + SURVEY_QUESTIONS + ) + val transaction = activity.supportFragmentManager.beginTransaction() + transaction + .add(dialogFragment, TAG_SURVEY_WELCOME_DIALOG) + .commitNow() + } else { (activity as StopStatePlayingSessionWithSavedProgressListener) .deleteCurrentProgressAndStopSession(isCompletion = true) } - is AsyncResult.Success -> { - if (gatingResult.value) { - val dialogFragment = - SurveyWelcomeDialogFragment.newInstance( - profileId, - topicId, - explorationId, - SURVEY_QUESTIONS - ) - val transaction = activity.supportFragmentManager.beginTransaction() - transaction - .add(dialogFragment, TAG_SURVEY_WELCOME_DIALOG) - .commitNow() - - // Changes to underlying DataProviders will update the gating result. - // liveData.removeObserver(this) - } else { - (activity as StopStatePlayingSessionWithSavedProgressListener) - .deleteCurrentProgressAndStopSession(isCompletion = true) - } - } } } } 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 49ea8f88f8b..7782d29c532 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 @@ -209,30 +209,18 @@ class StateFragmentTest { @get:Rule val oppiaTestRule = OppiaTestRule() - @Inject - lateinit var profileTestHelper: ProfileTestHelper - @Inject - lateinit var context: Context - @Inject - lateinit var testCoroutineDispatchers: TestCoroutineDispatchers - @Inject - lateinit var editTextInputAction: EditTextInputAction - @field:[Inject BackgroundDispatcher] - lateinit var backgroundDispatcher: CoroutineDispatcher - @Inject - lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper - @Inject - lateinit var translationController: TranslationController - @Inject - lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Inject - lateinit var testGlideImageLoader: TestGlideImageLoader - @Inject - lateinit var profileManagementController: ProfileManagementController - @Inject - lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger - @Inject - lateinit var oppiaClock: FakeOppiaClock + @Inject lateinit var profileTestHelper: ProfileTestHelper + @Inject lateinit var context: Context + @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject lateinit var editTextInputAction: EditTextInputAction + @field:[Inject BackgroundDispatcher] lateinit var backgroundDispatcher: CoroutineDispatcher + @Inject lateinit var explorationCheckpointTestHelper: ExplorationCheckpointTestHelper + @Inject lateinit var translationController: TranslationController + @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject lateinit var testGlideImageLoader: TestGlideImageLoader + @Inject lateinit var profileManagementController: ProfileManagementController + @Inject lateinit var fakeAnalyticsEventLogger: FakeAnalyticsEventLogger + @Inject lateinit var oppiaClock: FakeOppiaClock private val profileId = ProfileId.newBuilder().apply { internalId = 1 }.build() From 26826e2d3479d3ed5fe8d0ef5a31386634f8633e Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Fri, 5 Apr 2024 22:26:22 +0300 Subject: [PATCH 15/17] Remove redundant var --- .../oppia/android/app/player/state/StateFragmentPresenter.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt index 8de5dce9ffc..f1c00a6569a 100755 --- a/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/player/state/StateFragmentPresenter.kt @@ -538,8 +538,7 @@ class StateFragmentPresenter @Inject constructor( } private fun maybeShowSurveyDialog(profileId: ProfileId, topicId: String) { - val liveData = surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData() - liveData.observe( + surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData().observe( activity, { gatingResult -> when (gatingResult) { From 17e5ee40998b7f7f7780f7942c034656b6f39a5b Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:38:47 +0300 Subject: [PATCH 16/17] Revert idling resource changes --- .../app/player/exploration/ExplorationActivityTest.kt | 2 +- .../org/oppia/android/app/player/state/StateFragmentTest.kt | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index 45acb596d07..3cecf389689 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -237,11 +237,11 @@ class ExplorationActivityTest { testCoroutineDispatchers.registerIdlingResource() profileTestHelper.initializeProfiles() fakeOppiaClock.setFakeTimeMode(FakeOppiaClock.FakeTimeMode.MODE_UPTIME_MILLIS) - testCoroutineDispatchers.unregisterIdlingResource() } @After fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() Intents.release() } 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 7782d29c532..8be1ca22cb9 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 @@ -226,6 +226,7 @@ class StateFragmentTest { @After fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() Intents.release() } @@ -5046,9 +5047,7 @@ class StateFragmentTest { Intents.init() setUpTestApplicationComponent() testCoroutineDispatchers.registerIdlingResource() - profileTestHelper.initializeProfiles().also { - testCoroutineDispatchers.unregisterIdlingResource() - } + profileTestHelper.initializeProfiles() // Initialize Glide such that all of its executors use the same shared dispatcher pool as the // rest of Oppia so that thread execution can be synchronized via Oppia's test coroutine From 35e36e93089922814a47ab36f290e68a91e0342d Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Fri, 5 Apr 2024 23:41:36 +0300 Subject: [PATCH 17/17] Remove formatting only changes --- .../app/player/exploration/ExplorationActivityTest.kt | 6 ------ .../org/oppia/android/app/player/state/StateFragmentTest.kt | 6 ++---- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt index 3cecf389689..800916c4391 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/player/exploration/ExplorationActivityTest.kt @@ -112,7 +112,6 @@ import org.oppia.android.domain.oppialogger.analytics.CpuPerformanceSnapshotterM import org.oppia.android.domain.oppialogger.logscheduler.MetricLogSchedulerModule import org.oppia.android.domain.oppialogger.loguploader.LogReportWorkerModule import org.oppia.android.domain.platformparameter.PlatformParameterSingletonModule -import org.oppia.android.domain.profile.ProfileManagementController import org.oppia.android.domain.question.QuestionModule import org.oppia.android.domain.spotlight.SpotlightStateController import org.oppia.android.domain.topic.FRACTIONS_EXPLORATION_ID_0 @@ -223,13 +222,8 @@ class ExplorationActivityTest { @Inject lateinit var fakeAccessibilityService: FakeAccessibilityService - @Inject - lateinit var profileManagementController: ProfileManagementController - private val internalProfileId: Int = 0 - private val afternoonUtcTimestampMillis = 1556101812000 - @Before fun setUp() { Intents.init() 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 8be1ca22cb9..cf9fca12bbb 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 @@ -204,10 +204,8 @@ import javax.inject.Singleton // SameParameterValue: tests should have specific context included/excluded for readability. @Suppress("FunctionName", "SameParameterValue") class StateFragmentTest { - @get:Rule - val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() - @get:Rule - val oppiaTestRule = OppiaTestRule() + @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + @get:Rule val oppiaTestRule = OppiaTestRule() @Inject lateinit var profileTestHelper: ProfileTestHelper @Inject lateinit var context: Context