From 9e8553cdcebb5f40b170d33b2e02bbe02081530b Mon Sep 17 00:00:00 2001 From: Adhiambo Peres <59600948+adhiamboperes@users.noreply.github.com> Date: Sat, 6 Apr 2024 04:22:09 +0300 Subject: [PATCH] Fix NPS Survey Gating (#5356) ## Explanation I found out during testing that there is an underlying gating DataProvider update behavior that occurs because of the way I initially designed the survey popup behaviour: - when the survey popup is displayed, we update the user profile with the current survey shown timestamp, to be used as gating so that the survey isn't triggered again before the grace period ends. - Updating the profile notifies upstream observers, including the gating DataProvider. - Due to this update, the gating provider is triggered to recompute. - This time, the gating result will be false for triggering the survey, since the grace period condition has changed. - The UI behavior is to navigate away from the exploration screen if the survey isn't to be shown. The activity is destroyed, and the popup along with it. I mitigated this by removing the observer once the initial gating result has been received and processed. ## Tests I added tests that test survey gating based on changes in the time criterion, while other gating criteria remains constant/fulfilled. Exhaustive gating tests were added in the SurveyGatingController. I also made a change in the `StateFragmentTest` that unregisters the idling resource after initializing profiles instead of at the end of the test runs. Previously, `StateFragmentTest`would not run locally and fail with the error: ``` Error performing 'single click - At Coordinates: 173, 329 and precision: 16, 16' on view 'with id: org.oppia.android:id/play_test_exploration_button'. at androidx.test.espresso.PerformException$Builder.build(PerformException.java:86) at androidx.test.espresso.base.DefaultFailureHandler.getUserFriendlyError(DefaultFailureHandler.java:87) at androidx.test.espresso.base.DefaultFailureHandler.handle(DefaultFailureHandler.java:59) at androidx.test.espresso.ViewInteraction.waitForAndHandleInteractionResults(ViewInteraction.java:322) at androidx.test.espresso.ViewInteraction.desugaredPerform(ViewInteraction.java:178) at androidx.test.espresso.ViewInteraction.perform(ViewInteraction.java:119) at org.oppia.android.app.player.state.StateFragmentTest.startPlayingExploration(StateFragmentTest.kt:475)org.oppia.android.app.player.state.StateFragmentTest.testFinishChapter_lateNight_isPastGracePeriod_minimumAggregateTimeMet_noSurveyPopup(StateFragmentTest.kt:264) Caused by: androidx.test.espresso.IdlingResourceTimeoutException: Wait for [TestCoroutineDispatcherIdlingResource] to become idle timed out at androidx.test.espresso.IdlingPolicy.handleTimeout(IdlingPolicy.java:61) at androidx.test.espresso.base.UiControllerImpl$5.resourcesHaveTimedOut(UiControllerImpl.java:434) at androidx.test.espresso.base.IdlingResourceRegistry$Dispatcher.handleTimeout(IdlingResourceRegistry.java:481) ``` This was caused by registering an idling resource before test begins, then when the activity starts, the main thread becomes busy. Generally, the test will continue when the main thread becomes idle, but the idling resource now blocks it and does not call onTransitionToIdle(). So it will always time out. Unregistering the idling resource before the activity starts frees up the main thread. Disabled running on Robolectric due to [known issue](https://github.com/oppia/oppia-android/issues/1612) with Drag and Drop interaction. Espresso Run Screenshot ![Screenshot 2024-03-13 at 19 35 12](https://github.com/oppia/oppia-android/assets/59600948/72d8aee9-f39d-4d26-92a9-72fb0919d46c) ## Essential Checklist - [ ] The PR title and explanation each start with "Fix #bugnum: " (If this PR fixes part of an issue, prefix the title with "Fix part of #bugnum: ...".) - [x] Any changes to [scripts/assets](https://github.com/oppia/oppia-android/tree/develop/scripts/assets) files have their rationale included in the PR explanation. - [x] The PR follows the [style guide](https://github.com/oppia/oppia-android/wiki/Coding-style-guide). - [x] The PR does not contain any unnecessary code changes from Android Studio ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#undo-unnecessary-changes)). - [x] The PR is made from a branch that's **not** called "develop" and is up-to-date with "develop". - [x] The PR is **assigned** to the appropriate reviewers ([reference](https://github.com/oppia/oppia-android/wiki/Guidance-on-submitting-a-PR#clarification-regarding-assignees-and-reviewers-section)). ## For UI-specific PRs only If your PR includes UI-related changes, then: - Add screenshots for portrait/landscape for both a tablet & phone of the before & after UI changes - For the screenshots above, include both English and pseudo-localized (RTL) screenshots (see [RTL guide](https://github.com/oppia/oppia-android/wiki/RTL-Guidelines)) - Add a video showing the full UX flow with a screen reader enabled (see [accessibility guide](https://github.com/oppia/oppia-android/wiki/Accessibility-A11y-Guide)) - For PRs introducing new UI elements or color changes, both light and dark mode screenshots must be included - Add a screenshot demonstrating that you ran affected Espresso tests locally & that they're passing --- .../ExplorationActivityPresenter.kt | 75 ++--- .../player/state/StateFragmentPresenter.kt | 74 ++--- .../StateFragmentTestActivityPresenter.kt | 3 +- .../SurveyWelcomeDialogFragmentPresenter.kt | 1 + .../app/player/state/StateFragmentTest.kt | 288 ++++++++++++++++++ .../ExplorationActivityLocalTest.kt | 208 ++++++++++++- .../android/domain/survey/SurveyController.kt | 9 +- .../domain/survey/SurveyGatingController.kt | 2 +- .../domain/survey/SurveyProgressController.kt | 4 +- .../survey/SurveyGatingControllerTest.kt | 140 ++++++++- 10 files changed, 709 insertions(+), 95 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..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 @@ -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 @@ -297,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) } } } @@ -528,42 +525,48 @@ 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() + + // Changes to underlying DataProviders will update the gating result. + liveData.removeObserver(this) + } else { + backPressActivitySelector() + } + } } } } + ) } 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..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 @@ -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(fragment, { maybeShowSurveyDialog(profileId, topicId) }) } private fun showHintsAndSolutions(helpIndex: HelpIndex, isCurrentStatePendingState: Boolean) { @@ -535,44 +538,43 @@ class StateFragmentPresenter @Inject constructor( } private fun maybeShowSurveyDialog(profileId: ProfileId, topicId: String) { - surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData() - .observe( - activity, - { 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 - ) + surveyGatingController.maybeShowSurvey(profileId, topicId).toLiveData().observe( + activity, + { 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() - } else { - (activity as StopStatePlayingSessionWithSavedProgressListener) - .deleteCurrentProgressAndStopSession(isCompletion = true) - } - } } } - ) + } + ) } /** 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..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,7 +3,6 @@ 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 @@ -98,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 -> 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 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 a05e4756684..1c13ec60360 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,6 +218,7 @@ 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() @@ -4317,6 +4320,258 @@ 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) + 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 + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. + 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 + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. + 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 + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. + 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 + @RunOn(TestPlatform.ESPRESSO) // TODO(#1612): Enable for Robolectric. + 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 + @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() { + // 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 + ) + ) + ) + } + } + + @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") @@ -4866,6 +5121,16 @@ class StateFragmentTest { setUpTest() } + private fun setUpTestWithSurveyFeatureOn() { + TestPlatformParameterModule.forceEnableNpsSurvey(true) + setUpTest() + } + + private fun setUpTestWithSurveyFeatureOff() { + TestPlatformParameterModule.forceEnableNpsSurvey(false) + setUpTest() + } + private fun setUpTest() { Intents.init() setUpTestApplicationComponent() @@ -5111,4 +5376,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 + } } 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..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 @@ -5,14 +5,22 @@ 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.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 import dagger.Component 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 +36,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 +70,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 +83,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 @@ -113,15 +125,19 @@ class ExplorationActivityLocalTest { @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @Inject + lateinit var fakeOppiaClock: FakeOppiaClock + + @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 - - @Before - fun setUp() { - setUpTestApplicationComponent() - testCoroutineDispatchers.registerIdlingResource() - } + private val afternoonUtcTimestampMillis = 1556101812000 @After fun tearDown() { @@ -130,6 +146,7 @@ class ExplorationActivityLocalTest { @Test fun testExploration_onLaunch_logsEvent() { + setUpTestApplicationComponent() getApplicationDependencies( internalProfileId, TEST_TOPIC_ID_0, @@ -155,6 +172,178 @@ 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(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() { + 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(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()) + } + } + + @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() + } + + 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 +383,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 +391,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/SurveyController.kt b/domain/src/main/java/org/oppia/android/domain/survey/SurveyController.kt index 13ee41e96c9..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 @@ -8,7 +8,6 @@ import org.oppia.android.domain.oppialogger.exceptions.ExceptionsController 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 @@ -54,13 +53,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/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) = 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 ) } 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)