diff --git a/.bashrc b/.bashrc new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt index f1fb73492b1..1a8f4de364b 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileEditFragmentPresenter.kt @@ -29,7 +29,8 @@ class ProfileEditFragmentPresenter @Inject constructor( private val activity: AppCompatActivity, private val fragment: Fragment, private val oppiaLogger: OppiaLogger, - private val profileManagementController: ProfileManagementController + private val profileManagementController: ProfileManagementController, + private val snackbarManager: SnackbarManager ) { @Inject @@ -151,6 +152,10 @@ class ProfileEditFragmentPresenter @Inject constructor( fragment, Observer { if (it is AsyncResult.Success) { +// snackbarManager.showSnackbar( +// R.string.profile_edit_delete_success, +// SnackbarController.SnackbarDuration.LONG +// ) if (fragment.requireContext().resources.getBoolean(R.bool.isTablet)) { val intent = Intent(fragment.requireContext(), AdministratorControlsActivity::class.java) diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListActivity.kt b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListActivity.kt index 346fa4ebdaf..bf8e8d0fd40 100644 --- a/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListActivity.kt +++ b/app/src/main/java/org/oppia/android/app/settings/profile/ProfileListActivity.kt @@ -15,11 +15,14 @@ class ProfileListActivity : RouteToProfileEditListener { @Inject lateinit var profileListActivityPresenter: ProfileListActivityPresenter + @Inject + lateinit var snackbarManager: SnackbarManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) (activityComponent as ActivityComponentImpl).inject(this) profileListActivityPresenter.handleOnCreate() + snackbarManager.enableShowingSnackbars(this) } override fun onSupportNavigateUp(): Boolean { diff --git a/app/src/main/java/org/oppia/android/app/settings/profile/SnackbarManager.kt b/app/src/main/java/org/oppia/android/app/settings/profile/SnackbarManager.kt new file mode 100644 index 00000000000..c600b80d371 --- /dev/null +++ b/app/src/main/java/org/oppia/android/app/settings/profile/SnackbarManager.kt @@ -0,0 +1,82 @@ +package org.oppia.android.app.settings.profile + +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.snackbar.Snackbar +import com.google.common.util.concurrent.SettableFuture +import kotlinx.coroutines.Deferred +import org.oppia.android.domain.snackbar.SnackbarController +import org.oppia.android.util.data.AsyncResult +import org.oppia.android.util.data.DataProviders.Companion.toLiveData +import javax.inject.Inject + +private const val TAG = "SnackbarManager" +private const val ERROR_MESSAGE = "can't be shown--no activity UI" +private const val GET_CURRENT_SNACKBAR_STATUS_PROVIDER_ID = + "get_current_snackbar_status_provider_id" + +class SnackbarManager @Inject constructor( + private val activity: AppCompatActivity, + private val snackbarController: SnackbarController +) { + private var currentShowingSnackbarId: String? = null + + // Must be called by activities wishing to show snackbars. + fun enableShowingSnackbars(contentView: View) { + snackbarController.getCurrentSnackbarState().toLiveData().observe(activity) { result -> + + when (result) { + is AsyncResult.Success -> when (val request = result.value) { + + is SnackbarController.CurrentSnackbarState.Showing -> { +// if (request.snackbarId != currentShowingSnackbarId){ +// snackbarController.snackbarRequestQueue.peek()?.let { showSnackbar(contentView, it) } +// } + } + + is SnackbarController.CurrentSnackbarState.NotShowing -> {} + + is SnackbarController.CurrentSnackbarState.WaitingToShow -> { + val showSnackbar = showSnackbar(contentView, request.nextRequest) + snackbarController.notifySnackbarShowing( + request.snackbarId, + showSnackbar.first, + showSnackbar.second + ) + } + } + else -> {} + } + // Show a new snackbar if the current state is "showing snackbar" with an ID different than currentShowingSnackbarId. + // Note that this should automatically handle the case of a new activity being opened before a previous snackbar finished (it should be reshown). + // Need to call back into SnackbarController via notifySnackbarShowing() to indicate that it's now showing. + } + } + + private fun showSnackbar( + activityView: View, + showRequest: SnackbarController.ShowSnackbarRequest + ): Pair, Deferred> { + val duration = when (showRequest.duration) { + SnackbarController.SnackbarDuration.SHORT -> Snackbar.LENGTH_SHORT + SnackbarController.SnackbarDuration.LONG -> Snackbar.LENGTH_LONG + } + + val showFuture = SettableFuture.create() + val dismissFuture = SettableFuture.create() + + Snackbar.make(activityView, showRequest.messageStringId, duration) + .addCallback(object : Snackbar.Callback() { + override fun onShown(snackbar: Snackbar) { + showFuture.set(Unit) + } + + override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { + dismissFuture.set(Unit) + } + }).show() + + // See: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-guava/kotlinx.coroutines.guava/as-deferred.html. + return showFuture as Deferred to dismissFuture as Deferred + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9b924f81a97..45ace439ce1 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -617,4 +617,5 @@ Lock Icon Download Status html Content + Profile has been successfully deleted. diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditFragmentTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditFragmentTest.kt index 5c28e75d5d9..c41f049fab6 100644 --- a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditFragmentTest.kt +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/ProfileEditFragmentTest.kt @@ -14,12 +14,14 @@ import androidx.test.espresso.intent.Intents.intended import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import androidx.test.espresso.intent.matcher.IntentMatchers.hasExtra import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.isChecked import androidx.test.espresso.matcher.ViewMatchers.isClickable import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isFocusable import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -121,14 +123,21 @@ import javax.inject.Singleton @Config(application = ProfileEditFragmentTest.TestApplication::class, qualifiers = "port-xxhdpi") class ProfileEditFragmentTest { - @get:Rule val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() - @get:Rule val oppiaTestRule = OppiaTestRule() - - @Inject lateinit var context: Context - @Inject lateinit var profileTestHelper: ProfileTestHelper - @Inject lateinit var profileManagementController: ProfileManagementController - @Inject lateinit var monitorFactory: DataProviderTestMonitor.Factory - @Inject lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + @get:Rule + val oppiaTestRule = OppiaTestRule() + + @Inject + lateinit var context: Context + @Inject + lateinit var profileTestHelper: ProfileTestHelper + @Inject + lateinit var profileManagementController: ProfileManagementController + @Inject + lateinit var monitorFactory: DataProviderTestMonitor.Factory + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers @Before fun setUp() { @@ -158,6 +167,33 @@ class ProfileEditFragmentTest { } } + @Test + fun testProfileEdit_startWithUserProfile_clickProfileDeletionButton_deleteSnackbarIsVisible() { + launchFragmentTestActivity(internalProfileId = 1).use { + onView(withId(R.id.profile_delete_button)).perform(click()) + testCoroutineDispatchers.runCurrent() + onView(withText(R.string.profile_edit_delete_dialog_positive)) + .inRoot(isDialog()) + .perform(click()) + testCoroutineDispatchers.runCurrent() + onView(withText(R.string.profile_edit_delete_success)) + .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + } + } + + @Test + fun testProfileEdit_startWithUserProfile_AfterDeleteSnackbar_profileActivityIsShownAgain() { + launchFragmentTestActivity(internalProfileId = 1).use { + onView(withId(R.id.profile_delete_button)).perform(click()) + testCoroutineDispatchers.runCurrent() + onView(withText(R.string.profile_edit_delete_dialog_positive)) + .inRoot(isDialog()) + .perform(click()) + testCoroutineDispatchers.runCurrent() + intended(hasComponent(ProfileListActivity::class.java.name)) + } + } + @Test @Config(qualifiers = "land") fun testProfileEdit_configChange_startWithUserProfile_clickDelete_checkOpensDeletionDialog() { diff --git a/app/src/sharedTest/java/org/oppia/android/app/settings/profile/SnackbarManagerTest.kt b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/SnackbarManagerTest.kt new file mode 100644 index 00000000000..39870d7395a --- /dev/null +++ b/app/src/sharedTest/java/org/oppia/android/app/settings/profile/SnackbarManagerTest.kt @@ -0,0 +1,216 @@ +package org.oppia.android.app.settings.profile + +import android.app.Application +import android.content.Context +import androidx.appcompat.app.AppCompatActivity +import androidx.test.core.app.ActivityScenario +import androidx.test.core.app.ApplicationProvider +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.intent.Intents +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 +import dagger.Component +import org.hamcrest.Matchers.allOf +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 +import org.oppia.android.app.application.ApplicationComponent +import org.oppia.android.app.application.ApplicationInjector +import org.oppia.android.app.application.ApplicationInjectorProvider +import org.oppia.android.app.application.ApplicationModule +import org.oppia.android.app.application.ApplicationStartupListenerModule +import org.oppia.android.app.application.testing.TestingBuildFlavorModule +import org.oppia.android.app.devoptions.DeveloperOptionsModule +import org.oppia.android.app.devoptions.DeveloperOptionsStarterModule +import org.oppia.android.app.player.state.itemviewmodel.SplitScreenInteractionModule +import org.oppia.android.app.shim.ViewBindingShimModule +import org.oppia.android.app.translation.testing.ActivityRecreatorTestModule +import org.oppia.android.data.backends.gae.NetworkConfigProdModule +import org.oppia.android.data.backends.gae.NetworkModule +import org.oppia.android.domain.classify.InteractionsModule +import org.oppia.android.domain.classify.rules.algebraicexpressioninput.AlgebraicExpressionInputModule +import org.oppia.android.domain.classify.rules.continueinteraction.ContinueModule +import org.oppia.android.domain.classify.rules.dragAndDropSortInput.DragDropSortInputModule +import org.oppia.android.domain.classify.rules.fractioninput.FractionInputModule +import org.oppia.android.domain.classify.rules.imageClickInput.ImageClickInputModule +import org.oppia.android.domain.classify.rules.itemselectioninput.ItemSelectionInputModule +import org.oppia.android.domain.classify.rules.mathequationinput.MathEquationInputModule +import org.oppia.android.domain.classify.rules.multiplechoiceinput.MultipleChoiceInputModule +import org.oppia.android.domain.classify.rules.numberwithunits.NumberWithUnitsRuleModule +import org.oppia.android.domain.classify.rules.numericexpressioninput.NumericExpressionInputModule +import org.oppia.android.domain.classify.rules.numericinput.NumericInputRuleModule +import org.oppia.android.domain.classify.rules.ratioinput.RatioInputModule +import org.oppia.android.domain.classify.rules.textinput.TextInputRuleModule +import org.oppia.android.domain.exploration.ExplorationProgressModule +import org.oppia.android.domain.exploration.ExplorationStorageModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionConfigModule +import org.oppia.android.domain.hintsandsolution.HintsAndSolutionProdModule +import org.oppia.android.domain.onboarding.ExpirationMetaDataRetrieverModule +import org.oppia.android.domain.oppialogger.LogStorageModule +import org.oppia.android.domain.oppialogger.LoggingIdentifierModule +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.question.QuestionModule +import org.oppia.android.domain.snackbar.SnackbarController +import org.oppia.android.domain.topic.PrimeTopicAssetsControllerModule +import org.oppia.android.domain.workmanager.WorkManagerConfigurationModule +import org.oppia.android.testing.OppiaTestRule +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.junit.InitializeDefaultLocaleRule +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.FakeOppiaClockModule +import org.oppia.android.util.accessibility.AccessibilityTestModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.caching.testing.CachingTestModule +import org.oppia.android.util.gcsresource.GcsResourceModule +import org.oppia.android.util.locale.LocaleProdModule +import org.oppia.android.util.logging.EventLoggingConfigurationModule +import org.oppia.android.util.logging.LoggerModule +import org.oppia.android.util.logging.SyncStatusModule +import org.oppia.android.util.logging.firebase.FirebaseLogUploaderModule +import org.oppia.android.util.networking.NetworkConnectionDebugUtilModule +import org.oppia.android.util.networking.NetworkConnectionUtilDebugModule +import org.oppia.android.util.parser.html.HtmlParserEntityTypeModule +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 + +/** Test for [SnackbarManager]. */ +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = SnackbarManagerTest.TestApplication::class, qualifiers = "port-xxhdpi") +class SnackbarManagerTest { + + @get:Rule + val initializeDefaultLocaleRule = InitializeDefaultLocaleRule() + + @get:Rule + val oppiaTestRule = OppiaTestRule() + + @Inject + lateinit var context: Context + + @Inject + lateinit var snackbarManager: SnackbarManager + + @Inject + lateinit var snackbarController: SnackbarController + + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + @Before + fun setUp() { + Intents.init() + setUpTestApplicationComponent() + testCoroutineDispatchers.registerIdlingResource() + } + + @After + fun tearDown() { + testCoroutineDispatchers.unregisterIdlingResource() + Intents.release() + } + + @Test + fun testShowSnackbarMethod_enqueuesRequestInTheRequestQueue_returns_SameMessage() { + snackbarManager.showSnackbar( + R.string.profile_edit_delete_success, + SnackbarController.SnackbarDuration.SHORT + ) + val element = snackbarController.snackbarRequestQueue.peek() + assertThat(element?.messageStringId).isEqualTo(R.string.profile_edit_delete_success) + } + + @Test + fun testShowSnackbarMethod_showsSnackbarInTheActivity_IfEnableSnackbarsIsCalled() { + snackbarManager.showSnackbar( + R.string.profile_edit_delete_success, + SnackbarController.SnackbarDuration.SHORT + ) + ActivityScenario.launch(ProfileListActivity::class.java).onActivity { + snackbarManager.enableShowingSnackbars(it) + } + testCoroutineDispatchers.runCurrent() + onView(allOf(withText(R.string.profile_edit_delete_success))) + .check(matches(isDisplayed())) + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + @Singleton + @Component( + modules = [ + RobolectricModule::class, + PlatformParameterModule::class, PlatformParameterSingletonModule::class, + TestDispatcherModule::class, ApplicationModule::class, + LoggerModule::class, ContinueModule::class, FractionInputModule::class, + ItemSelectionInputModule::class, MultipleChoiceInputModule::class, + NumberWithUnitsRuleModule::class, NumericInputRuleModule::class, TextInputRuleModule::class, + DragDropSortInputModule::class, ImageClickInputModule::class, InteractionsModule::class, + GcsResourceModule::class, GlideImageLoaderModule::class, ImageParsingModule::class, + HtmlParserEntityTypeModule::class, QuestionModule::class, TestLogReportingModule::class, + AccessibilityTestModule::class, LogStorageModule::class, CachingTestModule::class, + PrimeTopicAssetsControllerModule::class, ExpirationMetaDataRetrieverModule::class, + ViewBindingShimModule::class, RatioInputModule::class, WorkManagerConfigurationModule::class, + ApplicationStartupListenerModule::class, LogReportWorkerModule::class, + HintsAndSolutionConfigModule::class, HintsAndSolutionProdModule::class, + FirebaseLogUploaderModule::class, FakeOppiaClockModule::class, + DeveloperOptionsStarterModule::class, DeveloperOptionsModule::class, + ExplorationStorageModule::class, NetworkModule::class, NetworkConfigProdModule::class, + NetworkConnectionUtilDebugModule::class, NetworkConnectionDebugUtilModule::class, + AssetModule::class, LocaleProdModule::class, ActivityRecreatorTestModule::class, + NumericExpressionInputModule::class, AlgebraicExpressionInputModule::class, + MathEquationInputModule::class, SplitScreenInteractionModule::class, + LoggingIdentifierModule::class, ApplicationLifecycleModule::class, + SyncStatusModule::class, MetricLogSchedulerModule::class, TestingBuildFlavorModule::class, + EventLoggingConfigurationModule::class, ActivityRouterModule::class, + CpuPerformanceSnapshotterModule::class, ExplorationProgressModule::class + ] + ) + + interface TestApplicationComponent : ApplicationComponent { + @Component.Builder + interface Builder : ApplicationComponent.Builder + + fun inject(snackbarManagerTest: SnackbarManagerTest) + } + + class TestApplication : Application(), ActivityComponentFactory, ApplicationInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerSnackbarManagerTest_TestApplicationComponent.builder() + .setApplication(this) + .build() as TestApplicationComponent + } + + fun inject(snackbarManagerTest: SnackbarManagerTest) { + component.inject(snackbarManagerTest) + } + + override fun createActivityComponent(activity: AppCompatActivity): ActivityComponent { + return component.getActivityComponentBuilderProvider().get().setActivity(activity).build() + } + + override fun getApplicationInjector(): ApplicationInjector = component + } +} diff --git a/domain/src/main/java/org/oppia/android/domain/snackbar/SnackbarController.kt b/domain/src/main/java/org/oppia/android/domain/snackbar/SnackbarController.kt new file mode 100644 index 00000000000..0f8edca40f8 --- /dev/null +++ b/domain/src/main/java/org/oppia/android/domain/snackbar/SnackbarController.kt @@ -0,0 +1,86 @@ +package org.oppia.android.domain.snackbar + +import androidx.annotation.StringRes +import com.google.common.util.concurrent.SettableFuture +import kotlinx.coroutines.Deferred +import org.oppia.android.util.data.AsyncDataSubscriptionManager +import org.oppia.android.util.data.DataProvider +import org.oppia.android.util.data.DataProviders +import java.util.LinkedList +import java.util.Queue +import javax.inject.Inject +import javax.inject.Singleton + +private const val GET_CURRENT_SNACKBAR_REQUEST_PROVIDER_ID = + "get_current_snackbar_request_provider_id" + +/** Controller for enqueueing, dismissing, and retrieving snackbars. */ +@Singleton +class SnackbarController @Inject constructor( + private val dataProviders: DataProviders, + private val asyncDataSubscriptionManager: AsyncDataSubscriptionManager, +) { + + val dismissFuture = SettableFuture.create() + private val _snackbarRequestQueue: Queue = LinkedList() + + /** Queue of the snackbar requests that are to be shown based on FIFO. */ + val snackbarRequestQueue: Queue + get() = _snackbarRequestQueue + + val currentState = CurrentSnackbarState.NotShowing + + fun getCurrentSnackbarState(): DataProvider { + + val currentRequest = _snackbarRequestQueue.peek() + + if (_snackbarRequestQueue.isEmpty()) { + return dataProviders.createInMemoryDataProvider(CurrentSnackbarState.NotShowing) { + return@createInMemoryDataProvider CurrentSnackbarState.NotShowing + } + } + } + + fun enqueueSnackbar(request: ShowSnackbarRequest) { + _snackbarRequestQueue.add(request) + notifyPotentialSnackbarChange() + } + + fun notifySnackbarShowing(snackbarId: Int, onShow: Deferred, onDismiss: Deferred) { + // onDismiss is resolved when the snackbar by unique ID snackbarId is no longer showing. + CurrentSnackbarState.NotShowing + +// val showFuture = onShow.await() + } + + private fun notifyPotentialSnackbarChange() { + asyncDataSubscriptionManager.notifyChangeAsync(GET_CURRENT_SNACKBAR_REQUEST_PROVIDER_ID) + } + + sealed class CurrentSnackbarState { + object NotShowing : CurrentSnackbarState() + + data class Showing(val request: ShowSnackbarRequest, val snackbarId: Int) : + CurrentSnackbarState() + + data class WaitingToShow(val nextRequest: ShowSnackbarRequest, val snackbarId: Int) : + CurrentSnackbarState() + } + + data class ShowSnackbarRequest( + @StringRes val messageStringId: Int, + val duration: SnackbarDuration + ) + + private data class Snackbar(val request: ShowSnackbarRequest, val snackbarId: Int) + + /** These are for the length of the snackbar that is to be shown. */ + enum class SnackbarDuration { + + /** Indicates the short duration of the snackbar. */ + SHORT, + + /** Indicates the long duration of the snackbar. */ + LONG + } +} diff --git a/domain/src/test/java/org/oppia/android/domain/snackbar/SnackbarControllerTest.kt b/domain/src/test/java/org/oppia/android/domain/snackbar/SnackbarControllerTest.kt new file mode 100644 index 00000000000..bf9733320af --- /dev/null +++ b/domain/src/test/java/org/oppia/android/domain/snackbar/SnackbarControllerTest.kt @@ -0,0 +1,161 @@ +package org.oppia.android.domain.snackbar + +import android.app.Application +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import dagger.BindsInstance +import dagger.Component +import dagger.Module +import dagger.Provides +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.oppia.android.testing.TestLogReportingModule +import org.oppia.android.testing.data.DataProviderTestMonitor +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.FakeOppiaClockModule +import org.oppia.android.util.caching.AssetModule +import org.oppia.android.util.data.DataProvidersInjector +import org.oppia.android.util.data.DataProvidersInjectorProvider +import org.oppia.android.util.locale.LocaleProdModule +import org.robolectric.annotation.Config +import org.robolectric.annotation.LooperMode +import javax.inject.Inject +import javax.inject.Singleton + +private const val STRING_ID: Int = 1 +private const val STRING_ID_2: Int = 2 + +@Suppress("FunctionName", "SameParameterValue") +@RunWith(AndroidJUnit4::class) +@LooperMode(LooperMode.Mode.PAUSED) +@Config(application = SnackbarControllerTest.TestApplication::class) +class SnackbarControllerTest { + + @Inject + lateinit var snackbarController: SnackbarController + + @Inject + lateinit var dataProviderTestMonitor: DataProviderTestMonitor.Factory + + @Inject + lateinit var testCoroutineDispatchers: TestCoroutineDispatchers + + var request = SnackbarController.SnackbarRequest.ShowSnackbar( + messageStringId = STRING_ID, + duration = SnackbarController.SnackbarDuration.SHORT + ) + + var request2 = SnackbarController.SnackbarRequest.ShowSnackbar( + messageStringId = STRING_ID_2, + duration = SnackbarController.SnackbarDuration.SHORT + ) + + @Before + fun setUp() { + setUpTestApplicationComponent() + } + + @Test + fun testEnqueueSnackbarRequest_returns_requestInTheQueue() { + snackbarController.enqueueSnackbar(request) + assertThat(snackbarController.snackbarRequestQueue).contains(request) + } + + @Test + fun testEnqueueAndDismissSnackbarRequest_returns_requestNotInTheQueue() { + snackbarController.enqueueSnackbar(request) + snackbarController.dismissCurrentSnackbar() + assertThat(snackbarController.snackbarRequestQueue).doesNotContain(request) + } + + @Test + fun testEnqueueTwoRequests_returns_FirstRequestInTheQueue() { + snackbarController.enqueueSnackbar(request) + snackbarController.enqueueSnackbar(request2) + assertThat(snackbarController.snackbarRequestQueue.peek()).isEqualTo(request) + } + + @Test + fun testEnqueueRequest_returns_getCurrentSnackbar_success() { + snackbarController.enqueueSnackbar(request) + val monitor = dataProviderTestMonitor.createMonitor(snackbarController.getCurrentSnackbar()) + testCoroutineDispatchers.runCurrent() + monitor.ensureNextResultIsSuccess() + } + + @Test + fun testEnqueueTwoRequest_returns_firstRequest() { + snackbarController.enqueueSnackbar(request) + snackbarController.enqueueSnackbar(request2) + val result = + dataProviderTestMonitor.waitForNextSuccessfulResult(snackbarController.getCurrentSnackbar()) + testCoroutineDispatchers.runCurrent() + assertThat(result).isEqualTo(request) + } + + @Test + fun testEnqueueTwoRequest_verifyingFirst_thenDismissing_returns_secondRequest() { + snackbarController.enqueueSnackbar(request) + snackbarController.enqueueSnackbar(request2) + val result = + dataProviderTestMonitor.waitForNextSuccessfulResult(snackbarController.getCurrentSnackbar()) + testCoroutineDispatchers.runCurrent() + assertThat(result).isEqualTo(request) + snackbarController.dismissCurrentSnackbar() + val result2 = + dataProviderTestMonitor.waitForNextSuccessfulResult(snackbarController.getCurrentSnackbar()) + testCoroutineDispatchers.runCurrent() + assertThat(result2).isEqualTo(request2) + } + + private fun setUpTestApplicationComponent() { + ApplicationProvider.getApplicationContext().inject(this) + } + + @Module + class TestModule { + @Provides + @Singleton + fun provideContext(application: Application): Context { + return application + } + } + + @Singleton + @Component( + modules = [ + TestModule::class, AssetModule::class, LocaleProdModule::class, FakeOppiaClockModule::class, + TestLogReportingModule::class, TestDispatcherModule::class, RobolectricModule::class, + ] + ) + interface TestApplicationComponent : DataProvidersInjector { + @Component.Builder + interface Builder { + @BindsInstance + fun setApplication(application: Application): Builder + + fun build(): TestApplicationComponent + } + + fun inject(snackbarControllerTest: SnackbarControllerTest) + } + + class TestApplication : Application(), DataProvidersInjectorProvider { + private val component: TestApplicationComponent by lazy { + DaggerSnackbarControllerTest_TestApplicationComponent.builder() + .setApplication(this) + .build() + } + + fun inject(snackbarControllerTest: SnackbarControllerTest) { + component.inject(snackbarControllerTest) + } + + override fun getDataProvidersInjector(): DataProvidersInjector = component + } +}