tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 8d833c58106d6276d14450bbf83acb1e49bc22cf
parent 63b0f9430fae51732f8c0e10ba4d43cde098c9a1
Author: mcarare <48995920+mcarare@users.noreply.github.com>
Date:   Tue,  2 Dec 2025 14:39:29 +0000

Bug 1991657 - Inject dispatcher into AbstractBinding r=android-reviewers,jonalmeida

- Add a `flowScoped` extension to `Store` which accepts a `CoroutineDispatcher` and handles the creation of a `CoroutineScope` with a `SupervisorJob`.
- Inject a `CoroutineDispatcher` into `AbstractBinding` and all its subclasses.
- Default the dispatcher to `Dispatchers.Main` in production code.
- Update tests to provide a `StandardTestDispatcher` and replace `MainCoroutineRule` and `runTestOnMain` with `runTest` and `advanceUntilIdle()`.

Differential Revision: https://phabricator.services.mozilla.com/D267383

Diffstat:
Mmobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/HomepageThumbnails.kt | 5++++-
Mmobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/HomepageThumbnailsTest.kt | 62+++++++++++++++++++++++++++++++++++++++++---------------------
Mmobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt | 26++++++++++++++++++++++++++
Mmobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt | 5++++-
Mmobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt | 31++++++++++++++++++-------------
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/AboutHomeBinding.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/OpenInFirefoxBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/ReaderViewBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/bindings/ExternalAppLinkStatusBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/bindings/FindInPageBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/CustomTabColorsBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/StandardSnackbarErrorBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TranslationsBannerIntegration.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TranslationsBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/WebExtensionsMenuBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorBinding.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorMenuBinding.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/state/bindings/MenuBinding.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/state/bindings/PendingDeletionBinding.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/reviewprompt/ShowPlayStoreReviewPrompt.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/snackbar/SnackbarBinding.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/InactiveTabsBinding.kt | 7++++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SecureTabsTrayBinding.kt | 19++++++++++++++++---
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBinding.kt | 19++++++++++++++++---
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt | 5++++-
Mmobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt | 5++++-
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/AboutHomeBindingTest.kt | 31+++++++++++++++++++++----------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/OpenInFirefoxBindingTest.kt | 16++++++++++------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/ReaderViewBindingTest.kt | 23+++++++++++++++--------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/bindings/ExternalAppLinkStatusBindingTest.kt | 21+++++++++++++--------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/bindings/FindInPageBindingTest.kt | 15+++++++++------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/bindings/InactiveTabsBindingTest.kt | 16+++++++++-------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/browser/CustomTabColorsBindingTest.kt | 17+++++++++--------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/browser/StandardSnackbarErrorBindingTest.kt | 19+++++++++++++------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/browser/TranslationsBindingTest.kt | 650+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/menu/WebExtensionsMenuBindingTest.kt | 42++++++++++++++++++++++++++----------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/crashes/CrashReporterBindingTest.kt | 19++++++++++++-------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/RecentTabsListFeatureTest.kt | 609++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/library/history/state/bindings/MenuBindingTest.kt | 13+++++++------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/reviewprompt/ShowPlayStoreReviewPromptTest.kt | 34+++++++++++++++++++++-------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/snackbar/SnackbarBindingTest.kt | 97++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/SecureTabsTrayBindingTest.kt | 90+++++++++++++++++++++++++++++++------------------------------------------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBindingTest.kt | 102++++++++++++++++++++++++++++++++++++-------------------------------------------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBindingTest.kt | 21+++++++++++++--------
Mmobile/android/fenix/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogBindingTest.kt | 48+++++++++++++++++++++++++++++++++---------------
47 files changed, 1250 insertions(+), 927 deletions(-)

diff --git a/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/HomepageThumbnails.kt b/mobile/android/android-components/components/browser/thumbnails/src/main/java/mozilla/components/browser/thumbnails/HomepageThumbnails.kt @@ -7,6 +7,8 @@ package mozilla.components.browser.thumbnails import android.content.Context import android.graphics.Bitmap import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -33,8 +35,9 @@ class HomepageThumbnails( private val context: Context, private val store: BrowserStore, private val homepageUrl: String, + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, private val homepageRequest: ((RequestHomepageScreenshot) -> Unit)? = null, -) : AbstractBinding<BrowserState>(store) { +) : AbstractBinding<BrowserState>(store, mainDispatcher) { override suspend fun onState(flow: Flow<BrowserState>) { flow.map { it.selectedTab } diff --git a/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/HomepageThumbnailsTest.kt b/mobile/android/android-components/components/browser/thumbnails/src/test/java/mozilla/components/browser/thumbnails/HomepageThumbnailsTest.kt @@ -6,6 +6,8 @@ package mozilla.components.browser.thumbnails import android.graphics.Bitmap import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.TabListAction @@ -16,18 +18,14 @@ import mozilla.components.lib.state.Middleware import mozilla.components.support.test.middleware.CaptureActionsMiddleware import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext -import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class HomepageThumbnailsTest { - - @get:Rule - val coroutinesTestRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() private lateinit var store: BrowserStore private lateinit var thumbnails: HomepageThumbnails @@ -51,18 +49,20 @@ class HomepageThumbnailsTest { middlewares, ) bitmap = mock() - thumbnails = HomepageThumbnails(testContext, store, homepageUrl) { callback -> + thumbnails = HomepageThumbnails(testContext, store, homepageUrl, mainDispatcher = testDispatcher) { callback -> callback(bitmap) } } @Test - fun `capture thumbnail when homepage is opened`() { + fun `capture thumbnail when homepage is opened`() = runTest { thumbnails.start() + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertLastAction(ContentAction.UpdateThumbnailAction::class) { - assertEquals(tabId, this.tabId) - assertEquals(bitmap, this.bitmap) + assertEquals(tabId, tabId) + assertEquals(bitmap, bitmap) } } @@ -74,6 +74,8 @@ class HomepageThumbnailsTest { } feature.start() + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertNotDispatched(ContentAction.UpdateThumbnailAction::class) } @@ -81,11 +83,13 @@ class HomepageThumbnailsTest { fun `capture all thumbnails if multiple new tabs are opened`() { val store = BrowserStore(BrowserState(), middlewares) val bitmap: Bitmap = mock() - val feature = HomepageThumbnails(testContext, store, homepageUrl) { callback -> + val feature = HomepageThumbnails(testContext, store, homepageUrl, mainDispatcher = testDispatcher) { callback -> callback(bitmap) } feature.start() + testDispatcher.scheduler.advanceUntilIdle() + store.dispatch( TabListAction.AddTabAction( createTab(homepageUrl, id = "1"), @@ -94,10 +98,12 @@ class HomepageThumbnailsTest { store.dispatch( TabListAction.SelectTabAction( - tabId = "1", - ), + tabId = "1", + ), ) + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertLastAction(ContentAction.UpdateThumbnailAction::class) { assertEquals("1", it.sessionId) } @@ -110,10 +116,12 @@ class HomepageThumbnailsTest { store.dispatch( TabListAction.SelectTabAction( - "2", - ), + "2", + ), ) + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertLastAction(ContentAction.UpdateThumbnailAction::class) { assertEquals("2", it.sessionId) } @@ -126,10 +134,12 @@ class HomepageThumbnailsTest { store.dispatch( TabListAction.SelectTabAction( - tabId = "3", - ), + tabId = "3", + ), ) + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertLastAction(ContentAction.UpdateThumbnailAction::class) { assertEquals("2", it.sessionId) } @@ -146,6 +156,8 @@ class HomepageThumbnailsTest { ), ) + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertLastAction(ContentAction.UpdateThumbnailAction::class) { assertEquals("4", it.sessionId) } @@ -166,19 +178,23 @@ class HomepageThumbnailsTest { ), ) + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertNotDispatched(ContentAction.UpdateThumbnailAction::class) } @Test - fun `feature never captures thumbnail if there is no callback to create bitmap`() { - thumbnails = HomepageThumbnails(testContext, store, homepageUrl) + fun `feature never captures thumbnail if there is no callback to create bitmap`() = runTest { + thumbnails = HomepageThumbnails(testContext, store, homepageUrl, mainDispatcher = testDispatcher) thumbnails.start() + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertNotDispatched(ContentAction.UpdateThumbnailAction::class) } @Test - fun `feature never captures thumbnail if there is no selected tab ID`() { + fun `feature never captures thumbnail if there is no selected tab ID`() = runTest { store = BrowserStore( BrowserState( tabs = listOf( @@ -189,21 +205,25 @@ class HomepageThumbnailsTest { ) val bitmap: Bitmap = mock() - val feature = HomepageThumbnails(testContext, store, homepageUrl) { callback -> + val feature = HomepageThumbnails(testContext, store, homepageUrl, mainDispatcher = testDispatcher) { callback -> callback(bitmap) } feature.start() + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertNotDispatched(ContentAction.UpdateThumbnailAction::class) } @Test - fun `when homepage is opened and the os is in low memory condition thumbnail should not be captured`() { + fun `when homepage is opened and the os is in low memory condition thumbnail should not be captured`() = runTest { thumbnails.testLowMemory = true thumbnails.start() + testDispatcher.scheduler.advanceUntilIdle() + captureActionsMiddleware.assertNotDispatched(ContentAction.UpdateThumbnailAction::class) } } diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/ext/StoreExtensions.kt @@ -10,8 +10,10 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.awaitClose @@ -203,6 +205,30 @@ fun <S : State, A : Action> Store<S, A>.flowScoped( } /** + * Launches a coroutine in a new [CoroutineScope] using the provided [dispatcher] and creates a [Flow] + * for observing the [Store] in that scope. Invokes [block] inside that scope and passes the [Flow] to it. + * + * @param owner An optional [LifecycleOwner] that will be used to determine when to pause and resume + * the store subscription. When the [Lifecycle] is in STOPPED state then no [State] will be received. + * Once the [Lifecycle] switches back to at least STARTED state then the latest [State] and further + * updates will be emitted. + * @param dispatcher The [CoroutineDispatcher] to be used for the [CoroutineScope] in which the flow will be collected. + * @return The [CoroutineScope] [block] is getting executed in. + */ +@MainThread +fun <S : State, A : Action> Store<S, A>.flowScoped( + owner: LifecycleOwner? = null, + dispatcher: CoroutineDispatcher, + block: suspend (Flow<S>) -> Unit, +): CoroutineScope { + return CoroutineScope(SupervisorJob() + dispatcher).apply { + launch { + block(flow(owner)) + } + } +} + +/** * GenericLifecycleObserver implementation to bind an observer to a Lifecycle. */ private class SubscriptionLifecycleBinding<S : State, A : Action>( diff --git a/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt b/mobile/android/android-components/components/lib/state/src/main/java/mozilla/components/lib/state/helpers/AbstractBinding.kt @@ -5,7 +5,9 @@ package mozilla.components.lib.state.helpers import androidx.annotation.CallSuper +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import mozilla.components.lib.state.Action @@ -20,12 +22,13 @@ import mozilla.components.support.base.feature.LifecycleAwareFeature */ abstract class AbstractBinding<in S : State>( private val store: Store<S, out Action>, + protected val mainDispatcher: CoroutineDispatcher = Dispatchers.Main, ) : LifecycleAwareFeature { private var scope: CoroutineScope? = null @CallSuper override fun start() { - scope = store.flowScoped { flow -> + scope = store.flowScoped(dispatcher = mainDispatcher) { flow -> onState(flow) } } diff --git a/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt b/mobile/android/android-components/components/lib/state/src/test/java/mozilla/components/lib/state/helpers/AbstractBindingTest.kt @@ -5,21 +5,19 @@ package mozilla.components.lib.state.helpers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.test.StandardTestDispatcher import mozilla.components.lib.state.Store import mozilla.components.lib.state.TestAction import mozilla.components.lib.state.TestState import mozilla.components.lib.state.reducer -import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Assert.fail -import org.junit.Rule import org.junit.Test class AbstractBindingTest { - @get:Rule - val coroutinesTestRule = MainCoroutineRule() + val testDispatcher = StandardTestDispatcher() @Test fun `binding onState is invoked when a flow is created`() { @@ -29,10 +27,12 @@ class AbstractBindingTest { ) val binding = TestBinding(store) + testDispatcher.scheduler.advanceUntilIdle() assertFalse(binding.invoked) binding.start() + testDispatcher.scheduler.advanceUntilIdle() assertTrue(binding.invoked) } @@ -45,10 +45,12 @@ class AbstractBindingTest { ) val binding = TestBinding(store) + testDispatcher.scheduler.advanceUntilIdle() assertFalse(binding.invoked) binding.stop() + testDispatcher.scheduler.advanceUntilIdle() assertFalse(binding.invoked) } @@ -69,26 +71,29 @@ class AbstractBindingTest { fail() } } + testDispatcher.scheduler.advanceUntilIdle() store.dispatch(TestAction.IncrementAction) binding.start() + testDispatcher.scheduler.advanceUntilIdle() store.dispatch(TestAction.IncrementAction) binding.stop() + testDispatcher.scheduler.advanceUntilIdle() store.dispatch(TestAction.IncrementAction) } -} -class TestBinding( - store: Store<TestState, TestAction>, - private val onStateUpdated: (TestState) -> Unit = {}, -) : AbstractBinding<TestState>(store) { - var invoked = false - override suspend fun onState(flow: Flow<TestState>) { - invoked = true - flow.collect { onStateUpdated(it) } + inner class TestBinding( + store: Store<TestState, TestAction>, + private val onStateUpdated: (TestState) -> Unit = {}, + ) : AbstractBinding<TestState>(store, testDispatcher) { + var invoked = false + override suspend fun onState(flow: Flow<TestState>) { + invoked = true + flow.collect { onStateUpdated(it) } + } } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/AboutHomeBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/AboutHomeBinding.kt @@ -5,6 +5,8 @@ package org.mozilla.fenix import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -23,7 +25,8 @@ import org.mozilla.fenix.home.HomeFragment class AboutHomeBinding( browserStore: BrowserStore, private val navController: NavController, -) : AbstractBinding<BrowserState>(browserStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserState>(browserStore, mainDispatcher) { override suspend fun onState(flow: Flow<BrowserState>) { flow diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OpenInFirefoxBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/OpenInFirefoxBinding.kt @@ -5,6 +5,8 @@ package org.mozilla.fenix import android.content.Intent +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -26,6 +28,8 @@ import org.mozilla.fenix.components.appstate.AppState * @param customTabsUseCases The [CustomTabsUseCases] used to turn the session into a regular tab and select it. * @param openInFenixIntent The [Intent] used to open the tab in the browser. * @param sessionFeature The [SessionFeature] used to release the session from the EngineView. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class OpenInFirefoxBinding( private val activity: HomeActivity, @@ -34,7 +38,8 @@ class OpenInFirefoxBinding( private val customTabsUseCases: CustomTabsUseCases, private val openInFenixIntent: Intent, private val sessionFeature: ViewBoundFeatureWrapper<SessionFeature>, -) : AbstractBinding<AppState>(appStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<AppState>(appStore, mainDispatcher) { override suspend fun onState(flow: Flow<AppState>) { flow.map { state -> state.openInFirefoxRequested } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ReaderViewBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/ReaderViewBinding.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -24,11 +26,14 @@ import org.mozilla.fenix.components.appstate.readerview.ReaderViewState.ShowCont * @param appStore The [AppStore] used to observe [AppState.isReaderViewActive]. * @param readerMenuController The [ReaderModeController] that will used for toggling the reader * view feature and controls. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class ReaderViewBinding( private val appStore: AppStore, private val readerMenuController: ReaderModeController, -) : AbstractBinding<AppState>(appStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<AppState>(appStore, mainDispatcher) { override suspend fun onState(flow: Flow<AppState>) { flow.map { state -> state.readerViewState } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bindings/ExternalAppLinkStatusBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bindings/ExternalAppLinkStatusBinding.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.bindings +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -25,13 +27,16 @@ import org.mozilla.fenix.utils.Settings * @param appLinksUseCases The use cases for handling app links. * @param appStore The application store for dispatching actions. * @param browserStore The browser store to observe state changes. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class ExternalAppLinkStatusBinding( private val settings: Settings, private val appLinksUseCases: AppLinksUseCases, private val appStore: AppStore, browserStore: BrowserStore, -) : AbstractBinding<BrowserState>(browserStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserState>(browserStore, mainDispatcher) { override suspend fun onState(flow: Flow<BrowserState>) { flow diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bindings/FindInPageBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/bindings/FindInPageBinding.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.bindings +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -18,11 +20,14 @@ import org.mozilla.fenix.components.appstate.AppState * * @param appStore The [AppStore] used to observe [AppState.showFindInPage]. * @param onFindInPageLaunch Invoked when the find in page feature should be launched. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class FindInPageBinding( private val appStore: AppStore, private val onFindInPageLaunch: () -> Unit, -) : AbstractBinding<AppState>(appStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<AppState>(appStore, mainDispatcher) { override suspend fun onState(flow: Flow<AppState>) { flow.map { state -> state.showFindInPage } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/CustomTabColorsBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/CustomTabColorsBinding.kt @@ -7,6 +7,8 @@ package org.mozilla.fenix.browser import android.view.Window import androidx.annotation.ColorInt import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChangedBy import mozilla.components.lib.state.helpers.AbstractBinding @@ -20,11 +22,14 @@ import org.mozilla.fenix.browser.store.BrowserScreenStore * * @param browserScreenStore [BrowserScreenStore] to observe for custom colors changes. * @param window [Window] allowing to update the system bars' backgrounds. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class CustomTabColorsBinding( browserScreenStore: BrowserScreenStore, private val window: Window? = null, -) : AbstractBinding<BrowserScreenState>(browserScreenStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserScreenState>(browserScreenStore, mainDispatcher) { override suspend fun onState(flow: Flow<BrowserScreenState>) { flow.distinctUntilChangedBy { it.customTabColors } .collect { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/StandardSnackbarErrorBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/StandardSnackbarErrorBinding.kt @@ -5,6 +5,8 @@ package org.mozilla.fenix.browser import android.view.ViewGroup +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull @@ -25,13 +27,16 @@ import org.mozilla.fenix.compose.snackbar.SnackbarState * @param appStore The [AppStore] containing information about when to show a snackbar styled for errors. * @param snackbarFactory The [SnackbarFactory] used to create the snackbar. * @param dismissLabel The label for the dismiss action on the snackbar. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class StandardSnackbarErrorBinding( private val snackbarParent: ViewGroup, private val appStore: AppStore, private val snackbarFactory: SnackbarFactory, private val dismissLabel: String, -) : AbstractBinding<AppState>(appStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<AppState>(appStore, mainDispatcher) { override suspend fun onState(flow: Flow<AppState>) { flow.map { state -> state.standardSnackbarError } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TranslationsBannerIntegration.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TranslationsBannerIntegration.kt @@ -12,7 +12,9 @@ import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.res.stringResource import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.isVisible +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -39,13 +41,16 @@ import org.mozilla.fenix.translations.TranslationToolbar * @param browserScreenStore [BrowserScreenStore] to sync the current translations status from. * @param binding [FragmentBrowserBinding] to inflate the banner into when needed. * @param onExpand invoked when user wants to expand the translations controls. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class TranslationsBannerIntegration( private val browserStore: BrowserStore, private val browserScreenStore: BrowserScreenStore, private val binding: FragmentBrowserBinding, private val onExpand: () -> Unit = {}, -) : AbstractBinding<BrowserScreenState>(browserScreenStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserScreenState>(browserScreenStore, mainDispatcher) { private var browserFlowScope: CoroutineScope? = null diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TranslationsBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/browser/TranslationsBinding.kt @@ -6,6 +6,8 @@ package org.mozilla.fenix.browser import androidx.annotation.VisibleForTesting import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -40,6 +42,8 @@ import org.mozilla.fenix.translations.TranslationsFlowState * @param onTranslationStatusUpdate Invoked when the translation status of the current page is updated. * @param onShowTranslationsDialog Invoked when [TranslationDialogBottomSheet] * should be automatically shown to the user. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class TranslationsBinding( private val browserStore: BrowserStore, @@ -48,7 +52,8 @@ class TranslationsBinding( private val navController: NavController? = null, private val onTranslationStatusUpdate: (PageTranslationStatus) -> Unit = { _ -> }, private val onShowTranslationsDialog: () -> Unit = { }, -) : AbstractBinding<BrowserState>(browserStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserState>(browserStore, mainDispatcher) { @Suppress("LongMethod", "CognitiveComplexMethod") override suspend fun onState(flow: Flow<BrowserState>) { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/WebExtensionsMenuBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/components/menu/WebExtensionsMenuBinding.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.components.menu +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -32,6 +34,8 @@ import org.mozilla.fenix.components.menu.store.WebExtensionMenuItem * @param menuStore The [Store] for holding the [MenuState] and applying [MenuAction]s. * @param iconSize for [WebExtensionMenuItem]. * @param onDismiss Callback invoked to dismiss the menu dialog. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class WebExtensionsMenuBinding( browserStore: BrowserStore, @@ -39,7 +43,8 @@ class WebExtensionsMenuBinding( private val menuStore: MenuStore, private val iconSize: Int, private val onDismiss: () -> Unit, -) : AbstractBinding<BrowserState>(browserStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserState>(browserStore, mainDispatcher) { override suspend fun onState(flow: Flow<BrowserState>) { // Browser level flows diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterBinding.kt @@ -5,6 +5,8 @@ package org.mozilla.fenix.crashes import android.content.Context +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChangedBy import mozilla.components.lib.crash.store.CrashState @@ -18,12 +20,15 @@ import org.mozilla.fenix.components.appstate.AppState * @param context The [Context] used to open links via Intents. * @param store The [AppStore] used to observe the [CrashState]. * @param onReporting a callback that is called when [CrashState] is [CrashState.Reporting]. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class CrashReporterBinding( private val context: Context, store: AppStore, private val onReporting: (List<String>?, Context) -> Unit, -) : AbstractBinding<AppState>(store) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<AppState>(store, mainDispatcher) { override suspend fun onState(flow: Flow<AppState>) { flow.distinctUntilChangedBy { state -> state.crashState } .collect { state -> diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.home.recenttabs +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -22,7 +24,8 @@ import org.mozilla.fenix.ext.asRecentTabs class RecentTabsListFeature( browserStore: BrowserStore, private val appStore: AppStore, -) : AbstractBinding<BrowserState>(browserStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserState>(browserStore, mainDispatcher) { override suspend fun onState(flow: Flow<BrowserState>) { // Listen for changes regarding the currently selected tab and in progress media tab. diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorBinding.kt @@ -6,6 +6,8 @@ package org.mozilla.fenix.home.toolbar import android.content.Context import androidx.core.graphics.drawable.toDrawable +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -33,7 +35,8 @@ internal class SearchSelectorBinding( private val toolbarView: HomeToolbarView, private val searchSelectorMenu: SearchSelectorMenu, browserStore: BrowserStore, -) : AbstractBinding<BrowserState>(browserStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserState>(browserStore, mainDispatcher) { override fun start() { super.start() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorMenuBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorMenuBinding.kt @@ -6,6 +6,8 @@ package org.mozilla.fenix.home.toolbar import android.content.Context import androidx.core.graphics.drawable.toDrawable +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -29,7 +31,8 @@ class SearchSelectorMenuBinding( private val interactor: SearchSelectorInteractor, private val searchSelectorMenu: SearchSelectorMenu, browserStore: BrowserStore, -) : AbstractBinding<BrowserState>(browserStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserState>(browserStore, mainDispatcher) { override suspend fun onState(flow: Flow<BrowserState>) { flow.map { state -> state.search } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/state/bindings/MenuBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/state/bindings/MenuBinding.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.library.history.state.bindings +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChangedBy import mozilla.components.lib.state.helpers.AbstractBinding @@ -16,7 +18,8 @@ import org.mozilla.fenix.library.history.HistoryFragmentStore class MenuBinding( store: HistoryFragmentStore, val invalidateOptionsMenu: () -> Unit, -) : AbstractBinding<HistoryFragmentState>(store) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<HistoryFragmentState>(store, mainDispatcher) { override suspend fun onState(flow: Flow<HistoryFragmentState>) { flow.distinctUntilChangedBy { it.mode } .collect { invalidateOptionsMenu() } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/state/bindings/PendingDeletionBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/library/history/state/bindings/PendingDeletionBinding.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.library.history.state.bindings +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChangedBy import mozilla.components.lib.state.helpers.AbstractBinding @@ -17,7 +19,8 @@ import org.mozilla.fenix.library.history.HistoryView class PendingDeletionBinding( appStore: AppStore, private val view: HistoryView, -) : AbstractBinding<AppState>(appStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<AppState>(appStore, mainDispatcher) { override suspend fun onState(flow: Flow<AppState>) { flow.distinctUntilChangedBy { it.pendingDeletionHistoryItems } .collect { view.update(it) } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/reviewprompt/ShowPlayStoreReviewPrompt.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/reviewprompt/ShowPlayStoreReviewPrompt.kt @@ -6,7 +6,9 @@ package org.mozilla.fenix.reviewprompt import android.app.Activity import androidx.navigation.NavDirections +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -30,7 +32,8 @@ class ShowPlayStoreReviewPrompt( private val activityRef: WeakReference<Activity>, private val uiScope: CoroutineScope, private val navigationDirection: (NavDirections) -> Unit, -) : AbstractBinding<AppState>(appStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<AppState>(appStore, mainDispatcher) { override suspend fun onState(flow: Flow<AppState>) { flow.map { it.reviewPrompt } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/snackbar/SnackbarBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/snackbar/SnackbarBinding.kt @@ -56,6 +56,8 @@ import org.mozilla.fenix.utils.getSnackbarTimeout * if the selected session should be used. * @param ioDispatcher The [CoroutineDispatcher] used for background operations executed when * the user starts a snackbar action. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ @Suppress("LongParameterList") class SnackbarBinding( @@ -68,7 +70,8 @@ class SnackbarBinding( private val sendTabUseCases: SendTabUseCases?, private val customTabSessionId: String?, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, -) : AbstractBinding<AppState>(appStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<AppState>(appStore, mainDispatcher) { private val currentSession get() = browserStore.state.findCustomTabOrSelectedTab(customTabSessionId) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/InactiveTabsBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/InactiveTabsBinding.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.tabstray +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -16,11 +18,14 @@ import org.mozilla.fenix.components.appstate.AppState * * @param appStore [AppStore] used to listen for changes to [AppState]. * @param tabsTrayStore [TabsTrayStore] used to listen for changes to [TabsTrayState]. + * @param mainDispatcher The [CoroutineDispatcher] on which the state observation and updates will occur. + * Defaults to [Dispatchers.Main]. */ class InactiveTabsBinding( appStore: AppStore, private val tabsTrayStore: TabsTrayStore, -) : AbstractBinding<AppState>(appStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<AppState>(appStore, mainDispatcher) { override suspend fun onState(flow: Flow<AppState>) { flow.distinctUntilChangedBy { it.inactiveTabsExpanded } .collectLatest { diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SecureTabsTrayBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/SecureTabsTrayBinding.kt @@ -5,7 +5,10 @@ package org.mozilla.fenix.tabstray import android.view.WindowManager +import androidx.annotation.VisibleForTesting import androidx.fragment.app.Fragment +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import mozilla.components.lib.state.helpers.AbstractBinding @@ -22,7 +25,8 @@ class SecureTabsTrayBinding( private val settings: Settings, private val fragment: Fragment, private val dialog: TabsTrayDialog, -) : AbstractBinding<TabsTrayState>(store) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<TabsTrayState>(store, mainDispatcher) { override suspend fun onState(flow: Flow<TabsTrayState>) { flow.map { it } @@ -36,12 +40,21 @@ class SecureTabsTrayBinding( state.selectedPage == Page.PrivateTabs && !settings.shouldSecureModeBeOverridden ) { - fragment.secure() + setSecureMode(true) dialog.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE) } else if (!settings.lastKnownMode.isPrivate) { - fragment.removeSecure() + setSecureMode(false) dialog.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } } } + + @VisibleForTesting + internal fun setSecureMode(isSecure: Boolean) { + if (isSecure) { + fragment.secure() + } else { + fragment.removeSecure() + } + } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBinding.kt @@ -4,7 +4,10 @@ package org.mozilla.fenix.tabstray.binding +import androidx.annotation.VisibleForTesting import androidx.fragment.app.Fragment +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import mozilla.components.lib.state.helpers.AbstractBinding @@ -24,7 +27,8 @@ class SecureTabManagerBinding( store: TabsTrayStore, private val settings: Settings, private val fragment: Fragment, -) : AbstractBinding<TabsTrayState>(store) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<TabsTrayState>(store, mainDispatcher) { override suspend fun onState(flow: Flow<TabsTrayState>) { flow.map { it } @@ -38,9 +42,9 @@ class SecureTabManagerBinding( state.selectedPage == Page.PrivateTabs && !settings.shouldSecureModeBeOverridden ) { - fragment.secure() + setSecureMode(true) } else if (!settings.lastKnownMode.isPrivate) { - fragment.removeSecure() + setSecureMode(false) } } } @@ -48,6 +52,15 @@ class SecureTabManagerBinding( override fun stop() { super.stop() if (!settings.lastKnownMode.isPrivate) { + setSecureMode(false) + } + } + + @VisibleForTesting + internal fun setSecureMode(isSecure: Boolean) { + if (isSecure) { + fragment.secure() + } else { fragment.removeSecure() } } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.tabstray.syncedtabs +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -20,8 +22,9 @@ import org.mozilla.fenix.tabstray.TabsTrayStore */ class SyncButtonBinding( tabsTrayStore: TabsTrayStore, + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, private val onSyncNow: () -> Unit, -) : AbstractBinding<TabsTrayState>(tabsTrayStore) { +) : AbstractBinding<TabsTrayState>(tabsTrayStore, mainDispatcher) { override suspend fun onState(flow: Flow<TabsTrayState>) { flow.map { it.syncing } .distinctUntilChanged() diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/translations/TranslationsDialogBinding.kt @@ -4,6 +4,8 @@ package org.mozilla.fenix.translations +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChangedBy @@ -30,7 +32,8 @@ class TranslationsDialogBinding( browserStore: BrowserStore, private val translationsDialogStore: TranslationsDialogStore, private val getTranslatedPageTitle: (localizedFrom: String?, localizedTo: String?) -> String, -) : AbstractBinding<BrowserState>(browserStore) { + mainDispatcher: CoroutineDispatcher = Dispatchers.Main, +) : AbstractBinding<BrowserState>(browserStore, mainDispatcher) { @Suppress("LongMethod", "CyclomaticComplexMethod") override suspend fun onState(flow: Flow<BrowserState>) { diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/AboutHomeBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/AboutHomeBindingTest.kt @@ -7,6 +7,8 @@ package org.mozilla.fenix import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.TabSessionState @@ -14,12 +16,9 @@ import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.utils.ABOUT_HOME_URL import mozilla.components.support.test.mock -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import mozilla.components.support.test.whenever import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.never @@ -27,8 +26,7 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) class AboutHomeBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() private lateinit var browserStore: BrowserStore private lateinit var tabId: String @@ -53,10 +51,11 @@ class AboutHomeBindingTest { } @Test - fun `WHEN URL is updated to ABOUT_HOME THEN navigate to the homepage`() = runTestOnMain { + fun `WHEN URL is updated to ABOUT_HOME THEN navigate to the homepage`() = runTest(testDispatcher) { val binding = AboutHomeBinding( browserStore = browserStore, navController = navController, + mainDispatcher = testDispatcher, ) binding.start() @@ -68,13 +67,15 @@ class AboutHomeBindingTest { ), ) + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(ABOUT_HOME_URL, tab.content.url) verify(navController).navigate(NavGraphDirections.actionGlobalHome()) } @Test - fun `GIVEN homepage is the currently shown WHEN URL is updated to ABOUT_HOME THEN do not navigate to the homepage`() = runTestOnMain { + fun `GIVEN homepage is the currently shown WHEN URL is updated to ABOUT_HOME THEN do not navigate to the homepage`() = runTest(testDispatcher) { val mockDestination: NavDestination = mock() whenever(mockDestination.id).thenReturn(R.id.homeFragment) whenever(navController.currentDestination).thenReturn(mockDestination) @@ -82,6 +83,7 @@ class AboutHomeBindingTest { val binding = AboutHomeBinding( browserStore = browserStore, navController = navController, + mainDispatcher = testDispatcher, ) binding.start() @@ -93,13 +95,15 @@ class AboutHomeBindingTest { ), ) + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(ABOUT_HOME_URL, tab.content.url) verify(navController, never()).navigate(NavGraphDirections.actionGlobalHome()) } @Test - fun `GIVEN onboarding is the currently shown WHEN URL is updated to ABOUT_HOME THEN do not navigate to the homepage`() = runTestOnMain { + fun `GIVEN onboarding is the currently shown WHEN URL is updated to ABOUT_HOME THEN do not navigate to the homepage`() = runTest(testDispatcher) { val mockDestination: NavDestination = mock() whenever(mockDestination.id).thenReturn(R.id.onboardingFragment) whenever(navController.currentDestination).thenReturn(mockDestination) @@ -107,7 +111,8 @@ class AboutHomeBindingTest { val binding = AboutHomeBinding( browserStore = browserStore, navController = navController, - ) + mainDispatcher = testDispatcher, + ) binding.start() @@ -118,13 +123,16 @@ class AboutHomeBindingTest { ), ) + // Wait for ContentAction.UpdateUrlAction + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(ABOUT_HOME_URL, tab.content.url) verify(navController, never()).navigate(NavGraphDirections.actionGlobalHome()) } @Test - fun `WHEN URL is updated to a URL that is not ABOUT_HOME THEN do not navigate to the homepage`() = runTestOnMain { + fun `WHEN URL is updated to a URL that is not ABOUT_HOME THEN do not navigate to the homepage`() = runTest(testDispatcher) { val binding = AboutHomeBinding( browserStore = browserStore, navController = navController, @@ -140,6 +148,9 @@ class AboutHomeBindingTest { ), ) + // Wait for ContentAction.UpdateUrlAction + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(newUrl, tab.content.url) verify(navController, never()).navigate(NavGraphDirections.actionGlobalHome()) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/OpenInFirefoxBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/OpenInFirefoxBindingTest.kt @@ -7,25 +7,26 @@ package org.mozilla.fenix import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 import junit.framework.TestCase.assertFalse +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.tabs.CustomTabsUseCases import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.test.mock -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import mozilla.components.support.test.whenever import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.verify import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class OpenInFirefoxBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() + + private val testDispatcher = StandardTestDispatcher() private lateinit var activity: HomeActivity private lateinit var customTabsUseCases: CustomTabsUseCases @@ -41,7 +42,7 @@ class OpenInFirefoxBindingTest { } @Test - fun `WHEN open in Firefox is requested THEN open in Firefox`() = runTestOnMain { + fun `WHEN open in Firefox is requested THEN open in Firefox`() = runTest(testDispatcher) { val appStore = AppStore() val binding = OpenInFirefoxBinding( @@ -51,6 +52,7 @@ class OpenInFirefoxBindingTest { customTabsUseCases = customTabsUseCases, openInFenixIntent = openInFenixIntent, sessionFeature = sessionFeature, + mainDispatcher = testDispatcher, ) val getSessionFeature: SessionFeature = mock() @@ -63,6 +65,8 @@ class OpenInFirefoxBindingTest { appStore.dispatch(AppAction.OpenInFirefoxStarted) + testDispatcher.scheduler.advanceUntilIdle() + verify(getSessionFeature).release() verify(migrateCustomTabsUseCases).invoke("", select = true) verify(activity).startActivity(openInFenixIntent) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/ReaderViewBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/ReaderViewBindingTest.kt @@ -5,12 +5,11 @@ package org.mozilla.fenix import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.support.test.mock -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.verify @@ -22,9 +21,8 @@ import org.mozilla.fenix.components.appstate.readerview.ReaderViewState @RunWith(AndroidJUnit4::class) class ReaderViewBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() private lateinit var readerModeController: ReaderModeController @Before @@ -33,55 +31,64 @@ class ReaderViewBindingTest { } @Test - fun `WHEN the reader view state is active THEN show reader view`() = runTestOnMain { + fun `WHEN the reader view state is active THEN show reader view`() = runTest(testDispatcher) { val appStore = AppStore() val binding = ReaderViewBinding( appStore = appStore, readerMenuController = readerModeController, + mainDispatcher = testDispatcher, ) binding.start() appStore.dispatch(ReaderViewAction.ReaderViewStarted) + testDispatcher.scheduler.advanceUntilIdle() + verify(readerModeController).showReaderView() assertEquals(ReaderViewState.None, appStore.state.readerViewState) } @Test - fun `WHEN the reader view state is dismiss THEN hide reader view`() = runTestOnMain { + fun `WHEN the reader view state is dismiss THEN hide reader view`() = runTest(testDispatcher) { val appStore = AppStore( initialState = AppState(), ) val binding = ReaderViewBinding( appStore = appStore, readerMenuController = readerModeController, + mainDispatcher = testDispatcher, ) binding.start() appStore.dispatch(ReaderViewAction.ReaderViewDismissed) + testDispatcher.scheduler.advanceUntilIdle() + verify(readerModeController).hideReaderView() assertEquals(ReaderViewState.None, appStore.state.readerViewState) } @Test - fun `WHEN the reader view state is show controls THEN show reader view customization controls`() = runTestOnMain { + fun `WHEN the reader view state is show controls THEN show reader view customization controls`() = runTest(testDispatcher) { val appStore = AppStore( initialState = AppState(), ) val binding = ReaderViewBinding( appStore = appStore, readerMenuController = readerModeController, + mainDispatcher = testDispatcher, ) binding.start() appStore.dispatch(ReaderViewAction.ReaderViewControlsShown) + testDispatcher.scheduler.advanceUntilIdle() + verify(readerModeController).showControls() assertEquals(ReaderViewState.None, appStore.state.readerViewState) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/bindings/ExternalAppLinkStatusBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/bindings/ExternalAppLinkStatusBindingTest.kt @@ -3,15 +3,14 @@ package org.mozilla.fenix.bindings import io.mockk.every import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.app.links.AppLinksUseCases -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import org.junit.Before -import org.junit.Rule import org.junit.Test import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction @@ -19,9 +18,7 @@ import org.mozilla.fenix.components.appstate.SupportedMenuNotifications import org.mozilla.fenix.utils.Settings class ExternalAppLinkStatusBindingTest { - - @get:Rule - val coroutinesTestRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() private lateinit var appStore: AppStore private lateinit var settings: Settings private lateinit var useCases: AppLinksUseCases @@ -42,7 +39,7 @@ class ExternalAppLinkStatusBindingTest { } @Test - fun `GIVEN the current url has no external app available WHEN a different url with an external app is selected THEN add open in app menu notification`() = runTestOnMain { + fun `GIVEN the current url has no external app available WHEN a different url with an external app is selected THEN add open in app menu notification`() = runTest(testDispatcher) { val tabExternal = createTab(url = urlExternal, id = "external") val tabNonExternal = createTab(url = urlNonExternal, id = "nonExternal") val browserStore = BrowserStore( @@ -56,13 +53,17 @@ class ExternalAppLinkStatusBindingTest { appLinksUseCases = useCases, appStore = appStore, browserStore = browserStore, + mainDispatcher = testDispatcher, ) binding.start() + testDispatcher.scheduler.advanceUntilIdle() browserStore.dispatch( TabListAction.SelectTabAction(tabId = "nonExternal"), ) + testDispatcher.scheduler.advanceUntilIdle() + verify { appStore.dispatch( AppAction.MenuNotification.AddMenuNotification( @@ -73,7 +74,7 @@ class ExternalAppLinkStatusBindingTest { } @Test - fun `GIVEN the current url has an external app available WHEN a different url without an external app is selected THEN remove open in app menu notification`() = runTestOnMain { + fun `GIVEN the current url has an external app available WHEN a different url without an external app is selected THEN remove open in app menu notification`() = runTest(testDispatcher) { val tabExternal = createTab(url = urlExternal, id = "external") val tabNonExternal = createTab(url = urlNonExternal, id = "nonExternal") val browserStore = BrowserStore( @@ -87,13 +88,17 @@ class ExternalAppLinkStatusBindingTest { appLinksUseCases = useCases, appStore = appStore, browserStore = browserStore, + mainDispatcher = testDispatcher, ) binding.start() + testDispatcher.scheduler.advanceUntilIdle() browserStore.dispatch( TabListAction.SelectTabAction(tabId = "external"), ) + testDispatcher.scheduler.advanceUntilIdle() + verify { appStore.dispatch( AppAction.MenuNotification.RemoveMenuNotification( diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/bindings/FindInPageBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/bindings/FindInPageBindingTest.kt @@ -4,33 +4,36 @@ package org.mozilla.fenix.bindings -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Rule import org.junit.Test import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction.FindInPageAction class FindInPageBindingTest { - @get:Rule - val coroutinesTestRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() @Test - fun `WHEN find in page started action is dispatched THEN launch find in page feature`() = runTestOnMain { + fun `WHEN find in page started action is dispatched THEN launch find in page feature`() = runTest(testDispatcher) { val appStore = AppStore() var onFindInPageLaunchCalled = false val binding = FindInPageBinding( appStore = appStore, onFindInPageLaunch = { onFindInPageLaunchCalled = true }, + mainDispatcher = testDispatcher, ) binding.start() appStore.dispatch(FindInPageAction.FindInPageStarted) + // Wait for FindInPageAction.FindInPageStarted + testDispatcher.scheduler.advanceUntilIdle() + // Wait for FindInPageAction.FindInPageShown + assertFalse(appStore.state.showFindInPage) assertTrue(onFindInPageLaunchCalled) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/bindings/InactiveTabsBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/bindings/InactiveTabsBindingTest.kt @@ -4,10 +4,11 @@ package org.mozilla.fenix.bindings +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.state.createTab -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain -import org.junit.Rule +import mozilla.components.support.test.ext.joinBlocking import org.junit.Test import org.mockito.Mockito.spy import org.mockito.Mockito.verify @@ -19,11 +20,10 @@ import org.mozilla.fenix.tabstray.TabsTrayAction import org.mozilla.fenix.tabstray.TabsTrayState import org.mozilla.fenix.tabstray.TabsTrayStore +@OptIn(ExperimentalCoroutinesApi::class) class InactiveTabsBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() - + private val testDispatcher = StandardTestDispatcher() lateinit var tabsTrayStore: TabsTrayStore lateinit var appStore: AppStore @@ -31,7 +31,7 @@ class InactiveTabsBindingTest { private val tab1 = createTab(url = tabId1, id = tabId1) @Test - fun `WHEN inactiveTabsExpanded changes THEN tabs tray action dispatched with update`() = runTestOnMain { + fun `WHEN inactiveTabsExpanded changes THEN tabs tray action dispatched with update`() = runTest(testDispatcher) { appStore = AppStore( AppState( inactiveTabsExpanded = false, @@ -49,9 +49,11 @@ class InactiveTabsBindingTest { val binding = InactiveTabsBinding( appStore = appStore, tabsTrayStore = tabsTrayStore, + mainDispatcher = testDispatcher, ) binding.start() appStore.dispatch(AppAction.UpdateInactiveExpanded(true)) + testDispatcher.scheduler.advanceUntilIdle() verify(tabsTrayStore).dispatch(TabsTrayAction.UpdateInactiveExpanded(true)) } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/browser/CustomTabColorsBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/browser/CustomTabColorsBindingTest.kt @@ -7,8 +7,8 @@ package org.mozilla.fenix.browser import android.view.Window import io.mockk.mockk import io.mockk.verify -import mozilla.components.support.test.rule.MainCoroutineRule -import org.junit.Rule +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.browser.store.BrowserScreenAction.CustomTabColorsUpdated @@ -19,18 +19,18 @@ import org.robolectric.RobolectricTestRunner @Suppress("DEPRECATION") // for accessing the window properties @RunWith(RobolectricTestRunner::class) class CustomTabColorsBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() private val window: Window = mockk(relaxed = true) private val store = BrowserScreenStore() - private val binding = CustomTabColorsBinding(store, window) + private val binding = CustomTabColorsBinding(store, window, testDispatcher) @Test - fun `WHEN colors for the system bars change THEN apply them to the system bars`() { + fun `WHEN colors for the system bars change THEN apply them to the system bars`() = runTest(testDispatcher) { binding.start() store.dispatch(CustomTabColorsUpdated(CustomTabColors(1, 2, 3, 4, 5))) + testDispatcher.scheduler.advanceUntilIdle() verify { window.statusBarColor = 2 } verify { window.navigationBarColor = 3 } @@ -38,11 +38,12 @@ class CustomTabColorsBindingTest { } @Test - fun `WHEN custom colors are not available THEN don't apply any color change`() { - val binding = CustomTabColorsBinding(store, window) + fun `WHEN custom colors are not available THEN don't apply any color change`() = runTest(testDispatcher) { + val binding = CustomTabColorsBinding(store, window, testDispatcher) binding.start() store.dispatch(CustomTabColorsUpdated(CustomTabColors(null, null, null, null))) + testDispatcher.scheduler.advanceUntilIdle() verify(exactly = 0) { window.statusBarColor = any<Int>() } verify(exactly = 0) { window.navigationBarColor = any<Int>() } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/browser/StandardSnackbarErrorBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/browser/StandardSnackbarErrorBindingTest.kt @@ -10,19 +10,20 @@ import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.mockk import io.mockk.verify -import mozilla.components.support.test.rule.MainCoroutineRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Rule import org.junit.Test import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction import org.mozilla.fenix.compose.snackbar.Snackbar import org.mozilla.fenix.compose.snackbar.SnackbarFactory +@OptIn(ExperimentalCoroutinesApi::class) class StandardSnackbarErrorBindingTest { - @get:Rule - val coroutinesTestRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() private lateinit var snackbarContainer: ViewGroup private lateinit var snackbar: Snackbar private lateinit var snackbarFactory: SnackbarFactory @@ -46,13 +47,14 @@ class StandardSnackbarErrorBindingTest { } @Test - fun `WHEN show standard snackbar error action dispatched THEN snackbar should appear`() { + fun `WHEN show standard snackbar error action dispatched THEN snackbar should appear`() = runTest(testDispatcher) { val appStore = AppStore() val standardSnackbarError = StandardSnackbarErrorBinding( snackbarContainer, appStore, snackbarFactory, "Dismiss", + testDispatcher, ) standardSnackbarError.start() @@ -63,17 +65,20 @@ class StandardSnackbarErrorBindingTest { ), ), ) + testDispatcher.scheduler.advanceUntilIdle() + verify { snackbar.show() } } @Test - fun `WHEN show standard snackbar error action dispatched and binding is stopped THEN snackbar should appear when binding is again started`() { + fun `WHEN show standard snackbar error action dispatched and binding is stopped THEN snackbar should appear when binding is again started`() = runTest(testDispatcher) { val appStore = AppStore() val standardSnackbarError = StandardSnackbarErrorBinding( snackbarContainer, appStore, snackbarFactory, "Dismiss", + testDispatcher, ) standardSnackbarError.start() @@ -84,6 +89,8 @@ class StandardSnackbarErrorBindingTest { ), ), ) + testDispatcher.scheduler.advanceUntilIdle() + standardSnackbarError.stop() standardSnackbarError.start() diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/browser/TranslationsBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/browser/TranslationsBindingTest.kt @@ -7,6 +7,8 @@ package org.mozilla.fenix.browser import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.TranslationsAction import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.ReaderState @@ -21,8 +23,6 @@ import mozilla.components.concept.engine.translate.TranslationOperation import mozilla.components.concept.engine.translate.TranslationPair import mozilla.components.concept.engine.translate.TranslationSupport import mozilla.components.support.test.mock -import mozilla.components.support.test.rule.MainCoroutineRule -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.atLeast @@ -41,8 +41,7 @@ import org.mozilla.fenix.components.appstate.snackbar.SnackbarState.TranslationI @RunWith(AndroidJUnit4::class) class TranslationsBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() lateinit var browserStore: BrowserStore val browserScreenStore: BrowserScreenStore = mock() @@ -55,333 +54,364 @@ class TranslationsBindingTest { private val onShowTranslationsDialog: () -> Unit = spy() @Test - fun `GIVEN translationState WHEN translation status isTranslated THEN inform about translation changes`() { - val englishLanguage = Language("en", "English") - val spanishLanguage = Language("es", "Spanish") - val expectedTranslationStatus = PageTranslationStatus( - isTranslationPossible = true, - isTranslated = true, - isTranslateProcessing = true, - fromSelectedLanguage = englishLanguage, - toSelectedLanguage = spanishLanguage, - ) - - browserStore = BrowserStore( - BrowserState( - tabs = listOf(tab), - selectedTabId = tabId, - translationEngine = TranslationsBrowserState(isEngineSupported = true), - ), - ) - - val binding = TranslationsBinding( - browserStore = browserStore, - browserScreenStore = browserScreenStore, - appStore = appStore, - onTranslationStatusUpdate = onTranslationsActionUpdated, - onShowTranslationsDialog = {}, - ) - binding.start() - - val detectedLanguages = DetectedLanguages( - documentLangTag = englishLanguage.code, - supportedDocumentLang = true, - userPreferredLangTag = spanishLanguage.code, - ) - - val translationEngineState = TranslationEngineState( - detectedLanguages = detectedLanguages, - error = null, - isEngineReady = true, - hasVisibleChange = true, - requestedTranslationPair = TranslationPair( - fromLanguage = englishLanguage.code, - toLanguage = spanishLanguage.code, - ), - ) - - val supportLanguages = TranslationSupport( - fromLanguages = listOf(englishLanguage), - toLanguages = listOf(spanishLanguage), - ) - - browserStore.dispatch( - TranslationsAction.SetSupportedLanguagesAction( - supportedLanguages = supportLanguages, - ), - ) - - browserStore.dispatch( - TranslationsAction.TranslateStateChangeAction( - tabId = tabId, - translationEngineState = translationEngineState, - ), - ) - - browserStore.dispatch( - TranslationsAction.TranslateAction( - tabId = tab.id, - fromLanguage = englishLanguage.code, - toLanguage = spanishLanguage.code, - options = null, - ), - ) - - verify(onTranslationsActionUpdated).invoke(expectedTranslationStatus) - verify(browserScreenStore).dispatch( - PageTranslationStatusUpdated(expectedTranslationStatus), - ) - } + fun `GIVEN translationState WHEN translation status isTranslated THEN inform about translation changes`() = + runTest { + val englishLanguage = Language("en", "English") + val spanishLanguage = Language("es", "Spanish") + val expectedTranslationStatus = PageTranslationStatus( + isTranslationPossible = true, + isTranslated = true, + isTranslateProcessing = true, + fromSelectedLanguage = englishLanguage, + toSelectedLanguage = spanishLanguage, + ) + + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + translationEngine = TranslationsBrowserState(isEngineSupported = true), + ), + ) - @Test - fun `GIVEN translationState WHEN translation status isExpectedTranslate THEN inform about translation changes`() { - val expectedTranslationStatus = PageTranslationStatus( - isTranslationPossible = true, - isTranslated = false, - isTranslateProcessing = false, - ) - browserStore = BrowserStore( - BrowserState( - tabs = listOf(tab), - selectedTabId = tabId, - translationEngine = TranslationsBrowserState(isEngineSupported = true), - ), - ) - val appState: AppState = mock() - doReturn(None(TranslationInProgress(""))).`when`(appState).snackbarState - doReturn(appState).`when`(appStore).state - - val binding = TranslationsBinding( - browserStore = browserStore, - browserScreenStore = browserScreenStore, - appStore = appStore, - onTranslationStatusUpdate = onTranslationsActionUpdated, - onShowTranslationsDialog = {}, - ) - binding.start() - - browserStore.dispatch( - TranslationsAction.TranslateExpectedAction( - tabId = tabId, - ), - ) - - verify(onTranslationsActionUpdated).invoke(expectedTranslationStatus) - verify(browserScreenStore).dispatch( - PageTranslationStatusUpdated(expectedTranslationStatus), - ) - verify(appStore, atLeast(1)).dispatch(SnackbarAction.SnackbarDismissed) - } + val binding = TranslationsBinding( + browserStore = browserStore, + browserScreenStore = browserScreenStore, + appStore = appStore, + onTranslationStatusUpdate = onTranslationsActionUpdated, + onShowTranslationsDialog = {}, + mainDispatcher = testDispatcher, + ) + binding.start() + testDispatcher.scheduler.advanceUntilIdle() + + val detectedLanguages = DetectedLanguages( + documentLangTag = englishLanguage.code, + supportedDocumentLang = true, + userPreferredLangTag = spanishLanguage.code, + ) + + val translationEngineState = TranslationEngineState( + detectedLanguages = detectedLanguages, + error = null, + isEngineReady = true, + hasVisibleChange = true, + requestedTranslationPair = TranslationPair( + fromLanguage = englishLanguage.code, + toLanguage = spanishLanguage.code, + ), + ) - @Test - fun `GIVEN translationState WHEN translation status is not isExpectedTranslate or isTranslated THEN inform about translation changes`() { - val expectedTranslationStatus = PageTranslationStatus( - isTranslationPossible = false, - isTranslated = false, - isTranslateProcessing = false, - ) - browserStore = BrowserStore( - BrowserState( - tabs = listOf(tab), - selectedTabId = tabId, - ), - ) - val appState: AppState = mock() - doReturn(None(TranslationInProgress(""))).`when`(appState).snackbarState - doReturn(appState).`when`(appStore).state - - val binding = TranslationsBinding( - browserStore = browserStore, - browserScreenStore = browserScreenStore, - appStore = appStore, - onTranslationStatusUpdate = onTranslationsActionUpdated, - onShowTranslationsDialog = {}, - ) - binding.start() - - verify(onTranslationsActionUpdated).invoke(expectedTranslationStatus) - verify(browserScreenStore).dispatch( - PageTranslationStatusUpdated(expectedTranslationStatus), - ) - verify(appStore).dispatch(SnackbarAction.SnackbarDismissed) - } + val supportLanguages = TranslationSupport( + fromLanguages = listOf(englishLanguage), + toLanguages = listOf(spanishLanguage), + ) - @Test - fun `GIVEN translationState WHEN translation state isOfferTranslate is true THEN offer to translate the current page`() { - browserStore = BrowserStore( - BrowserState( - tabs = listOf(tab), - selectedTabId = tabId, - translationEngine = TranslationsBrowserState(isEngineSupported = true), - ), - ) - - val binding = TranslationsBinding( - browserStore = browserStore, - onTranslationStatusUpdate = onTranslationsActionUpdated, - onShowTranslationsDialog = onShowTranslationsDialog, - ) - binding.start() - - browserStore.dispatch( - TranslationsAction.TranslateOfferAction( - tabId = tab.id, - isOfferTranslate = true, - ), - ) - - verify(onShowTranslationsDialog).invoke() - } + browserStore.dispatch( + TranslationsAction.SetSupportedLanguagesAction( + supportedLanguages = supportLanguages, + ), + ) - @Test - fun `GIVEN store dependencies set WHEN translation state isOfferTranslate is true THEN offer to translate the current page`() { - val currentDestination: NavDestination = mock { - doReturn(R.id.browserFragment).`when`(this).id - } - val navController: NavController = mock { - doReturn(currentDestination).`when`(this).currentDestination + browserStore.dispatch( + TranslationsAction.TranslateStateChangeAction( + tabId = tabId, + translationEngineState = translationEngineState, + ), + ) + + browserStore.dispatch( + TranslationsAction.TranslateAction( + tabId = tab.id, + fromLanguage = englishLanguage.code, + toLanguage = spanishLanguage.code, + options = null, + ), + ) + testDispatcher.scheduler.advanceUntilIdle() + + verify(onTranslationsActionUpdated).invoke(expectedTranslationStatus) + verify(browserScreenStore).dispatch( + PageTranslationStatusUpdated(expectedTranslationStatus), + ) } - val expectedNavigation = BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment() - browserStore = BrowserStore( - BrowserState( - tabs = listOf(tab), - selectedTabId = tabId, - translationEngine = TranslationsBrowserState(isEngineSupported = true), - ), - ) - - val binding = spy( - TranslationsBinding( + + @Test + fun `GIVEN translationState WHEN translation status isExpectedTranslate THEN inform about translation changes`() = + runTest { + val expectedTranslationStatus = PageTranslationStatus( + isTranslationPossible = true, + isTranslated = false, + isTranslateProcessing = false, + ) + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + translationEngine = TranslationsBrowserState(isEngineSupported = true), + ), + ) + val appState: AppState = mock() + doReturn(None(TranslationInProgress(""))).`when`(appState).snackbarState + doReturn(appState).`when`(appStore).state + + val binding = TranslationsBinding( browserStore = browserStore, browserScreenStore = browserScreenStore, appStore = appStore, - navController = navController, onTranslationStatusUpdate = onTranslationsActionUpdated, - onShowTranslationsDialog = onShowTranslationsDialog, - ), - ) - binding.start() - - browserStore.dispatch( - TranslationsAction.TranslateOfferAction( - tabId = tab.id, - isOfferTranslate = true, - ), - ) - - verify(onShowTranslationsDialog, never()).invoke() - verify(binding).recordTranslationStartTelemetry() - verify(appStore, atLeast(1)).dispatch(SnackbarAction.SnackbarDismissed) - verify(navController).navigate(expectedNavigation) - } + onShowTranslationsDialog = {}, + mainDispatcher = testDispatcher, + ) + binding.start() + testDispatcher.scheduler.advanceUntilIdle() + + browserStore.dispatch( + TranslationsAction.TranslateExpectedAction( + tabId = tabId, + ), + ) + testDispatcher.scheduler.advanceUntilIdle() + + verify(onTranslationsActionUpdated).invoke(expectedTranslationStatus) + verify(browserScreenStore).dispatch( + PageTranslationStatusUpdated(expectedTranslationStatus), + ) + verify(appStore, atLeast(1)).dispatch(SnackbarAction.SnackbarDismissed) + } @Test - fun `GIVEN translationState WHEN readerState is active THEN inform about translation changes`() { - val expectedTranslationStatus = PageTranslationStatus( - isTranslationPossible = false, - isTranslated = false, - isTranslateProcessing = false, - ) - val tabReaderStateActive = createTab( - "https://www.firefox.com", - id = "test-tab", - readerState = ReaderState(active = true), - ) - browserStore = BrowserStore( - BrowserState( - tabs = listOf(tabReaderStateActive), - selectedTabId = tabReaderStateActive.id, - ), - ) - - val binding = TranslationsBinding( - browserStore = browserStore, - browserScreenStore = browserScreenStore, - appStore = appStore, - onTranslationStatusUpdate = onTranslationsActionUpdated, - onShowTranslationsDialog = onShowTranslationsDialog, - ) - binding.start() - - verify(onTranslationsActionUpdated).invoke(expectedTranslationStatus) - } + fun `GIVEN translationState WHEN translation status is not isExpectedTranslate or isTranslated THEN inform about translation changes`() = + runTest { + val expectedTranslationStatus = PageTranslationStatus( + isTranslationPossible = false, + isTranslated = false, + isTranslateProcessing = false, + ) + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + ), + ) + val appState: AppState = mock() + doReturn(None(TranslationInProgress(""))).`when`(appState).snackbarState + doReturn(appState).`when`(appStore).state + + val binding = TranslationsBinding( + browserStore = browserStore, + browserScreenStore = browserScreenStore, + appStore = appStore, + onTranslationStatusUpdate = onTranslationsActionUpdated, + onShowTranslationsDialog = {}, + mainDispatcher = testDispatcher, + ) + binding.start() + testDispatcher.scheduler.advanceUntilIdle() + + verify(onTranslationsActionUpdated).invoke(expectedTranslationStatus) + verify(browserScreenStore).dispatch( + PageTranslationStatusUpdated(expectedTranslationStatus), + ) + verify(appStore).dispatch(SnackbarAction.SnackbarDismissed) + } @Test - fun `GIVEN translationState WHEN translation state isOfferTranslate is false THEN don't offer to translate the current page`() { - browserStore = BrowserStore( - BrowserState( - tabs = listOf(tab), - selectedTabId = tabId, - translationEngine = TranslationsBrowserState(isEngineSupported = true), - ), - ) - - val binding = spy( - TranslationsBinding( + fun `GIVEN translationState WHEN translation state isOfferTranslate is true THEN offer to translate the current page`() = + runTest { + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + translationEngine = TranslationsBrowserState(isEngineSupported = true), + ), + ) + + val binding = TranslationsBinding( browserStore = browserStore, onTranslationStatusUpdate = onTranslationsActionUpdated, onShowTranslationsDialog = onShowTranslationsDialog, - ), - ) - binding.start() - - browserStore.dispatch( - TranslationsAction.TranslateOfferAction( - tabId = tab.id, - isOfferTranslate = false, - ), - ) - - verify(onShowTranslationsDialog, never()).invoke() - verify(binding, never()).recordTranslationStartTelemetry() - verify(appStore, never()).dispatch(SnackbarAction.SnackbarDismissed) - } + mainDispatcher = testDispatcher, + ) + binding.start() + testDispatcher.scheduler.advanceUntilIdle() + + browserStore.dispatch( + TranslationsAction.TranslateOfferAction( + tabId = tab.id, + isOfferTranslate = true, + ), + ) + testDispatcher.scheduler.advanceUntilIdle() + + verify(onShowTranslationsDialog).invoke() + } + + @Test + fun `GIVEN store dependencies set WHEN translation state isOfferTranslate is true THEN offer to translate the current page`() = + runTest { + val currentDestination: NavDestination = mock { + doReturn(R.id.browserFragment).`when`(this).id + } + val navController: NavController = mock { + doReturn(currentDestination).`when`(this).currentDestination + } + val expectedNavigation = + BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment() + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + translationEngine = TranslationsBrowserState(isEngineSupported = true), + ), + ) + + val binding = spy( + TranslationsBinding( + browserStore = browserStore, + browserScreenStore = browserScreenStore, + appStore = appStore, + navController = navController, + onTranslationStatusUpdate = onTranslationsActionUpdated, + onShowTranslationsDialog = onShowTranslationsDialog, + mainDispatcher = testDispatcher, + ), + ) + binding.start() + testDispatcher.scheduler.advanceUntilIdle() + + browserStore.dispatch( + TranslationsAction.TranslateOfferAction( + tabId = tab.id, + isOfferTranslate = true, + ), + ) + testDispatcher.scheduler.advanceUntilIdle() + + verify(onShowTranslationsDialog, never()).invoke() + verify(binding).recordTranslationStartTelemetry() + verify(appStore, atLeast(1)).dispatch(SnackbarAction.SnackbarDismissed) + verify(navController).navigate(expectedNavigation) + } @Test - fun `GIVEN translationState WHEN translation state has an error THEN don't offer to translate the current page`() { - browserStore = BrowserStore( - BrowserState( - tabs = listOf(tab), - selectedTabId = tabId, - translationEngine = TranslationsBrowserState( - isEngineSupported = true, + fun `GIVEN translationState WHEN readerState is active THEN inform about translation changes`() = + runTest { + val expectedTranslationStatus = PageTranslationStatus( + isTranslationPossible = false, + isTranslated = false, + isTranslateProcessing = false, + ) + val tabReaderStateActive = createTab( + "https://www.firefox.com", + id = "test-tab", + readerState = ReaderState(active = true), + ) + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tabReaderStateActive), + selectedTabId = tabReaderStateActive.id, ), - ), - ) + ) - val binding = spy( - TranslationsBinding( + val binding = TranslationsBinding( browserStore = browserStore, + browserScreenStore = browserScreenStore, + appStore = appStore, onTranslationStatusUpdate = onTranslationsActionUpdated, onShowTranslationsDialog = onShowTranslationsDialog, - ), - ) - binding.start() - - browserStore.dispatch( - TranslationsAction.TranslateExpectedAction( - tabId = tabId, - ), - ) - - browserStore.dispatch( - TranslationsAction.TranslateOfferAction( - tabId = tab.id, - isOfferTranslate = false, - ), - ) - - browserStore.dispatch( - TranslationsAction.TranslateExceptionAction( - tabId, - TranslationOperation.TRANSLATE, - TranslationError.CouldNotTranslateError(null), - ), - ) - - verify(onShowTranslationsDialog).invoke() - verify(binding, never()).recordTranslationStartTelemetry() - verify(onShowTranslationsDialog).invoke() - verify(appStore, never()).dispatch(SnackbarAction.SnackbarDismissed) - } + mainDispatcher = testDispatcher, + ) + binding.start() + testDispatcher.scheduler.advanceUntilIdle() + + verify(onTranslationsActionUpdated).invoke(expectedTranslationStatus) + } + + @Test + fun `GIVEN translationState WHEN translation state isOfferTranslate is false THEN don't offer to translate the current page`() = + runTest { + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + translationEngine = TranslationsBrowserState(isEngineSupported = true), + ), + ) + + val binding = spy( + TranslationsBinding( + browserStore = browserStore, + onTranslationStatusUpdate = onTranslationsActionUpdated, + onShowTranslationsDialog = onShowTranslationsDialog, + mainDispatcher = testDispatcher, + ), + ) + binding.start() + testDispatcher.scheduler.advanceUntilIdle() + + browserStore.dispatch( + TranslationsAction.TranslateOfferAction( + tabId = tab.id, + isOfferTranslate = false, + ), + ) + testDispatcher.scheduler.advanceUntilIdle() + + verify(onShowTranslationsDialog, never()).invoke() + verify(binding, never()).recordTranslationStartTelemetry() + verify(appStore, never()).dispatch(SnackbarAction.SnackbarDismissed) + } + + @Test + fun `GIVEN translationState WHEN translation state has an error THEN don't offer to translate the current page`() = + runTest { + browserStore = BrowserStore( + BrowserState( + tabs = listOf(tab), + selectedTabId = tabId, + translationEngine = TranslationsBrowserState( + isEngineSupported = true, + ), + ), + ) + + val binding = spy( + TranslationsBinding( + browserStore = browserStore, + onTranslationStatusUpdate = onTranslationsActionUpdated, + onShowTranslationsDialog = onShowTranslationsDialog, + mainDispatcher = testDispatcher, + ), + ) + binding.start() + testDispatcher.scheduler.advanceUntilIdle() + + browserStore.dispatch( + TranslationsAction.TranslateExpectedAction( + tabId = tabId, + ), + ) + + browserStore.dispatch( + TranslationsAction.TranslateOfferAction( + tabId = tab.id, + isOfferTranslate = false, + ), + ) + + browserStore.dispatch( + TranslationsAction.TranslateExceptionAction( + tabId, + TranslationOperation.TRANSLATE, + TranslationError.CouldNotTranslateError(null), + ), + ) + testDispatcher.scheduler.advanceUntilIdle() + + verify(onShowTranslationsDialog).invoke() + verify(binding, never()).recordTranslationStartTelemetry() + verify(onShowTranslationsDialog).invoke() + verify(appStore, never()).dispatch(SnackbarAction.SnackbarDismissed) + } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/menu/WebExtensionsMenuBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/menu/WebExtensionsMenuBindingTest.kt @@ -5,6 +5,9 @@ package org.mozilla.fenix.components.menu import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.WebExtensionState import mozilla.components.browser.state.state.createTab @@ -15,12 +18,9 @@ import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.spy @@ -29,17 +29,17 @@ import org.mozilla.fenix.components.menu.store.MenuAction import org.mozilla.fenix.components.menu.store.MenuState import org.mozilla.fenix.components.menu.store.MenuStore +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class WebExtensionsMenuBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() lateinit var browserStore: BrowserStore private lateinit var menuStore: MenuStore @Test fun `WHEN browser web extension state get updated in the browserStore THEN invoke action update browser web extension menu items`() = - runTestOnMain { + runTest { val defaultBrowserAction = createWebExtensionBrowserAction("default_browser_action_title") @@ -86,8 +86,10 @@ class WebExtensionsMenuBindingTest { menuStore = menuStore, iconSize = 24.dpToPx(testContext.resources.displayMetrics), onDismiss = {}, + mainDispatcher = testDispatcher, ) binding.start() + testDispatcher.scheduler.advanceUntilIdle() val browserItemsUpdateCaptor = argumentCaptor<MenuAction.UpdateWebExtensionBrowserMenuItems>() @@ -104,7 +106,7 @@ class WebExtensionsMenuBindingTest { @Test fun `WHEN all web extensions are disabled THEN show disabled extensions onboarding`() = - runTestOnMain { + runTest { val extensions: Map<String, WebExtensionState> = mapOf( "id" to WebExtensionState( id = "id", @@ -135,8 +137,10 @@ class WebExtensionsMenuBindingTest { menuStore = menuStore, iconSize = 24.dpToPx(testContext.resources.displayMetrics), onDismiss = {}, + mainDispatcher = testDispatcher, ) binding.start() + testDispatcher.scheduler.advanceUntilIdle() val showDisabledExtensionsOnboardingCaptor = argumentCaptor<MenuAction.UpdateShowDisabledExtensionsOnboarding>() @@ -147,7 +151,7 @@ class WebExtensionsMenuBindingTest { @Test fun `WHEN only one web extension is disabled THEN not show disabled extensions onboarding`() = - runTestOnMain { + runTest { val extensions: Map<String, WebExtensionState> = mapOf( "id" to WebExtensionState( id = "id", @@ -184,8 +188,10 @@ class WebExtensionsMenuBindingTest { menuStore = menuStore, iconSize = 24.dpToPx(testContext.resources.displayMetrics), onDismiss = {}, + mainDispatcher = testDispatcher, ) binding.start() + testDispatcher.scheduler.advanceUntilIdle() val showDisabledExtensionsOnboardingCaptor = argumentCaptor<MenuAction.UpdateShowDisabledExtensionsOnboarding>() @@ -196,7 +202,7 @@ class WebExtensionsMenuBindingTest { @Test fun `WHEN page web extension state get updated in the browserStore THEN invoke action update page web extension menu items`() = - runTestOnMain { + runTest { val defaultPageAction = createWebExtensionPageAction("default_page_action_title") val overriddenPageAction = createWebExtensionPageAction("overridden_page_action_title") @@ -241,8 +247,10 @@ class WebExtensionsMenuBindingTest { menuStore = menuStore, iconSize = 24.dpToPx(testContext.resources.displayMetrics), onDismiss = {}, + mainDispatcher = testDispatcher, ) binding.start() + testDispatcher.scheduler.advanceUntilIdle() val pageItemsUpdateCaptor = argumentCaptor<MenuAction.UpdateWebExtensionBrowserMenuItems>() @@ -259,7 +267,7 @@ class WebExtensionsMenuBindingTest { @Test fun `WHEN page web extension state disabled get updated in the browserStore THEN not invoke action update page web extension menu items`() = - runTestOnMain { + runTest { val defaultPageAction = createWebExtensionPageAction("default_page_action_title", enabled = false) @@ -294,8 +302,10 @@ class WebExtensionsMenuBindingTest { menuStore = menuStore, iconSize = 24.dpToPx(testContext.resources.displayMetrics), onDismiss = {}, + mainDispatcher = testDispatcher, ) binding.start() + testDispatcher.scheduler.advanceUntilIdle() val pageItemsUpdateCaptor = argumentCaptor<MenuAction.UpdateWebExtensionBrowserMenuItems>() @@ -311,12 +321,12 @@ class WebExtensionsMenuBindingTest { WebExtensionPageAction( title = title, enabled = enabled, - loadIcon = mock(), - badgeText = "", - badgeTextColor = 0, - badgeBackgroundColor = 0, - onClick = {}, - ) + loadIcon = mock(), + badgeText = "", + badgeTextColor = 0, + badgeBackgroundColor = 0, + onClick = {}, + ) private fun createWebExtensionBrowserAction(title: String) = WebExtensionBrowserAction( title, diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/crashes/CrashReporterBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/crashes/CrashReporterBindingTest.kt @@ -7,13 +7,12 @@ package org.mozilla.fenix.crashes import android.content.Context import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.mockk +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.lib.crash.store.CrashAction -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.components.AppStore @@ -21,11 +20,11 @@ import org.mozilla.fenix.components.appstate.AppAction @RunWith(AndroidJUnit4::class) class CrashReporterBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() + + private val testDispatcher = StandardTestDispatcher() @Test - fun `GIVEN CrashAction ShowPrompt WHEN an action is dispatched THEN CrashReporterBinding is called with null crashIDs`() = runTestOnMain { + fun `GIVEN CrashAction ShowPrompt WHEN an action is dispatched THEN CrashReporterBinding is called with null crashIDs`() = runTest(testDispatcher) { val appStore = AppStore() var onReportingCalled = false val binding = CrashReporterBinding( @@ -35,15 +34,18 @@ class CrashReporterBindingTest { assertEquals(listOf<String>(), crashIDs) onReportingCalled = true }, + mainDispatcher = testDispatcher, ) binding.start() appStore.dispatch(AppAction.CrashActionWrapper(CrashAction.ShowPrompt())) + testDispatcher.scheduler.advanceUntilIdle() + assertTrue(onReportingCalled) } @Test - fun `GIVEN CrashAction PullCrashes WHEN an action is dispatched THEN CrashReporterBinding is called with non null crashIDs`() = runTestOnMain { + fun `GIVEN CrashAction PullCrashes WHEN an action is dispatched THEN CrashReporterBinding is called with non null crashIDs`() = runTest(testDispatcher) { val appStore = AppStore() var onReportingCalled = false val binding = CrashReporterBinding( @@ -54,10 +56,13 @@ class CrashReporterBindingTest { assertEquals(listOf("1", "2"), crashIDs) onReportingCalled = true }, + mainDispatcher = testDispatcher, ) binding.start() appStore.dispatch(AppAction.CrashActionWrapper(CrashAction.ShowPrompt(listOf("1", "2")))) + testDispatcher.scheduler.advanceUntilIdle() + assertTrue(onReportingCalled) } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/RecentTabsListFeatureTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/home/RecentTabsListFeatureTest.kt @@ -5,6 +5,8 @@ package org.mozilla.fenix.home import io.mockk.mockk +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.ContentAction.UpdateIconAction import mozilla.components.browser.state.action.ContentAction.UpdateTitleAction import mozilla.components.browser.state.action.MediaSessionAction @@ -16,7 +18,6 @@ import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.mediasession.MediaSession import mozilla.components.feature.media.middleware.LastMediaAccessMiddleware import mozilla.components.support.test.middleware.CaptureActionsMiddleware -import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull @@ -24,7 +25,6 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Ignore -import org.junit.Rule import org.junit.Test import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction @@ -34,12 +34,10 @@ import org.mozilla.fenix.home.recenttabs.RecentTabsListFeature class RecentTabsListFeatureTest { + private val testDispatcher = StandardTestDispatcher() private lateinit var appStore: AppStore private lateinit var middleware: CaptureActionsMiddleware<AppState, AppAction> - @get:Rule - val coroutinesTestRule = MainCoroutineRule() - @Before fun setup() { middleware = CaptureActionsMiddleware() @@ -52,309 +50,350 @@ class RecentTabsListFeatureTest { } @Test - fun `GIVEN no selected, last active or in progress media tab WHEN the feature starts THEN dispatch an empty list`() { - val browserStore = BrowserStore() - val appStore = AppStore() - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - - assertEquals(0, appStore.state.recentTabs.size) - } + fun `GIVEN no selected, last active or in progress media tab WHEN the feature starts THEN dispatch an empty list`() = + runTest(testDispatcher) { + val browserStore = BrowserStore() + val appStore = AppStore() + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals(0, appStore.state.recentTabs.size) + } @Test - fun `GIVEN no selected but last active tab available WHEN the feature starts THEN dispatch the last active tab as a recent tab list`() { - val tab = createTab( - url = "https://www.mozilla.org", - id = "1", - ) - val tabs = listOf(tab) - val browserStore = BrowserStore( - BrowserState(tabs = tabs), - ) - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - - assertEquals(1, appStore.state.recentTabs.size) - } + fun `GIVEN no selected but last active tab available WHEN the feature starts THEN dispatch the last active tab as a recent tab list`() = + runTest(testDispatcher) { + val tab = createTab( + url = "https://www.mozilla.org", + id = "1", + ) + val tabs = listOf(tab) + val browserStore = BrowserStore( + BrowserState(tabs = tabs), + ) + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(1, appStore.state.recentTabs.size) + } @Test - fun `GIVEN a selected tab WHEN the feature starts THEN dispatch the selected tab as a recent tab list`() { - val tab = createTab( - url = "https://www.mozilla.org", - id = "1", - ) - val tabs = listOf(tab) - val browserStore = BrowserStore( - BrowserState( - tabs = tabs, - selectedTabId = "1", - ), - ) - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - - assertEquals(1, appStore.state.recentTabs.size) - } + fun `GIVEN a selected tab WHEN the feature starts THEN dispatch the selected tab as a recent tab list`() = + runTest(testDispatcher) { + val tab = createTab( + url = "https://www.mozilla.org", + id = "1", + ) + val tabs = listOf(tab) + val browserStore = BrowserStore( + BrowserState( + tabs = tabs, + selectedTabId = "1", + ), + ) + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(1, appStore.state.recentTabs.size) + } @Ignore("Disabled until we want to enable this feature. See #21670.") @Test - fun `GIVEN a valid inProgressMediaTabId and another selected tab exists WHEN the feature starts THEN dispatch both as as a recent tabs list`() { - val mediaTab = createTab( - url = "https://mozilla.com", - id = "42", - lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 123), - ) - val selectedTab = createTab("https://mozilla.com", id = "43") - val browserStore = BrowserStore( - BrowserState( - tabs = listOf(mediaTab, selectedTab), - selectedTabId = "43", - ), - ) - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - - assertEquals(2, appStore.state.recentTabs.size) - assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) - assertEquals(selectedTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) - assertTrue(appStore.state.recentTabs[1] is RecentTab.Tab) - assertEquals(mediaTab, (appStore.state.recentTabs[1] as RecentTab.Tab).state) - } + fun `GIVEN a valid inProgressMediaTabId and another selected tab exists WHEN the feature starts THEN dispatch both as as a recent tabs list`() = + runTest(testDispatcher) { + val mediaTab = createTab( + url = "https://mozilla.com", + id = "42", + lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 123), + ) + val selectedTab = createTab("https://mozilla.com", id = "43") + val browserStore = BrowserStore( + BrowserState( + tabs = listOf(mediaTab, selectedTab), + selectedTabId = "43", + ), + ) + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals(2, appStore.state.recentTabs.size) + assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) + assertEquals(selectedTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) + assertTrue(appStore.state.recentTabs[1] is RecentTab.Tab) + assertEquals(mediaTab, (appStore.state.recentTabs[1] as RecentTab.Tab).state) + } @Ignore("Disabled until we want to enable this feature. See #21670.") @Test - fun `GIVEN a valid inProgressMediaTabId exists and that is the selected tab WHEN the feature starts THEN dispatch just one tab as the recent tabs list`() { - val selectedMediaTab = createTab( - "https://mozilla.com", - id = "42", - lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 123), - ) - val browserStore = BrowserStore( - BrowserState( - tabs = listOf(selectedMediaTab), - selectedTabId = "42", - ), - ) - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - - assertEquals(1, appStore.state.recentTabs.size) - assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) - assertEquals(selectedMediaTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) - } + fun `GIVEN a valid inProgressMediaTabId exists and that is the selected tab WHEN the feature starts THEN dispatch just one tab as the recent tabs list`() = + runTest(testDispatcher) { + val selectedMediaTab = createTab( + "https://mozilla.com", + id = "42", + lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 123), + ) + val browserStore = BrowserStore( + BrowserState( + tabs = listOf(selectedMediaTab), + selectedTabId = "42", + ), + ) + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + + assertEquals(1, appStore.state.recentTabs.size) + assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) + assertEquals(selectedMediaTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) + } @Test - fun `WHEN the browser state has an updated select tab THEN dispatch the new recent tab list`() { - val tab1 = createTab( - url = "https://www.mozilla.org", - id = "1", - ) - val tab2 = createTab( - url = "https://www.firefox.com", - id = "2", - ) - val tabs = listOf(tab1, tab2) - val browserStore = BrowserStore( - BrowserState( - tabs = tabs, - selectedTabId = "1", - ), - ) - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - - assertEquals(1, appStore.state.recentTabs.size) - assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) - assertEquals(tab1, (appStore.state.recentTabs[0] as RecentTab.Tab).state) - - browserStore.dispatch(TabListAction.SelectTabAction(tab2.id)) - - assertEquals(1, appStore.state.recentTabs.size) - assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) - assertEquals(tab2, (appStore.state.recentTabs[0] as RecentTab.Tab).state) - } + fun `WHEN the browser state has an updated select tab THEN dispatch the new recent tab list`() = + runTest(testDispatcher) { + val tab1 = createTab( + url = "https://www.mozilla.org", + id = "1", + ) + val tab2 = createTab( + url = "https://www.firefox.com", + id = "2", + ) + val tabs = listOf(tab1, tab2) + val browserStore = BrowserStore( + BrowserState( + tabs = tabs, + selectedTabId = "1", + ), + ) + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(1, appStore.state.recentTabs.size) + assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) + assertEquals(tab1, (appStore.state.recentTabs[0] as RecentTab.Tab).state) + + browserStore.dispatch(TabListAction.SelectTabAction(tab2.id)) + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(1, appStore.state.recentTabs.size) + assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) + assertEquals(tab2, (appStore.state.recentTabs[0] as RecentTab.Tab).state) + } @Ignore("Disabled until we want to enable this feature. See #21670.") @Test - fun `WHEN the browser state has an in progress media tab THEN dispatch the new recent tab list`() { - val initialMediaTab = createTab( - url = "https://mozilla.com", - id = "1", - lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 123), - ) - val newMediaTab = createTab( - url = "http://mozilla.org", - id = "2", - lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 100), - ) - val browserStore = BrowserStore( - initialState = BrowserState( - tabs = listOf(initialMediaTab, newMediaTab), - selectedTabId = "1", - ), - middleware = listOf(LastMediaAccessMiddleware()), - ) - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - assertEquals(2, appStore.state.recentTabs.size) - assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) - assertEquals(initialMediaTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) - - browserStore.dispatch( - MediaSessionAction.UpdateMediaPlaybackStateAction("2", MediaSession.PlaybackState.PLAYING), - ) - assertEquals(2, appStore.state.recentTabs.size) - assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) - assertEquals(initialMediaTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) - // UpdateMediaPlaybackStateAction would set the current timestamp as the new value for lastMediaAccess - val updatedLastMediaAccess = - (appStore.state.recentTabs[1] as RecentTab.Tab).state.lastMediaAccessState.lastMediaAccess - assertTrue("expected lastMediaAccess ($updatedLastMediaAccess) > 100", updatedLastMediaAccess > 100) - assertEquals( - "http://mozilla.org", - (appStore.state.recentTabs[1] as RecentTab.Tab).state.lastMediaAccessState.lastMediaUrl, - ) - // Check that the media tab is updated ignoring just the lastMediaAccess property. - assertEquals( - newMediaTab, - (appStore.state.recentTabs[1] as RecentTab.Tab).state.copy( + fun `WHEN the browser state has an in progress media tab THEN dispatch the new recent tab list`() = + runTest(testDispatcher) { + val initialMediaTab = createTab( + url = "https://mozilla.com", + id = "1", + lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 123), + ) + val newMediaTab = createTab( + url = "http://mozilla.org", + id = "2", lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 100), - ), - ) - } + ) + val browserStore = BrowserStore( + initialState = BrowserState( + tabs = listOf(initialMediaTab, newMediaTab), + selectedTabId = "1", + ), + middleware = listOf(LastMediaAccessMiddleware()), + ) + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(2, appStore.state.recentTabs.size) + assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) + assertEquals(initialMediaTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) + + browserStore.dispatch( + MediaSessionAction.UpdateMediaPlaybackStateAction( + "2", + MediaSession.PlaybackState.PLAYING, + ), + ) + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(2, appStore.state.recentTabs.size) + assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) + assertEquals(initialMediaTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) + // UpdateMediaPlaybackStateAction would set the current timestamp as the new value for lastMediaAccess + val updatedLastMediaAccess = + (appStore.state.recentTabs[1] as RecentTab.Tab).state.lastMediaAccessState.lastMediaAccess + assertTrue( + "expected lastMediaAccess ($updatedLastMediaAccess) > 100", + updatedLastMediaAccess > 100, + ) + assertEquals( + "http://mozilla.org", + (appStore.state.recentTabs[1] as RecentTab.Tab).state.lastMediaAccessState.lastMediaUrl, + ) + // Check that the media tab is updated ignoring just the lastMediaAccess property. + assertEquals( + newMediaTab, + (appStore.state.recentTabs[1] as RecentTab.Tab).state.copy( + lastMediaAccessState = LastMediaAccessState("https://mozilla.com", 100), + ), + ) + } @Test - fun `WHEN the browser state selects a private tab THEN dispatch an empty list`() { - val selectedNormalTab = createTab( - url = "https://www.mozilla.org", - id = "1", - lastAccess = 0, - ) - val lastAccessedNormalTab = createTab( - url = "https://www.mozilla.org", - id = "2", - lastAccess = 1, - ) - val privateTab = createTab( - url = "https://www.firefox.com", - id = "3", - private = true, - ) - val tabs = listOf(selectedNormalTab, lastAccessedNormalTab, privateTab) - val browserStore = BrowserStore( - BrowserState( - tabs = tabs, - selectedTabId = "1", - ), - ) - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - - assertEquals(1, appStore.state.recentTabs.size) - assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) - assertEquals(selectedNormalTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) - - browserStore.dispatch(TabListAction.SelectTabAction(privateTab.id)) - - // If the selected tab is a private tab the feature should show the last accessed normal tab. - assertEquals(1, appStore.state.recentTabs.size) - assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) - assertEquals(lastAccessedNormalTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) - } + fun `WHEN the browser state selects a private tab THEN dispatch an empty list`() = + runTest(testDispatcher) { + val selectedNormalTab = createTab( + url = "https://www.mozilla.org", + id = "1", + lastAccess = 0, + ) + val lastAccessedNormalTab = createTab( + url = "https://www.mozilla.org", + id = "2", + lastAccess = 1, + ) + val privateTab = createTab( + url = "https://www.firefox.com", + id = "3", + private = true, + ) + val tabs = listOf(selectedNormalTab, lastAccessedNormalTab, privateTab) + val browserStore = BrowserStore( + BrowserState( + tabs = tabs, + selectedTabId = "1", + ), + ) + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(1, appStore.state.recentTabs.size) + assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) + assertEquals(selectedNormalTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) + + browserStore.dispatch(TabListAction.SelectTabAction(privateTab.id)) + testDispatcher.scheduler.advanceUntilIdle() + + // If the selected tab is a private tab the feature should show the last accessed normal tab. + assertEquals(1, appStore.state.recentTabs.size) + assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) + assertEquals( + lastAccessedNormalTab, + (appStore.state.recentTabs[0] as RecentTab.Tab).state, + ) + } @Test - fun `WHEN the selected tabs title or icon update THEN update the home store`() { - val browserStore = BrowserStore( - BrowserState( - tabs = listOf( - createTab( - url = "https://www.mozilla.org", - id = "1", + fun `WHEN the selected tabs title or icon update THEN update the home store`() = + runTest(testDispatcher) { + val browserStore = BrowserStore( + BrowserState( + tabs = listOf( + createTab( + url = "https://www.mozilla.org", + id = "1", + ), ), + selectedTabId = "1", ), - selectedTabId = "1", - ), - ) - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - - middleware.assertLastAction(AppAction.RecentTabsChange::class) { - val tab = it.recentTabs.first() as RecentTab.Tab - assertTrue(tab.state.content.title.isEmpty()) - assertNull(tab.state.content.icon) - } - - browserStore.dispatch(UpdateTitleAction("1", "test")) - - middleware.assertLastAction(AppAction.RecentTabsChange::class) { - val tab = it.recentTabs.first() as RecentTab.Tab - assertEquals("test", tab.state.content.title) - assertNull(tab.state.content.icon) + ) + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + middleware.assertLastAction(AppAction.RecentTabsChange::class) { + val tab = it.recentTabs.first() as RecentTab.Tab + assertTrue(tab.state.content.title.isEmpty()) + assertNull(tab.state.content.icon) + } + + browserStore.dispatch(UpdateTitleAction("1", "test")) + testDispatcher.scheduler.advanceUntilIdle() + middleware.assertLastAction(AppAction.RecentTabsChange::class) { + val tab = it.recentTabs.first() as RecentTab.Tab + assertEquals("test", tab.state.content.title) + assertNull(tab.state.content.icon) + } + + browserStore.dispatch(UpdateIconAction("1", "https://www.mozilla.org", mockk())) + + testDispatcher.scheduler.advanceUntilIdle() + middleware.assertLastAction(AppAction.RecentTabsChange::class) { + val tab = it.recentTabs.first() as RecentTab.Tab + assertEquals("test", tab.state.content.title) + assertNotNull(tab.state.content.icon) + } } - browserStore.dispatch(UpdateIconAction("1", "https://www.mozilla.org", mockk())) - - middleware.assertLastAction(AppAction.RecentTabsChange::class) { - val tab = it.recentTabs.first() as RecentTab.Tab - assertEquals("test", tab.state.content.title) - assertNotNull(tab.state.content.icon) - } - } - @Test - fun `GIVEN inProgressMediaTab already set WHEN the media tab is closed THEN remove it from recent tabs`() { - val initialMediaTab = createTab(url = "https://mozilla.com", id = "1") - val selectedTab = createTab(url = "https://mozilla.com/firefox", id = "2") - val browserStore = BrowserStore( - initialState = BrowserState(listOf(initialMediaTab, selectedTab), selectedTabId = "2"), - middleware = listOf(LastMediaAccessMiddleware()), - ) - val feature = RecentTabsListFeature( - browserStore = browserStore, - appStore = appStore, - ) - - feature.start() - browserStore.dispatch(TabListAction.RemoveTabsAction(listOf("1"))) - - assertEquals(1, appStore.state.recentTabs.size) - assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) - assertEquals(selectedTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) - } + fun `GIVEN inProgressMediaTab already set WHEN the media tab is closed THEN remove it from recent tabs`() = + runTest(testDispatcher) { + val initialMediaTab = createTab(url = "https://mozilla.com", id = "1") + val selectedTab = createTab(url = "https://mozilla.com/firefox", id = "2") + val browserStore = BrowserStore( + initialState = BrowserState( + listOf(initialMediaTab, selectedTab), + selectedTabId = "2", + ), + middleware = listOf(LastMediaAccessMiddleware()), + ) + val feature = RecentTabsListFeature( + browserStore = browserStore, + appStore = appStore, + mainDispatcher = testDispatcher, + ) + + feature.start() + testDispatcher.scheduler.advanceUntilIdle() + browserStore.dispatch(TabListAction.RemoveTabsAction(listOf("1"))) + testDispatcher.scheduler.advanceUntilIdle() + assertEquals(1, appStore.state.recentTabs.size) + assertTrue(appStore.state.recentTabs[0] is RecentTab.Tab) + assertEquals(selectedTab, (appStore.state.recentTabs[0] as RecentTab.Tab).state) + } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/library/history/state/bindings/MenuBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/library/history/state/bindings/MenuBindingTest.kt @@ -4,29 +4,30 @@ package org.mozilla.fenix.library.history.state.bindings -import mozilla.components.support.test.rule.MainCoroutineRule +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertTrue -import org.junit.Rule import org.junit.Test import org.mozilla.fenix.library.history.HistoryFragmentAction import org.mozilla.fenix.library.history.HistoryFragmentState import org.mozilla.fenix.library.history.HistoryFragmentStore class MenuBindingTest { - @get:Rule - val coroutinesTestRule = MainCoroutineRule() + + private val testDispatcher = StandardTestDispatcher() @Test - fun `WHEN the mode is updated THEN the menu is invalidated`() { + fun `WHEN the mode is updated THEN the menu is invalidated`() = runTest(testDispatcher) { var menuInvalidated = false val store = HistoryFragmentStore(HistoryFragmentState.initial.copy(mode = HistoryFragmentState.Mode.Syncing)) val binding = MenuBinding( store = store, invalidateOptionsMenu = { menuInvalidated = true }, + mainDispatcher = testDispatcher, ) binding.start() - store.dispatch(HistoryFragmentAction.FinishSync) + testDispatcher.scheduler.advanceUntilIdle() assertTrue(menuInvalidated) } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/reviewprompt/ShowPlayStoreReviewPromptTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/reviewprompt/ShowPlayStoreReviewPromptTest.kt @@ -9,12 +9,12 @@ import androidx.navigation.NavDirections import androidx.test.ext.junit.runners.AndroidJUnit4 import io.mockk.coVerify import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.support.test.middleware.CaptureActionsMiddleware -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.NavGraphDirections @@ -26,11 +26,11 @@ import org.mozilla.fenix.components.appstate.AppState import org.mozilla.fenix.reviewprompt.ReviewPromptState.NotEligible import java.lang.ref.WeakReference +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class ShowPlayStoreReviewPromptTest { - @get:Rule - val coroutinesTestRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() var navDirection: NavDirections? = null lateinit var promptController: PlayStoreReviewPromptController lateinit var mockActivity: Activity @@ -46,7 +46,7 @@ class ShowPlayStoreReviewPromptTest { @Test fun `GIVEN observing review prompt state WHEN eligible for custom prompt THEN custom prompt shown`() = - runTestOnMain { + runTest(testDispatcher) { val appStore = AppStore( initialState = AppState( reviewPrompt = ReviewPromptState.Eligible(ReviewPromptState.Eligible.Type.Custom), @@ -56,11 +56,13 @@ class ShowPlayStoreReviewPromptTest { appStore, promptController, activityRef, - uiScope = coroutinesTestRule.scope, + uiScope = this, { navDirection = it }, + mainDispatcher = testDispatcher, ) feature.start() + testDispatcher.scheduler.advanceUntilIdle() coVerify(exactly = 0) { promptController.tryPromptReview(mockActivity) @@ -77,7 +79,7 @@ class ShowPlayStoreReviewPromptTest { @Test fun `GIVEN observing review prompt state WHEN state is unknown THEN does nothing`() = - runTestOnMain { + runTest(testDispatcher) { val captureMiddleware = CaptureActionsMiddleware<AppState, AppAction>() val appStore = AppStore( initialState = AppState( @@ -89,11 +91,13 @@ class ShowPlayStoreReviewPromptTest { appStore, promptController, activityRef, - uiScope = coroutinesTestRule.scope, + uiScope = this, { navDirection = it }, + mainDispatcher = testDispatcher, ) feature.start() + testDispatcher.scheduler.advanceUntilIdle() coVerify(exactly = 0) { promptController.tryPromptReview(mockActivity) @@ -104,7 +108,7 @@ class ShowPlayStoreReviewPromptTest { @Test fun `GIVEN observing review prompt state WHEN not eligible THEN does nothing`() = - runTestOnMain { + runTest(testDispatcher) { val captureMiddleware = CaptureActionsMiddleware<AppState, AppAction>() val appStore = AppStore( initialState = AppState( @@ -116,11 +120,13 @@ class ShowPlayStoreReviewPromptTest { appStore, promptController, activityRef, - uiScope = coroutinesTestRule.scope, + uiScope = this, { navDirection = it }, + mainDispatcher = testDispatcher, ) feature.start() + testDispatcher.scheduler.advanceUntilIdle() coVerify(exactly = 0) { promptController.tryPromptReview(mockActivity) @@ -129,7 +135,7 @@ class ShowPlayStoreReviewPromptTest { } @Test - fun `GIVEN observing review prompt state WHEN eligible for play store prompt THEN show it`() = runTestOnMain { + fun `GIVEN observing review prompt state WHEN eligible for play store prompt THEN show it`() = runTest(testDispatcher) { val captureMiddleware = CaptureActionsMiddleware<AppState, AppAction>() val appStore = AppStore( initialState = AppState( @@ -141,11 +147,13 @@ class ShowPlayStoreReviewPromptTest { appStore, promptController, activityRef, - uiScope = coroutinesTestRule.scope, + uiScope = this, { navDirection = it }, + mainDispatcher = testDispatcher, ) feature.start() + testDispatcher.scheduler.advanceUntilIdle() coVerify(exactly = 1) { promptController.tryPromptReview(mockActivity) diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/snackbar/SnackbarBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/snackbar/SnackbarBindingTest.kt @@ -15,8 +15,8 @@ import com.google.android.material.snackbar.Snackbar.LENGTH_SHORT import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.content.DownloadState @@ -35,11 +35,8 @@ import mozilla.components.support.test.argumentCaptor import mozilla.components.support.test.eq import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.doReturn @@ -78,9 +75,8 @@ import org.mozilla.fenix.utils.getSnackbarTimeout @RunWith(AndroidJUnit4::class) class SnackbarBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() private val appStore = AppStore() private val snackbarDelegate: FenixSnackbarDelegate = mock() private val navController: NavController = mock() @@ -88,7 +84,7 @@ class SnackbarBindingTest { private var settings: Settings = mock() @Before - fun setup() { + fun setup() = runTest(testDispatcher) { settings = mockk(relaxed = true) { every { accessibilityServicesEnabled } returns false } @@ -96,7 +92,7 @@ class SnackbarBindingTest { } @Test - fun `GIVEN translation is in progress for the current selected session WHEN snackbar state is updated to translation in progress THEN display the snackbar`() = runTestOnMain { + fun `GIVEN translation is in progress for the current selected session WHEN snackbar state is updated to translation in progress THEN display the snackbar`() = runTest(testDispatcher) { val sessionId = "sessionId" val tab = createTab(url = "https://www.mozilla.org", id = sessionId) val browserStore = BrowserStore( @@ -113,6 +109,7 @@ class SnackbarBindingTest { appStore.dispatch( TranslationsAction.TranslationStarted(sessionId = sessionId), ) + waitForStoreToSettle() verify(snackbarDelegate).show( text = R.string.translation_in_progress_snackbar, @@ -124,7 +121,7 @@ class SnackbarBindingTest { } @Test - fun `GIVEN translation is in progress for a different session WHEN snackbar state is updated to translation in progress THEN do not display the snackbar`() = runTestOnMain { + fun `GIVEN translation is in progress for a different session WHEN snackbar state is updated to translation in progress THEN do not display the snackbar`() = runTest(testDispatcher) { val tab1 = createTab(url = "https://www.mozilla.org", id = "1") val tab2 = createTab(url = "https://www.mozilla.org", id = "2") val browserStore = BrowserStore( @@ -141,6 +138,7 @@ class SnackbarBindingTest { appStore.dispatch( TranslationsAction.TranslationStarted(sessionId = tab2.id), ) + waitForStoreToSettle() verify(snackbarDelegate, never()).show( text = R.string.translation_in_progress_snackbar, @@ -150,18 +148,19 @@ class SnackbarBindingTest { } @Test - fun `WHEN the snackbar state is updated to dismiss THEN dismiss the snackbar`() = runTestOnMain { + fun `WHEN the snackbar state is updated to dismiss THEN dismiss the snackbar`() = runTest(testDispatcher) { val binding = buildSnackbarBinding() binding.start() appStore.dispatch(SnackbarAction.SnackbarDismissed) + waitForStoreToSettle() assertEquals(None(Dismiss(None())), appStore.state.snackbarState) verify(snackbarDelegate).dismiss() } @Test - fun `GIVEN bookmark's parent is a root node WHEN the bookmark added state is observed THEN display friendly title`() = runTestOnMain { + fun `GIVEN bookmark's parent is a root node WHEN the bookmark added state is observed THEN display friendly title`() = runTest(testDispatcher) { val parent = buildParentBookmarkNode(guid = BookmarkRoot.Mobile.id, title = "mobile") val binding = buildSnackbarBinding() binding.start() @@ -173,6 +172,7 @@ class SnackbarBindingTest { source = Source.TEST, ), ) + waitForStoreToSettle() assertEquals(None(BookmarkAdded("1", parent)), appStore.state.snackbarState) @@ -190,7 +190,7 @@ class SnackbarBindingTest { } @Test - fun `GIVEN bookmark's parent is not a root node but has a root node title WHEN the bookmark added state is observed THEN display custom title`() = runTestOnMain { + fun `GIVEN bookmark's parent is not a root node but has a root node title WHEN the bookmark added state is observed THEN display custom title`() = runTest(testDispatcher) { val parent = buildParentBookmarkNode(title = "mobile", guid = "not a root") val binding = buildSnackbarBinding() binding.start() @@ -203,6 +203,8 @@ class SnackbarBindingTest { ), ) + waitForStoreToSettle() + assertEquals(None(BookmarkAdded("1", parent)), appStore.state.snackbarState) val outputMessage = testContext.getString(R.string.bookmark_saved_in_folder_snackbar, "mobile") @@ -219,7 +221,7 @@ class SnackbarBindingTest { } @Test - fun `GIVEN no bookmark is added WHEN the bookmark added state is observed THEN display the error snackbar`() = runTestOnMain { + fun `GIVEN no bookmark is added WHEN the bookmark added state is observed THEN display the error snackbar`() = runTest(testDispatcher) { val parent = buildParentBookmarkNode() val binding = buildSnackbarBinding() binding.start() @@ -228,6 +230,8 @@ class SnackbarBindingTest { BookmarkAction.BookmarkAdded(guidToEdit = null, parentNode = parent, source = Source.TEST), ) + waitForStoreToSettle() + assertEquals(None(BookmarkAdded(null, parent)), appStore.state.snackbarState) verify(snackbarDelegate).show( text = R.string.bookmark_invalid_url_error, @@ -236,13 +240,14 @@ class SnackbarBindingTest { } @Test - fun `GIVEN there is no parent folder for an added bookmark WHEN the bookmark added state is observed THEN display the error snackbar`() = runTestOnMain { + fun `GIVEN there is no parent folder for an added bookmark WHEN the bookmark added state is observed THEN display the error snackbar`() = runTest(testDispatcher) { val binding = buildSnackbarBinding() binding.start() appStore.dispatch( BookmarkAction.BookmarkAdded(guidToEdit = "guid", parentNode = null, source = Source.TEST), ) + waitForStoreToSettle() assertEquals(None(BookmarkAdded("guid", null)), appStore.state.snackbarState) verify(snackbarDelegate).show( @@ -253,13 +258,14 @@ class SnackbarBindingTest { } @Test - fun `WHEN the shortcut added state action is dispatched THEN display the appropriate snackbar`() = runTestOnMain { + fun `WHEN the shortcut added state action is dispatched THEN display the appropriate snackbar`() = runTest(testDispatcher) { val binding = buildSnackbarBinding() binding.start() appStore.dispatch( AppAction.ShortcutAction.ShortcutAdded, ) + waitForStoreToSettle() assertEquals(None(ShortcutAdded), appStore.state.snackbarState) verify(snackbarDelegate).show( @@ -270,13 +276,14 @@ class SnackbarBindingTest { } @Test - fun `WHEN the delete and quit selected state action is dispatched THEN display the appropriate snackbar`() = runTestOnMain { + fun `WHEN the delete and quit selected state action is dispatched THEN display the appropriate snackbar`() = runTest(testDispatcher) { val binding = buildSnackbarBinding() binding.start() appStore.dispatch( AppAction.DeleteAndQuitStarted, ) + waitForStoreToSettle() assertEquals(None(DeletingBrowserDataInProgress), appStore.state.snackbarState) verify(snackbarDelegate).show( @@ -287,7 +294,7 @@ class SnackbarBindingTest { } @Test - fun `WHEN the user has successfully signed in THEN display the appropriate snackbar`() = runTestOnMain { + fun `WHEN the user has successfully signed in THEN display the appropriate snackbar`() = runTest(testDispatcher) { val binding = buildSnackbarBinding() binding.start() @@ -295,6 +302,8 @@ class SnackbarBindingTest { AppAction.UserAccountAuthenticated, ) + waitForStoreToSettle() + assertEquals(None(UserAccountAuthenticated), appStore.state.snackbarState) verify(snackbarDelegate).show( text = R.string.sync_syncing_in_progress, @@ -305,11 +314,12 @@ class SnackbarBindingTest { } @Test - fun `WHEN share to app failed THEN display a snackbar`() { + fun `WHEN share to app failed THEN display a snackbar`() = runTest(testDispatcher) { val binding = buildSnackbarBinding() binding.start() appStore.dispatch(ShareAction.ShareToAppFailed) + waitForStoreToSettle() verify(snackbarDelegate).show( text = R.string.share_error_snackbar, @@ -321,13 +331,14 @@ class SnackbarBindingTest { } @Test - fun `WHEN sharing a tab was successful THEN display an appropriate snackbar`() { + fun `WHEN sharing a tab was successful THEN display an appropriate snackbar`() = runTest(testDispatcher) { val destinations = listOf("a") val sharedTabs = listOf(mock<TabData>()) val binding = buildSnackbarBinding() binding.start() appStore.dispatch(ShareAction.SharedTabsSuccessfully(destinations, sharedTabs)) + waitForStoreToSettle() verify(snackbarDelegate).show( text = R.string.sync_sent_tab_snackbar_2, @@ -339,13 +350,14 @@ class SnackbarBindingTest { } @Test - fun `WHEN sharing multiple tabs was successful THEN display an appropriate snackbar`() { + fun `WHEN sharing multiple tabs was successful THEN display an appropriate snackbar`() = runTest(testDispatcher) { val destinations = listOf("a") val sharedTabs = listOf(mock<TabData>(), mock<TabData>()) val binding = buildSnackbarBinding() binding.start() appStore.dispatch(ShareAction.SharedTabsSuccessfully(destinations, sharedTabs)) + waitForStoreToSettle() verify(snackbarDelegate).show( text = R.string.sync_sent_tabs_snackbar_2, @@ -357,13 +369,14 @@ class SnackbarBindingTest { } @Test - fun `WHEN sharing tabs failed THEN show a snackbar`() { + fun `WHEN sharing tabs failed THEN show a snackbar`() = runTest(testDispatcher) { val destinations = listOf("a") val sharedTabs = listOf(mock<TabData>()) val binding = buildSnackbarBinding() binding.start() appStore.dispatch(ShareAction.ShareTabsFailed(destinations, sharedTabs)) + waitForStoreToSettle() verify(snackbarDelegate).show( text = eq(R.string.sync_sent_tab_error_snackbar), @@ -378,7 +391,7 @@ class SnackbarBindingTest { } @Test - fun `GIVEN sharing tabs to another device failed and user chose to retry WHEN this succeeds THEN show a snackbar`() = runTestOnMain { + fun `GIVEN sharing tabs to another device failed and user chose to retry WHEN this succeeds THEN show a snackbar`() = runTest(testDispatcher) { val destinations = listOf("a") val sharedTabs = listOf(mock<TabData>()) val retryActionCaptor = argumentCaptor<((v: View) -> Unit)>() @@ -389,11 +402,11 @@ class SnackbarBindingTest { doReturn(retryResult).`when`(sendToDeviceUseCase).invoke(any(), any<List<TabData>>()) val binding = buildSnackbarBinding( sendTabUseCases = sendTabUseCases, - ioDispatcher = coroutineRule.testDispatcher, ) binding.start() appStore.dispatch(ShareAction.ShareTabsFailed(destinations, sharedTabs)) + waitForStoreToSettle() verify(snackbarDelegate).show( text = eq(R.string.sync_sent_tab_error_snackbar), @@ -405,6 +418,7 @@ class SnackbarBindingTest { ) retryActionCaptor.value.invoke(mock()) + waitForStoreToSettle() verify(snackbarDelegate).show( text = R.string.sync_sent_tab_snackbar_2, @@ -416,7 +430,7 @@ class SnackbarBindingTest { } @Test - fun `GIVEN sharing tabs to other devices failed and user chose to retry WHEN this fails again THEN show a snackbar`() = runTestOnMain { + fun `GIVEN sharing tabs to other devices failed and user chose to retry WHEN this fails again THEN show a snackbar`() = runTest(testDispatcher) { val destinations = listOf("a", "b") val sharedTabs = listOf(mock<TabData>()) val retryActionCaptor = argumentCaptor<((v: View) -> Unit)>() @@ -427,11 +441,11 @@ class SnackbarBindingTest { doReturn(retryResult).`when`(sendToAllDevicesUseCase).invoke(any<List<TabData>>()) val binding = buildSnackbarBinding( sendTabUseCases = sendTabUseCases, - ioDispatcher = coroutineRule.testDispatcher, ) binding.start() appStore.dispatch(ShareAction.ShareTabsFailed(destinations, sharedTabs)) + waitForStoreToSettle() verify(snackbarDelegate).show( text = eq(R.string.sync_sent_tab_error_snackbar), @@ -443,6 +457,7 @@ class SnackbarBindingTest { ) retryActionCaptor.value.invoke(mock()) + waitForStoreToSettle() verify(snackbarDelegate, times(2)).show( text = eq(R.string.sync_sent_tab_error_snackbar), @@ -457,11 +472,12 @@ class SnackbarBindingTest { } @Test - fun `WHEN a link is copied to clipboard THEN display a snackbar`() { + fun `WHEN a link is copied to clipboard THEN display a snackbar`() = runTest(testDispatcher) { val binding = buildSnackbarBinding() binding.start() appStore.dispatch(ShareAction.CopyLinkToClipboard) + waitForStoreToSettle() verify(snackbarDelegate).show( text = R.string.toast_copy_link_to_clipboard, @@ -472,7 +488,7 @@ class SnackbarBindingTest { } @Test - fun `WHEN the current tab is closed THEN display a snackbar`() { + fun `WHEN the current tab is closed THEN display a snackbar`() = runTest(testDispatcher) { val snackbarAction = argumentCaptor<((v: View) -> Unit)>() val undoUsecase: UndoTabRemovalUseCase = mock() doReturn(undoUsecase).`when`(tabsUseCases).undo @@ -480,6 +496,7 @@ class SnackbarBindingTest { binding.start() appStore.dispatch(AppAction.CurrentTabClosed(isPrivate = false)) + waitForStoreToSettle() verify(snackbarDelegate).show( text = eq(testContext.tabClosedUndoMessage(false)), @@ -498,12 +515,13 @@ class SnackbarBindingTest { } @Test - fun `WHEN download is failed THEN display a snackbar`() { + fun `WHEN download is failed THEN display a snackbar`() = runTest(testDispatcher) { val snackbarAction = argumentCaptor<((v: View) -> Unit)>() val binding = buildSnackbarBinding() binding.start() appStore.dispatch(AppAction.DownloadAction.DownloadFailed(fileName = "fileName")) + waitForStoreToSettle() verify(snackbarDelegate).show( text = eq(testContext.getString(R.string.download_item_status_failed)), @@ -525,7 +543,7 @@ class SnackbarBindingTest { } @Test - fun `WHEN download is completed THEN display a snackbar`() { + fun `WHEN download is completed THEN display a snackbar`() = runTest(testDispatcher) { val snackbarAction = argumentCaptor<((v: View) -> Unit)>() val binding = buildSnackbarBinding() binding.start() @@ -545,6 +563,7 @@ class SnackbarBindingTest { ) appStore.dispatch(AppAction.DownloadAction.DownloadCompleted(downloadState)) + waitForStoreToSettle() verify(snackbarDelegate).show( text = eq(testContext.getString(R.string.download_completed_snackbar)), @@ -559,7 +578,7 @@ class SnackbarBindingTest { } @Test - fun `WHEN download file can't be open THEN display a snackbar`() { + fun `WHEN download file can't be open THEN display a snackbar`() = runTest(testDispatcher) { val binding = buildSnackbarBinding() binding.start() @@ -578,6 +597,7 @@ class SnackbarBindingTest { ) appStore.dispatch(AppAction.DownloadAction.CannotOpenFile(downloadState)) + waitForStoreToSettle() verify(snackbarDelegate).show( text = "No app found to open files", @@ -587,7 +607,7 @@ class SnackbarBindingTest { } @Test - fun `WHEN download file is in progress THEN display a snackbar`() { + fun `WHEN download file is in progress THEN display a snackbar`() = runTest(testDispatcher) { val snackbarAction = argumentCaptor<((v: View) -> Unit)>() val binding = buildSnackbarBinding( browserStore = BrowserStore( @@ -602,6 +622,7 @@ class SnackbarBindingTest { binding.start() appStore.dispatch(AppAction.DownloadAction.DownloadInProgress(downloadId = "id")) + waitForStoreToSettle() verify(snackbarDelegate).show( text = eq(testContext.getString(R.string.download_in_progress_snackbar)), @@ -623,12 +644,13 @@ class SnackbarBindingTest { } @Test - fun `WHEN a webcompat report is successfully sent THEN show a snackbar`() { + fun `WHEN a webcompat report is successfully sent THEN show a snackbar`() = runTest(testDispatcher) { val snackbarAction = argumentCaptor<((v: View) -> Unit)>() val binding = buildSnackbarBinding() binding.start() appStore.dispatch(WebCompatAction.WebCompatReportSent) + waitForStoreToSettle() verify(snackbarDelegate).show( text = eq(testContext.getString(R.string.webcompat_reporter_success_snackbar_text_2)), @@ -657,7 +679,6 @@ class SnackbarBindingTest { tabsUseCases: TabsUseCases = this.tabsUseCases, sendTabUseCases: SendTabUseCases? = null, customTabSessionId: String? = null, - ioDispatcher: CoroutineDispatcher = Dispatchers.IO, ) = SnackbarBinding( context = context, browserStore = browserStore, @@ -667,9 +688,15 @@ class SnackbarBindingTest { tabsUseCases = tabsUseCases, sendTabUseCases = sendTabUseCases, customTabSessionId = customTabSessionId, - ioDispatcher = ioDispatcher, + ioDispatcher = testDispatcher, + mainDispatcher = testDispatcher, ) + private fun waitForStoreToSettle() = runTest(testDispatcher) { + // Run the enqueued tasks + testDispatcher.scheduler.advanceUntilIdle() + } + private fun buildParentBookmarkNode( guid: String = "guid", title: String = "title", diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/SecureTabsTrayBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/SecureTabsTrayBindingTest.kt @@ -12,120 +12,100 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.verify -import mozilla.components.support.test.rule.MainCoroutineRule +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.mozilla.fenix.ext.removeSecure -import org.mozilla.fenix.ext.secure import org.mozilla.fenix.utils.Settings -class SecureTabsTrayBindingTest { +/** + * Tests for [SecureTabsTrayBinding]. + */ - @get:Rule - val coroutinesTestRule = MainCoroutineRule() +class SecureTabsTrayBindingTest { + private val testDispatcher = StandardTestDispatcher() private val settings: Settings = mockk(relaxed = true) private val fragment: Fragment = mockk(relaxed = true) private val dialog: TabsTrayDialog = mockk(relaxed = true) private val window: Window = mockk(relaxed = true) + private lateinit var secureTabsTrayBinding: SecureTabsTrayBinding + private lateinit var tabsTrayStore: TabsTrayStore @Before fun setup() { - every { fragment.secure() } just Runs - every { fragment.removeSecure() } just Runs + tabsTrayStore = TabsTrayStore(TabsTrayState()) + secureTabsTrayBinding = SecureTabsTrayBinding( + store = tabsTrayStore, + settings = settings, + fragment = fragment, + dialog = dialog, + mainDispatcher = testDispatcher, + ) + every { dialog.window } returns window every { window.addFlags(any()) } just Runs every { window.clearFlags(any()) } just Runs + every { secureTabsTrayBinding.setSecureMode(true) } just Runs + every { secureTabsTrayBinding.setSecureMode(false) } just Runs } @Test - fun `WHEN tab selected page switches to private THEN set fragment to secure`() { - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabsTrayBinding = SecureTabsTrayBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - dialog = dialog, - ) + fun `WHEN tab selected page switches to private THEN set fragment to secure`() = runTest(testDispatcher) { + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.PrivateTabs)) secureTabsTrayBinding.start() - tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.PrivateTabs)) + testDispatcher.scheduler.advanceUntilIdle() - verify { fragment.secure() } verify { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) } } @Test - fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode true THEN set fragment to un-secure`() { - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabsTrayBinding = SecureTabsTrayBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - dialog = dialog, - ) + fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode true THEN set fragment to un-secure`() = runTest(testDispatcher) { every { settings.allowScreenshotsInPrivateMode } returns true secureTabsTrayBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.PrivateTabs)) - verify { fragment.removeSecure() } verify { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } } @Test - fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode false and shouldSecureModeBeOverridden true THEN set fragment to un-secure`() { - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabsTrayBinding = SecureTabsTrayBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - dialog = dialog, - ) + fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode false and shouldSecureModeBeOverridden true THEN set fragment to un-secure`() = runTest(testDispatcher) { every { settings.allowScreenshotsInPrivateMode } returns false every { settings.allowScreenCaptureInSecureScreens } returns false secureTabsTrayBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.PrivateTabs)) - verify { fragment.removeSecure() } verify { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } } @Test - fun `GIVEN not in private mode WHEN tab selected page switches to normal tabs from private THEN set fragment to un-secure`() { + fun `GIVEN not in private mode WHEN tab selected page switches to normal tabs from private THEN set fragment to un-secure`() = runTest(testDispatcher) { every { settings.lastKnownMode.isPrivate } returns false - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabsTrayBinding = SecureTabsTrayBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - dialog = dialog, - ) secureTabsTrayBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.NormalTabs)) - verify { fragment.removeSecure() } verify { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } } @Test - fun `GIVEN private mode WHEN tab selected page switches to normal tabs from private THEN do nothing`() { + fun `GIVEN private mode WHEN tab selected page switches to normal tabs from private THEN do nothing`() = runTest(testDispatcher) { every { settings.lastKnownMode.isPrivate } returns true - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabsTrayBinding = SecureTabsTrayBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - dialog = dialog, - ) secureTabsTrayBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.NormalTabs)) - verify(exactly = 0) { fragment.removeSecure() } verify(exactly = 0) { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/binding/SecureTabManagerBindingTest.kt @@ -10,12 +10,10 @@ import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.verify -import mozilla.components.support.test.rule.MainCoroutineRule +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Before -import org.junit.Rule import org.junit.Test -import org.mozilla.fenix.ext.removeSecure -import org.mozilla.fenix.ext.secure import org.mozilla.fenix.tabstray.Page import org.mozilla.fenix.tabstray.TabsTrayAction import org.mozilla.fenix.tabstray.TabsTrayState @@ -24,112 +22,106 @@ import org.mozilla.fenix.utils.Settings class SecureTabManagerBindingTest { - @get:Rule - val coroutinesTestRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() private val settings: Settings = mockk(relaxed = true) private val fragment: Fragment = mockk(relaxed = true) + private lateinit var secureTabManagerBinding: SecureTabManagerBinding + private lateinit var tabsTrayStore: TabsTrayStore @Before fun setup() { - every { fragment.secure() } just Runs - every { fragment.removeSecure() } just Runs - } - - @Test - fun `WHEN tab selected page switches to private THEN set fragment to secure`() { - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabManagerBinding = SecureTabManagerBinding( + tabsTrayStore = TabsTrayStore(TabsTrayState()) + secureTabManagerBinding = SecureTabManagerBinding( store = tabsTrayStore, settings = settings, fragment = fragment, + mainDispatcher = testDispatcher, ) + every { secureTabManagerBinding.setSecureMode(false) } just Runs + every { secureTabManagerBinding.setSecureMode(true) } just Runs + } + + @Test + fun `WHEN tab selected page switches to private THEN set fragment to secure`() = runTest(testDispatcher) { + every { settings.shouldSecureModeBeOverridden } returns false + secureTabManagerBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.PrivateTabs)) + testDispatcher.scheduler.advanceUntilIdle() - verify { fragment.secure() } + verify { secureTabManagerBinding.setSecureMode(true) } } @Test - fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode true THEN set fragment to un-secure`() { - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabManagerBinding = SecureTabManagerBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - ) + fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode true THEN set fragment to un-secure`() = runTest(testDispatcher) { every { settings.allowScreenshotsInPrivateMode } returns true secureTabManagerBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.PrivateTabs)) + testDispatcher.scheduler.advanceUntilIdle() - verify { fragment.removeSecure() } + verify { secureTabManagerBinding.setSecureMode(false) } } @Test - fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode false and shouldSecureModeBeOverridden true THEN set fragment to un-secure`() { - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabManagerBinding = SecureTabManagerBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - ) + fun `WHEN tab selected page switches to private and allowScreenshotsInPrivateMode false and shouldSecureModeBeOverridden true THEN set fragment to un-secure`() = runTest(testDispatcher) { every { settings.allowScreenshotsInPrivateMode } returns false every { settings.shouldSecureModeBeOverridden } returns true secureTabManagerBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.PrivateTabs)) + testDispatcher.scheduler.advanceUntilIdle() - verify { fragment.removeSecure() } + verify { secureTabManagerBinding.setSecureMode(false) } } @Test - fun `GIVEN not in private mode WHEN tab selected page switches to normal tabs from private THEN set fragment to un-secure`() { + fun `GIVEN not in private mode WHEN tab selected page switches to normal tabs from private THEN set fragment to un-secure`() = runTest(testDispatcher) { every { settings.lastKnownMode.isPrivate } returns false - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabManagerBinding = SecureTabManagerBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - ) secureTabManagerBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.NormalTabs)) + testDispatcher.scheduler.advanceUntilIdle() - verify { fragment.removeSecure() } + verify { secureTabManagerBinding.setSecureMode(false) } } @Test - fun `GIVEN private mode WHEN tab selected page switches to normal tabs from private THEN do nothing`() { + fun `GIVEN private mode WHEN tab selected page switches to normal tabs from private THEN do nothing`() = runTest(testDispatcher) { every { settings.lastKnownMode.isPrivate } returns true - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabManagerBinding = SecureTabManagerBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - ) secureTabManagerBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.NormalTabs)) + testDispatcher.scheduler.advanceUntilIdle() - verify(exactly = 0) { fragment.removeSecure() } + verify(exactly = 0) { secureTabManagerBinding.setSecureMode(false) } } @Test - fun `GIVEN in Normal browsing mode WHEN fragment is stopped THEN set fragment to un-secure`() { + fun `GIVEN in Normal browsing mode WHEN fragment is stopped THEN set fragment to un-secure`() = runTest(testDispatcher) { every { settings.lastKnownMode.isPrivate } returns false - val tabsTrayStore = TabsTrayStore(TabsTrayState()) - val secureTabManagerBinding = SecureTabManagerBinding( - store = tabsTrayStore, - settings = settings, - fragment = fragment, - ) secureTabManagerBinding.start() + testDispatcher.scheduler.advanceUntilIdle() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.NormalTabs)) + testDispatcher.scheduler.advanceUntilIdle() + secureTabManagerBinding.stop() + testDispatcher.scheduler.advanceUntilIdle() - verify { fragment.removeSecure() } + verify { secureTabManagerBinding.setSecureMode(false) } } } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBindingTest.kt @@ -4,42 +4,47 @@ package org.mozilla.fenix.tabstray.syncedtabs -import mozilla.components.support.test.rule.MainCoroutineRule +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.junit.Rule import org.junit.Test import org.mozilla.fenix.tabstray.TabsTrayAction import org.mozilla.fenix.tabstray.TabsTrayStore +@OptIn(ExperimentalCoroutinesApi::class) class SyncButtonBindingTest { - @get:Rule - val coroutinesTestRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() @Test - fun `WHEN syncing state is true THEN invoke callback`() { + fun `WHEN syncing state is true THEN invoke callback`() = runTest(testDispatcher) { var invoked = false val store = TabsTrayStore() - val binding = SyncButtonBinding(store) { invoked = true } + val binding = SyncButtonBinding(store, testDispatcher) { invoked = true } binding.start() store.dispatch(TabsTrayAction.SyncNow) + testDispatcher.scheduler.advanceUntilIdle() assertTrue(invoked) } @Test - fun `WHEN syncing state is false THEN nothing is invoked`() { + fun `WHEN syncing state is false THEN nothing is invoked`() = runTest(testDispatcher) { var invoked = false val store = TabsTrayStore() - val binding = SyncButtonBinding(store) { invoked = true } + val binding = SyncButtonBinding(store, testDispatcher) { invoked = true } binding.start() + testDispatcher.scheduler.advanceUntilIdle() + assertFalse(invoked) store.dispatch(TabsTrayAction.SyncCompleted) + testDispatcher.scheduler.advanceUntilIdle() assertFalse(invoked) } diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogBindingTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/translations/TranslationsDialogBindingTest.kt @@ -5,6 +5,8 @@ package org.mozilla.fenix.translations import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.TranslationsAction import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.createTab @@ -17,11 +19,7 @@ import mozilla.components.concept.engine.translate.TranslationError import mozilla.components.concept.engine.translate.TranslationOperation import mozilla.components.concept.engine.translate.TranslationPair import mozilla.components.concept.engine.translate.TranslationSupport -import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.robolectric.testContext -import mozilla.components.support.test.rule.MainCoroutineRule -import mozilla.components.support.test.rule.runTestOnMain -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.never @@ -32,9 +30,8 @@ import org.mozilla.fenix.R @RunWith(AndroidJUnit4::class) class TranslationsDialogBindingTest { - @get:Rule - val coroutineRule = MainCoroutineRule() + private val testDispatcher = StandardTestDispatcher() lateinit var browserStore: BrowserStore private lateinit var translationsDialogStore: TranslationsDialogStore @@ -43,7 +40,7 @@ class TranslationsDialogBindingTest { @Test fun `WHEN fromLanguage and toLanguage get updated in the browserStore THEN translations dialog actions dispatched with the update`() = - runTestOnMain { + runTest(testDispatcher) { val englishLanguage = Language("en", "English") val spanishLanguage = Language("es", "Spanish") translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) @@ -64,6 +61,7 @@ class TranslationsDialogBindingTest { localizedTo, ) }, + mainDispatcher = testDispatcher, ) binding.start() @@ -93,6 +91,7 @@ class TranslationsDialogBindingTest { supportedLanguages = supportLanguages, ), ) + testDispatcher.scheduler.advanceUntilIdle() browserStore.dispatch( TranslationsAction.TranslateStateChangeAction( @@ -100,6 +99,7 @@ class TranslationsDialogBindingTest { translationEngineState = translationEngineState, ), ) + testDispatcher.scheduler.advanceUntilIdle() verify(translationsDialogStore).dispatch( TranslationsDialogAction.UpdateFromSelectedLanguage( @@ -124,7 +124,7 @@ class TranslationsDialogBindingTest { @Test fun `WHEN translate action is sent to the browserStore THEN update translation dialog store based on operation`() = - runTestOnMain { + runTest(testDispatcher) { val englishLanguage = Language("en", "English") val spanishLanguage = Language("es", "Spanish") translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) @@ -145,6 +145,7 @@ class TranslationsDialogBindingTest { localizedTo, ) }, + mainDispatcher = testDispatcher, ) binding.start() @@ -156,6 +157,7 @@ class TranslationsDialogBindingTest { null, ), ) + testDispatcher.scheduler.advanceUntilIdle() verify(translationsDialogStore).dispatch( TranslationsDialogAction.UpdateTranslationInProgress( @@ -171,7 +173,7 @@ class TranslationsDialogBindingTest { @Test fun `WHEN translate from languages list and translate to languages list are sent to the browserStore THEN update translation dialog store based on operation`() = - runTestOnMain { + runTest(testDispatcher) { translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) browserStore = BrowserStore( BrowserState( @@ -190,6 +192,7 @@ class TranslationsDialogBindingTest { localizedTo, ) }, + mainDispatcher = testDispatcher, ) binding.start() @@ -201,6 +204,7 @@ class TranslationsDialogBindingTest { supportedLanguages = supportedLanguages, ), ) + testDispatcher.scheduler.advanceUntilIdle() verify(translationsDialogStore).dispatch( TranslationsDialogAction.UpdateTranslateFromLanguages( @@ -216,7 +220,7 @@ class TranslationsDialogBindingTest { @Test fun `WHEN translate action success is sent to the browserStore THEN update translation dialog store based on operation`() = - runTestOnMain { + runTest(testDispatcher) { translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState(dismissDialogState = DismissDialogState.WaitingToBeDismissed))) browserStore = BrowserStore( @@ -236,6 +240,7 @@ class TranslationsDialogBindingTest { localizedTo, ) }, + mainDispatcher = testDispatcher, ) binding.start() @@ -245,6 +250,7 @@ class TranslationsDialogBindingTest { operation = TranslationOperation.TRANSLATE, ), ) + testDispatcher.scheduler.advanceUntilIdle() // Simulate success response post-translate val detectedLanguages = DetectedLanguages( @@ -270,6 +276,7 @@ class TranslationsDialogBindingTest { translationEngineState = translationEngineState, ), ) + testDispatcher.scheduler.advanceUntilIdle() verify(translationsDialogStore).dispatch( TranslationsDialogAction.UpdateTranslated( @@ -290,7 +297,7 @@ class TranslationsDialogBindingTest { @Test fun `WHEN translate fetch error is sent to the browserStore THEN update translation dialog store based on operation`() = - runTestOnMain { + runTest(testDispatcher) { translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) browserStore = BrowserStore( @@ -310,6 +317,7 @@ class TranslationsDialogBindingTest { localizedTo, ) }, + mainDispatcher = testDispatcher, ) binding.start() @@ -321,6 +329,7 @@ class TranslationsDialogBindingTest { translationError = fetchError, ), ) + testDispatcher.scheduler.advanceUntilIdle() verify(translationsDialogStore).dispatch( TranslationsDialogAction.UpdateTranslationError(fetchError), @@ -329,7 +338,7 @@ class TranslationsDialogBindingTest { @Test fun `WHEN a non-displayable error is sent to the browserStore THEN the translation dialog store is not updated`() = - runTestOnMain { + runTest(testDispatcher) { translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) browserStore = BrowserStore( @@ -349,6 +358,7 @@ class TranslationsDialogBindingTest { localizedTo, ) }, + mainDispatcher = testDispatcher, ) binding.start() @@ -358,6 +368,7 @@ class TranslationsDialogBindingTest { error = fetchError, ), ) + testDispatcher.scheduler.advanceUntilIdle() verify(translationsDialogStore, never()).dispatch( TranslationsDialogAction.UpdateTranslationError(fetchError), @@ -366,7 +377,7 @@ class TranslationsDialogBindingTest { @Test fun `WHEN a browser and session error is sent to the browserStore THEN the session error takes priority and the translation dialog store is updated`() = - runTestOnMain { + runTest(testDispatcher) { translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) browserStore = BrowserStore( @@ -386,6 +397,7 @@ class TranslationsDialogBindingTest { localizedTo, ) }, + mainDispatcher = testDispatcher, ) binding.start() @@ -397,6 +409,7 @@ class TranslationsDialogBindingTest { translationError = sessionError, ), ) + testDispatcher.scheduler.advanceUntilIdle() verify(translationsDialogStore).dispatch( TranslationsDialogAction.UpdateTranslationError(sessionError), @@ -408,6 +421,7 @@ class TranslationsDialogBindingTest { error = engineError, ), ) + testDispatcher.scheduler.advanceUntilIdle() verify(translationsDialogStore, never()).dispatch( TranslationsDialogAction.UpdateTranslationError(engineError), @@ -416,7 +430,7 @@ class TranslationsDialogBindingTest { @Test fun `WHEN set translation download size action sent to the browserStore THEN update translation dialog store based on operation`() = - runTestOnMain { + runTest(testDispatcher) { translationsDialogStore = spy(TranslationsDialogStore(TranslationsDialogState())) browserStore = BrowserStore( @@ -436,6 +450,7 @@ class TranslationsDialogBindingTest { localizedTo, ) }, + mainDispatcher = testDispatcher, ) binding.start() @@ -452,9 +467,12 @@ class TranslationsDialogBindingTest { translationSize = translationDownloadSize, ), ) + testDispatcher.scheduler.advanceUntilIdle() verify(translationsDialogStore).dispatch( - TranslationsDialogAction.UpdateDownloadTranslationDownloadSize(translationDownloadSize), + TranslationsDialogAction.UpdateDownloadTranslationDownloadSize( + translationDownloadSize, + ), ) } }