tor-browser

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

commit 2cf6abd54c1cbb56ad123ceebac555f38752b09b
parent fa582eed17d875e161c0654cb4fd1a69aebf7a44
Author: pollymce <pmceldowney@mozilla.com>
Date:   Wed,  3 Dec 2025 15:10:05 +0000

Bug 2000584 - async all the engine observer action dispatches r=android-reviewers,jonalmeida

This seems to speed up the browsertime performance metrics.

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

Diffstat:
Mmobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt | 151+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Mmobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/LinkingMiddleware.kt | 2+-
Mmobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt | 512++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mmobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt | 9+++++++--
4 files changed, 463 insertions(+), 211 deletions(-)

diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/EngineObserver.kt @@ -6,6 +6,8 @@ package mozilla.components.browser.state.engine import android.content.Intent import android.os.Environment +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.CookieBannerAction @@ -52,46 +54,47 @@ private const val PAGE_LOAD_COMPLETION_PROGRESS = 100 internal class EngineObserver( private val tabId: String, private val store: Store<BrowserState, BrowserAction>, + private val scope: CoroutineScope, ) : EngineSession.Observer { override fun onScrollChange(scrollX: Int, scrollY: Int) { - store.dispatch(ReaderAction.UpdateReaderScrollYAction(tabId, scrollY)) + dispatchAsync(ReaderAction.UpdateReaderScrollYAction(tabId, scrollY)) } override fun onNavigateBack() { - store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, "")) + dispatchAsync(ContentAction.UpdateSearchTermsAction(tabId, "")) } override fun onNavigateForward() { - store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, "")) + dispatchAsync(ContentAction.UpdateSearchTermsAction(tabId, "")) } override fun onGotoHistoryIndex() { - store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, "")) + dispatchAsync(ContentAction.UpdateSearchTermsAction(tabId, "")) } override fun onLoadData() { - store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, "")) + dispatchAsync(ContentAction.UpdateSearchTermsAction(tabId, "")) } override fun onLoadUrl() { if (store.state.findTabOrCustomTab(tabId)?.content?.isSearch == true) { - store.dispatch(ContentAction.UpdateIsSearchAction(tabId, false)) + dispatchAsync(ContentAction.UpdateIsSearchAction(tabId, false)) } else { - store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, "")) + dispatchAsync(ContentAction.UpdateSearchTermsAction(tabId, "")) } } override fun onFirstContentfulPaint() { - store.dispatch(ContentAction.UpdateFirstContentfulPaintStateAction(tabId, true)) + dispatchAsync(ContentAction.UpdateFirstContentfulPaintStateAction(tabId, true)) } override fun onPaintStatusReset() { - store.dispatch(ContentAction.UpdateFirstContentfulPaintStateAction(tabId, false)) + dispatchAsync(ContentAction.UpdateFirstContentfulPaintStateAction(tabId, false)) } override fun onLocationChange(url: String, hasUserGesture: Boolean) { - store.dispatch(ContentAction.UpdateUrlAction(tabId, url, hasUserGesture)) + dispatchAsync(ContentAction.UpdateUrlAction(tabId, url, hasUserGesture)) } @Suppress("DEPRECATION") // Session observable is deprecated @@ -101,11 +104,11 @@ internal class EngineObserver( triggeredByWebContent: Boolean, ) { if (triggeredByWebContent) { - store.dispatch(ContentAction.UpdateSearchTermsAction(tabId, "")) + dispatchAsync(ContentAction.UpdateSearchTermsAction(tabId, "")) } val loadRequest = LoadRequestState(url, triggeredByRedirect, triggeredByWebContent) - store.dispatch(ContentAction.UpdateLoadRequestAction(tabId, loadRequest)) + dispatchAsync(ContentAction.UpdateLoadRequestAction(tabId, loadRequest)) } override fun onLaunchIntentRequest( @@ -114,7 +117,7 @@ internal class EngineObserver( fallbackUrl: String?, appName: String?, ) { - store.dispatch( + dispatchAsync( ContentAction.UpdateAppIntentAction( tabId, AppIntentState(url, appIntent, fallbackUrl, appName), @@ -123,11 +126,11 @@ internal class EngineObserver( } override fun onTitleChange(title: String) { - store.dispatch(ContentAction.UpdateTitleAction(tabId, title)) + dispatchAsync(ContentAction.UpdateTitleAction(tabId, title)) } override fun onPreviewImageChange(previewImageUrl: String) { - store.dispatch(ContentAction.UpdatePreviewImageAction(tabId, previewImageUrl)) + dispatchAsync(ContentAction.UpdatePreviewImageAction(tabId, previewImageUrl)) } override fun onProgress(progress: Int) { @@ -135,32 +138,32 @@ internal class EngineObserver( // referencing to a field in the state is not recommended, this flow should be reconsidered // while the visual completeness logic is revisited in Bug 1966977. if (progress == PAGE_LOAD_COMPLETION_PROGRESS && !store.state.translationsInitialized) { - store.dispatch(TranslationsAction.InitTranslationsBrowserState) + dispatchAsync(TranslationsAction.InitTranslationsBrowserState) } - store.dispatch(ContentAction.UpdateProgressAction(tabId, progress)) + dispatchAsync(ContentAction.UpdateProgressAction(tabId, progress)) } override fun onLoadingStateChange(loading: Boolean) { - store.dispatch(ContentAction.UpdateLoadingStateAction(tabId, loading)) + dispatchAsync(ContentAction.UpdateLoadingStateAction(tabId, loading)) if (loading) { - store.dispatch(ContentAction.ClearFindResultsAction(tabId)) - store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, false)) - store.dispatch(TrackingProtectionAction.ClearTrackersAction(tabId)) + dispatchAsync(ContentAction.ClearFindResultsAction(tabId)) + dispatchAsync(ContentAction.UpdateRefreshCanceledStateAction(tabId, false)) + dispatchAsync(TrackingProtectionAction.ClearTrackersAction(tabId)) } } override fun onNavigationStateChange(canGoBack: Boolean?, canGoForward: Boolean?) { canGoBack?.let { - store.dispatch(ContentAction.UpdateBackNavigationStateAction(tabId, canGoBack)) + dispatchAsync(ContentAction.UpdateBackNavigationStateAction(tabId, canGoBack)) } canGoForward?.let { - store.dispatch(ContentAction.UpdateForwardNavigationStateAction(tabId, canGoForward)) + dispatchAsync(ContentAction.UpdateForwardNavigationStateAction(tabId, canGoForward)) } } override fun onSecurityChange(secure: Boolean, host: String?, issuer: String?, certificate: X509Certificate?) { - store.dispatch( + dispatchAsync( ContentAction.UpdateSecurityInfoAction( tabId, SecurityInfoState(secure, host ?: "", issuer ?: "", certificate), @@ -169,41 +172,41 @@ internal class EngineObserver( } override fun onTrackerBlocked(tracker: Tracker) { - store.dispatch(TrackingProtectionAction.TrackerBlockedAction(tabId, tracker)) + dispatchAsync(TrackingProtectionAction.TrackerBlockedAction(tabId, tracker)) } override fun onTrackerLoaded(tracker: Tracker) { - store.dispatch(TrackingProtectionAction.TrackerLoadedAction(tabId, tracker)) + dispatchAsync(TrackingProtectionAction.TrackerLoadedAction(tabId, tracker)) } override fun onExcludedOnTrackingProtectionChange(excluded: Boolean) { - store.dispatch(TrackingProtectionAction.ToggleExclusionListAction(tabId, excluded)) + dispatchAsync(TrackingProtectionAction.ToggleExclusionListAction(tabId, excluded)) } override fun onTrackerBlockingEnabledChange(enabled: Boolean) { - store.dispatch(TrackingProtectionAction.ToggleAction(tabId, enabled)) + dispatchAsync(TrackingProtectionAction.ToggleAction(tabId, enabled)) } override fun onCookieBannerChange(status: EngineSession.CookieBannerHandlingStatus) { - store.dispatch(CookieBannerAction.UpdateStatusAction(tabId, status)) + dispatchAsync(CookieBannerAction.UpdateStatusAction(tabId, status)) } override fun onTranslatePageChange() { - store.dispatch(TranslationsAction.SetTranslateProcessingAction(tabId, isProcessing = false)) + dispatchAsync(TranslationsAction.SetTranslateProcessingAction(tabId, isProcessing = false)) } override fun onLongPress(hitResult: HitResult) { - store.dispatch( + dispatchAsync( ContentAction.UpdateHitResultAction(tabId, hitResult), ) } override fun onFind(text: String) { - store.dispatch(ContentAction.ClearFindResultsAction(tabId)) + dispatchAsync(ContentAction.ClearFindResultsAction(tabId)) } override fun onFindResult(activeMatchOrdinal: Int, numberOfMatches: Int, isDoneCounting: Boolean) { - store.dispatch( + dispatchAsync( ContentAction.AddFindResultAction( tabId, FindResultState( @@ -246,7 +249,7 @@ internal class EngineObserver( etag = response?.headers?.get(E_TAG), ) - store.dispatch( + dispatchAsync( ContentAction.UpdateDownloadAction( tabId, download, @@ -255,7 +258,7 @@ internal class EngineObserver( } override fun onDesktopModeChange(enabled: Boolean) { - store.dispatch( + dispatchAsync( ContentAction.UpdateTabDesktopMode( tabId, enabled, @@ -264,7 +267,7 @@ internal class EngineObserver( } override fun onFullScreenChange(enabled: Boolean) { - store.dispatch( + dispatchAsync( ContentAction.FullScreenChangedAction( tabId, enabled, @@ -273,7 +276,7 @@ internal class EngineObserver( } override fun onMetaViewportFitChanged(layoutInDisplayCutoutMode: Int) { - store.dispatch( + dispatchAsync( ContentAction.ViewportFitChangedAction( tabId, layoutInDisplayCutoutMode, @@ -282,7 +285,7 @@ internal class EngineObserver( } override fun onContentPermissionRequest(permissionRequest: PermissionRequest) { - store.dispatch( + dispatchAsync( ContentAction.UpdatePermissionsRequest( tabId, permissionRequest, @@ -291,7 +294,7 @@ internal class EngineObserver( } override fun onCancelContentPermissionRequest(permissionRequest: PermissionRequest) { - store.dispatch( + dispatchAsync( ContentAction.ConsumePermissionsRequest( tabId, permissionRequest, @@ -300,7 +303,7 @@ internal class EngineObserver( } override fun onAppPermissionRequest(permissionRequest: PermissionRequest) { - store.dispatch( + dispatchAsync( ContentAction.UpdateAppPermissionsRequest( tabId, permissionRequest, @@ -309,7 +312,7 @@ internal class EngineObserver( } override fun onPromptRequest(promptRequest: PromptRequest) { - store.dispatch( + dispatchAsync( ContentAction.UpdatePromptRequestAction( tabId, promptRequest, @@ -318,27 +321,27 @@ internal class EngineObserver( } override fun onPromptDismissed(promptRequest: PromptRequest) { - store.dispatch( + dispatchAsync( ContentAction.ConsumePromptRequestAction(tabId, promptRequest), ) } override fun onPromptUpdate(previousPromptRequestUid: String, promptRequest: PromptRequest) { - store.dispatch( + dispatchAsync( ContentAction.ReplacePromptRequestAction(tabId, previousPromptRequestUid, promptRequest), ) } override fun onRepostPromptCancelled() { - store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, true)) + dispatchAsync(ContentAction.UpdateRefreshCanceledStateAction(tabId, true)) } override fun onBeforeUnloadPromptDenied() { - store.dispatch(ContentAction.UpdateRefreshCanceledStateAction(tabId, true)) + dispatchAsync(ContentAction.UpdateRefreshCanceledStateAction(tabId, true)) } override fun onWindowRequest(windowRequest: WindowRequest) { - store.dispatch( + dispatchAsync( ContentAction.UpdateWindowRequestAction( tabId, windowRequest, @@ -347,13 +350,13 @@ internal class EngineObserver( } override fun onShowDynamicToolbar() { - store.dispatch( + dispatchAsync( ContentAction.UpdateExpandedToolbarStateAction(tabId, true), ) } override fun onMediaActivated(mediaSessionController: MediaSession.Controller) { - store.dispatch( + dispatchAsync( MediaSessionAction.ActivatedMediaSessionAction( tabId, mediaSessionController, @@ -362,15 +365,15 @@ internal class EngineObserver( } override fun onMediaDeactivated() { - store.dispatch(MediaSessionAction.DeactivatedMediaSessionAction(tabId)) + dispatchAsync(MediaSessionAction.DeactivatedMediaSessionAction(tabId)) } override fun onMediaMetadataChanged(metadata: MediaSession.Metadata) { - store.dispatch(MediaSessionAction.UpdateMediaMetadataAction(tabId, metadata)) + dispatchAsync(MediaSessionAction.UpdateMediaMetadataAction(tabId, metadata)) } override fun onMediaPlaybackStateChanged(playbackState: MediaSession.PlaybackState) { - store.dispatch( + dispatchAsync( MediaSessionAction.UpdateMediaPlaybackStateAction( tabId, playbackState, @@ -379,7 +382,7 @@ internal class EngineObserver( } override fun onMediaFeatureChanged(features: MediaSession.Feature) { - store.dispatch( + dispatchAsync( MediaSessionAction.UpdateMediaFeatureAction( tabId, features, @@ -388,7 +391,7 @@ internal class EngineObserver( } override fun onMediaPositionStateChanged(positionState: MediaSession.PositionState) { - store.dispatch( + dispatchAsync( MediaSessionAction.UpdateMediaPositionStateAction( tabId, positionState, @@ -397,7 +400,7 @@ internal class EngineObserver( } override fun onMediaMuteChanged(muted: Boolean) { - store.dispatch( + dispatchAsync( MediaSessionAction.UpdateMediaMutedAction( tabId, muted, @@ -409,7 +412,7 @@ internal class EngineObserver( fullscreen: Boolean, elementMetadata: MediaSession.ElementMetadata?, ) { - store.dispatch( + dispatchAsync( MediaSessionAction.UpdateMediaFullscreenAction( tabId, fullscreen, @@ -419,11 +422,11 @@ internal class EngineObserver( } override fun onWebAppManifestLoaded(manifest: WebAppManifest) { - store.dispatch(ContentAction.UpdateWebAppManifestAction(tabId, manifest)) + dispatchAsync(ContentAction.UpdateWebAppManifestAction(tabId, manifest)) } override fun onCrash() { - store.dispatch( + dispatchAsync( CrashAction.SessionCrashedAction( tabId, ), @@ -431,7 +434,7 @@ internal class EngineObserver( } override fun onProcessKilled() { - store.dispatch( + dispatchAsync( EngineAction.KillEngineSessionAction( tabId, ), @@ -439,7 +442,7 @@ internal class EngineObserver( } override fun onStateUpdated(state: EngineSessionState) { - store.dispatch( + dispatchAsync( EngineAction.UpdateEngineSessionStateAction( tabId, state, @@ -448,7 +451,7 @@ internal class EngineObserver( } override fun onRecordingStateChanged(devices: List<RecordingDevice>) { - store.dispatch( + dispatchAsync( ContentAction.SetRecordingDevices( tabId, devices, @@ -457,7 +460,7 @@ internal class EngineObserver( } override fun onHistoryStateChanged(historyList: List<HistoryItem>, currentIndex: Int) { - store.dispatch( + dispatchAsync( ContentAction.UpdateHistoryStateAction( tabId, historyList, @@ -467,46 +470,50 @@ internal class EngineObserver( } override fun onSaveToPdfException(throwable: Throwable) { - store.dispatch(EngineAction.SaveToPdfExceptionAction(tabId, throwable)) + dispatchAsync(EngineAction.SaveToPdfExceptionAction(tabId, throwable)) } override fun onPrintFinish() { - store.dispatch(EngineAction.PrintContentCompletedAction(tabId)) + dispatchAsync(EngineAction.PrintContentCompletedAction(tabId)) } override fun onPrintException(isPrint: Boolean, throwable: Throwable) { - store.dispatch(EngineAction.PrintContentExceptionAction(tabId, isPrint, throwable)) + dispatchAsync(EngineAction.PrintContentExceptionAction(tabId, isPrint, throwable)) } override fun onSaveToPdfComplete() { - store.dispatch(EngineAction.SaveToPdfCompleteAction(tabId)) + dispatchAsync(EngineAction.SaveToPdfCompleteAction(tabId)) } override fun onCheckForFormData(containsFormData: Boolean, adjustPriority: Boolean) { - store.dispatch(ContentAction.UpdateHasFormDataAction(tabId, containsFormData, adjustPriority)) + dispatchAsync(ContentAction.UpdateHasFormDataAction(tabId, containsFormData, adjustPriority)) } override fun onCheckForFormDataException(throwable: Throwable) { - store.dispatch(ContentAction.CheckForFormDataExceptionAction(tabId, throwable)) + dispatchAsync(ContentAction.CheckForFormDataExceptionAction(tabId, throwable)) } override fun onTranslateExpected() { - store.dispatch(TranslationsAction.TranslateExpectedAction(tabId)) + dispatchAsync(TranslationsAction.TranslateExpectedAction(tabId)) } override fun onTranslateOffer() { - store.dispatch(TranslationsAction.TranslateOfferAction(tabId = tabId, isOfferTranslate = true)) + dispatchAsync(TranslationsAction.TranslateOfferAction(tabId = tabId, isOfferTranslate = true)) } override fun onTranslateStateChange(state: TranslationEngineState) { - store.dispatch(TranslationsAction.TranslateStateChangeAction(tabId, state)) + dispatchAsync(TranslationsAction.TranslateStateChangeAction(tabId, state)) } override fun onTranslateComplete(operation: TranslationOperation) { - store.dispatch(TranslationsAction.TranslateSuccessAction(tabId, operation)) + dispatchAsync(TranslationsAction.TranslateSuccessAction(tabId, operation)) } override fun onTranslateException(operation: TranslationOperation, translationError: TranslationError) { - store.dispatch(TranslationsAction.TranslateExceptionAction(tabId, operation, translationError)) + dispatchAsync(TranslationsAction.TranslateExceptionAction(tabId, operation, translationError)) + } + + private fun dispatchAsync(action: BrowserAction) = scope.launch { + store.dispatch(action) } } diff --git a/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/LinkingMiddleware.kt b/mobile/android/android-components/components/browser/state/src/main/java/mozilla/components/browser/state/engine/middleware/LinkingMiddleware.kt @@ -83,7 +83,7 @@ internal class LinkingMiddleware( skipLoading: Boolean = true, includeParent: Boolean, ): Pair<String, EngineObserver> { - val observer = EngineObserver(tab.id, context.store) + val observer = EngineObserver(tab.id, context.store, scope) engineSession.register(observer) if (skipLoading) { diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/EngineObserverTest.kt @@ -7,6 +7,9 @@ package mozilla.components.browser.state.engine import android.content.Intent import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.ContentAction @@ -60,11 +63,12 @@ import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.`when` +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class EngineObserverTest { // TO DO: add tests for product URL after a test endpoint is implemented in desktop (Bug 1846341) @Test - fun engineSessionObserver() { + fun engineSessionObserver() = runTest { val engineSession = object : EngineSession() { override val settings: Settings = mock() override fun goBack(userInteraction: Boolean) {} @@ -141,9 +145,10 @@ class EngineObserverTest { val store = BrowserStore() store.dispatch(TabListAction.AddTabAction(createTab("https://www.mozilla.org", id = "mozilla"))) - engineSession.register(EngineObserver("mozilla", store)) + engineSession.register(createEngineObserver(store = store, scope = this)) engineSession.loadUrl("http://mozilla.org") engineSession.toggleDesktopMode(true) + advanceUntilIdle() assertEquals("http://mozilla.org", store.state.selectedTab?.content?.url) assertEquals(100, store.state.selectedTab?.content?.progress) @@ -156,7 +161,7 @@ class EngineObserverTest { } @Test - fun engineSessionObserverWithSecurityChanges() { + fun engineSessionObserverWithSecurityChanges() = runTest { val engineSession = object : EngineSession() { override val settings: Settings = mock() override fun goBack(userInteraction: Boolean) {} @@ -232,17 +237,21 @@ class EngineObserverTest { ), ) - engineSession.register(EngineObserver("mozilla", store)) + engineSession.register(createEngineObserver(store = store, scope = this)) engineSession.loadUrl("http://mozilla.org") + advanceUntilIdle() + assertEquals(SecurityInfoState(secure = false), store.state.tabs[0].content.securityInfo) engineSession.loadUrl("https://mozilla.org") + advanceUntilIdle() + assertEquals(SecurityInfoState(secure = true, "host", "issuer"), store.state.tabs[0].content.securityInfo) } @Test - fun engineSessionObserverWithTrackingProtection() { + fun engineSessionObserverWithTrackingProtection() = runTest { val engineSession = object : EngineSession() { override val settings: Settings = mock() override fun goBack(userInteraction: Boolean) {} @@ -310,23 +319,25 @@ class EngineObserverTest { ), ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) engineSession.register(observer) val tracker1 = Tracker("tracker1", emptyList()) val tracker2 = Tracker("tracker2", emptyList()) observer.onTrackerBlocked(tracker1) + advanceUntilIdle() assertEquals(listOf(tracker1), store.state.tabs[0].trackingProtection.blockedTrackers) observer.onTrackerBlocked(tracker2) + advanceUntilIdle() assertEquals(listOf(tracker1, tracker2), store.state.tabs[0].trackingProtection.blockedTrackers) } @Test - fun `WHEN the first page load is complete, set the translations initialized`() { + fun `WHEN the first page load is complete, set the translations initialized`() = runTest { val engineSession = object : EngineSession() { override val settings: Settings = mock() override fun goBack(userInteraction: Boolean) {} @@ -400,15 +411,17 @@ class EngineObserverTest { assertEquals(false, store.state.translationsInitialized) - engineSession.register(EngineObserver("mozilla", store)) + engineSession.register(createEngineObserver(store = store, scope = this)) engineSession.loadUrl("https://mozilla.org") + advanceUntilIdle() + assertEquals(true, store.state.translationsInitialized) } @Test - fun `WHEN the first page load is not complete, do not set the translations initialized`() { + fun `WHEN the first page load is not complete, do not set the translations initialized`() = runTest { val engineSession = object : EngineSession() { override val settings: Settings = mock() override fun goBack(userInteraction: Boolean) {} @@ -482,7 +495,7 @@ class EngineObserverTest { assertEquals(false, store.state.translationsInitialized) - engineSession.register(EngineObserver("mozilla", store)) + engineSession.register(createEngineObserver(store = store, scope = this)) engineSession.loadUrl("https://mozilla.org") @@ -490,13 +503,13 @@ class EngineObserverTest { } @Test - fun `Do not initialize the translations flow if the page load is not complete`() { + fun `Do not initialize the translations flow if the page load is not complete`() = runTest { val store: BrowserStore = mock() val state: BrowserState = mock() `when`(store.state).thenReturn(state) `when`(store.state.translationsInitialized).thenReturn(false) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onProgress(80) @@ -506,15 +519,16 @@ class EngineObserverTest { } @Test - fun `Initialize the translations flow if page load is complete and it is not yet initialized`() { + fun `Initialize the translations flow if page load is complete and it is not yet initialized`() = runTest { val store: BrowserStore = mock() val state: BrowserState = mock() `when`(store.state).thenReturn(state) `when`(store.state.translationsInitialized).thenReturn(false) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onProgress(100) + advanceUntilIdle() verify(store).dispatch( TranslationsAction.InitTranslationsBrowserState, @@ -522,13 +536,13 @@ class EngineObserverTest { } @Test - fun `Do not initialize the translations flow if it is already initialized`() { + fun `Do not initialize the translations flow if it is already initialized`() = runTest { val store: BrowserStore = mock() val state: BrowserState = mock() `when`(store.state).thenReturn(state) `when`(store.state.translationsInitialized).thenReturn(true) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onProgress(100) @@ -538,11 +552,12 @@ class EngineObserverTest { } @Test - fun engineSessionObserverExcludedOnTrackingProtection() { + fun engineSessionObserverExcludedOnTrackingProtection() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onExcludedOnTrackingProtectionChange(true) + advanceUntilIdle() verify(store).dispatch( TrackingProtectionAction.ToggleExclusionListAction( @@ -553,11 +568,12 @@ class EngineObserverTest { } @Test - fun `WHEN onCookieBannerChange is called THEN dispatch an CookieBannerAction UpdateStatusAction`() { + fun `WHEN onCookieBannerChange is called THEN dispatch an CookieBannerAction UpdateStatusAction`() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onCookieBannerChange(HANDLED) + advanceUntilIdle() verify(store).dispatch( CookieBannerAction.UpdateStatusAction( @@ -568,11 +584,12 @@ class EngineObserverTest { } @Test - fun `WHEN onTranslatePageChange is called THEN dispatch a TranslationsAction SetTranslateProcessingAction`() { + fun `WHEN onTranslatePageChange is called THEN dispatch a TranslationsAction SetTranslateProcessingAction`() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onTranslatePageChange() + advanceUntilIdle() verify(store).dispatch( TranslationsAction.SetTranslateProcessingAction( @@ -583,11 +600,12 @@ class EngineObserverTest { } @Test - fun `WHEN onTranslateComplete is called THEN dispatch a TranslationsAction TranslateSuccessAction`() { + fun `WHEN onTranslateComplete is called THEN dispatch a TranslationsAction TranslateSuccessAction`() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onTranslateComplete(operation = TranslationOperation.TRANSLATE) + advanceUntilIdle() verify(store).dispatch( TranslationsAction.TranslateSuccessAction("mozilla", operation = TranslationOperation.TRANSLATE), @@ -595,12 +613,13 @@ class EngineObserverTest { } @Test - fun `WHEN onTranslateException is called THEN dispatch a TranslationsAction TranslateExceptionAction`() { + fun `WHEN onTranslateException is called THEN dispatch a TranslationsAction TranslateExceptionAction`() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val exception = TranslationError.UnknownError(Exception()) observer.onTranslateException(operation = TranslationOperation.TRANSLATE, exception) + advanceUntilIdle() verify(store).dispatch( TranslationsAction.TranslateExceptionAction("mozilla", operation = TranslationOperation.TRANSLATE, exception), @@ -608,7 +627,7 @@ class EngineObserverTest { } @Test - fun engineObserverClearsWebsiteTitleIfNewPageStartsLoading() { + fun engineObserverClearsWebsiteTitleIfNewPageStartsLoading() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -621,18 +640,20 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onTitleChange("Mozilla") + advanceUntilIdle() assertEquals("Mozilla", store.state.tabs[0].content.title) observer.onLocationChange("https://getpocket.com", false) + advanceUntilIdle() assertEquals("", store.state.tabs[0].content.title) } @Test - fun `EngineObserver does not clear title if the URL did not change`() { + fun `EngineObserver does not clear title if the URL did not change`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -645,19 +666,21 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onTitleChange("Mozilla") + advanceUntilIdle() assertEquals("Mozilla", store.state.tabs[0].content.title) observer.onLocationChange("https://www.mozilla.org", false) + advanceUntilIdle() assertEquals("Mozilla", store.state.tabs[0].content.title) } @Test - fun `EngineObserver does not clear title if the URL changes hash`() { + fun `EngineObserver does not clear title if the URL changes hash`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -670,19 +693,21 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onTitleChange("Mozilla") + advanceUntilIdle() assertEquals("Mozilla", store.state.tabs[0].content.title) observer.onLocationChange("https://www.mozilla.org/#something", false) + advanceUntilIdle() assertEquals("Mozilla", store.state.tabs[0].content.title) } @Test - fun `EngineObserver clears previewImageUrl if new page starts loading`() { + fun `EngineObserver clears previewImageUrl if new page starts loading`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -696,18 +721,20 @@ class EngineObserverTest { ) val previewImageUrl = "https://test.com/og-image-url" - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onPreviewImageChange(previewImageUrl) + advanceUntilIdle() assertEquals(previewImageUrl, store.state.tabs[0].content.previewImageUrl) observer.onLocationChange("https://getpocket.com", false) + advanceUntilIdle() assertNull(store.state.tabs[0].content.previewImageUrl) } @Test - fun `EngineObserver does not clear previewImageUrl if the URL did not change`() { + fun `EngineObserver does not clear previewImageUrl if the URL did not change`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -721,23 +748,26 @@ class EngineObserverTest { ) val previewImageUrl = "https://test.com/og-image-url" - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onPreviewImageChange(previewImageUrl) + advanceUntilIdle() assertEquals(previewImageUrl, store.state.tabs[0].content.previewImageUrl) observer.onLocationChange("https://www.mozilla.org", false) + advanceUntilIdle() assertEquals(previewImageUrl, store.state.tabs[0].content.previewImageUrl) observer.onLocationChange("https://www.mozilla.org/#something", false) + advanceUntilIdle() assertEquals(previewImageUrl, store.state.tabs[0].content.previewImageUrl) } @Test - fun engineObserverClearsBlockedTrackersIfNewPageStartsLoading() { + fun engineObserverClearsBlockedTrackersIfNewPageStartsLoading() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -749,23 +779,25 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val tracker1 = Tracker("tracker1") val tracker2 = Tracker("tracker2") observer.onTrackerBlocked(tracker1) observer.onTrackerBlocked(tracker2) + advanceUntilIdle() assertEquals(listOf(tracker1, tracker2), store.state.tabs[0].trackingProtection.blockedTrackers) observer.onLoadingStateChange(true) + advanceUntilIdle() assertEquals(emptyList<String>(), store.state.tabs[0].trackingProtection.blockedTrackers) } @Test - fun engineObserverClearsLoadedTrackersIfNewPageStartsLoading() { + fun engineObserverClearsLoadedTrackersIfNewPageStartsLoading() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -777,23 +809,25 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val tracker1 = Tracker("tracker1") val tracker2 = Tracker("tracker2") observer.onTrackerLoaded(tracker1) observer.onTrackerLoaded(tracker2) + advanceUntilIdle() assertEquals(listOf(tracker1, tracker2), store.state.tabs[0].trackingProtection.loadedTrackers) observer.onLoadingStateChange(true) + advanceUntilIdle() assertEquals(emptyList<String>(), store.state.tabs[0].trackingProtection.loadedTrackers) } @Test - fun engineObserverClearsWebAppManifestIfNewPageStartsLoading() { + fun engineObserverClearsWebAppManifestIfNewPageStartsLoading() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -807,19 +841,21 @@ class EngineObserverTest { val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://mozilla.org") - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onWebAppManifestLoaded(manifest) + advanceUntilIdle() assertEquals(manifest, store.state.tabs[0].content.webAppManifest) observer.onLocationChange("https://getpocket.com", false) + advanceUntilIdle() assertNull(store.state.tabs[0].content.webAppManifest) } @Test - fun engineObserverClearsContentPermissionRequestIfNewPageStartsLoading() { + fun engineObserverClearsContentPermissionRequestIfNewPageStartsLoading() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -831,21 +867,23 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val request: PermissionRequest = mock() store.dispatch(ContentAction.UpdatePermissionsRequest("mozilla", request)) + advanceUntilIdle() assertEquals(listOf(request), store.state.tabs[0].content.permissionRequestsList) observer.onLocationChange("https://getpocket.com", false) + advanceUntilIdle() assertEquals(emptyList<PermissionRequest>(), store.state.tabs[0].content.permissionRequestsList) } @Test - fun engineObserverDoesNotClearContentPermissionRequestIfSamePageStartsLoading() { + fun engineObserverDoesNotClearContentPermissionRequestIfSamePageStartsLoading() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -857,7 +895,7 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val request: PermissionRequest = mock() @@ -871,7 +909,7 @@ class EngineObserverTest { } @Test - fun engineObserverDoesNotClearWebAppManifestIfNewPageInStartUrlScope() { + fun engineObserverDoesNotClearWebAppManifestIfNewPageInStartUrlScope() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -885,9 +923,10 @@ class EngineObserverTest { val manifest = WebAppManifest(name = "Mozilla", startUrl = "https://www.mozilla.org") - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onWebAppManifestLoaded(manifest) + advanceUntilIdle() assertEquals(manifest, store.state.tabs[0].content.webAppManifest) @@ -897,7 +936,7 @@ class EngineObserverTest { } @Test - fun engineObserverDoesNotClearWebAppManifestIfNewPageInScope() { + fun engineObserverDoesNotClearWebAppManifestIfNewPageInScope() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -915,22 +954,26 @@ class EngineObserverTest { scope = "https://www.mozilla.org/hello/", ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onWebAppManifestLoaded(manifest) + advanceUntilIdle() assertEquals(manifest, store.state.tabs[0].content.webAppManifest) observer.onLocationChange("https://www.mozilla.org/hello/page2.html", false) + advanceUntilIdle() assertEquals(manifest, store.state.tabs[0].content.webAppManifest) observer.onLocationChange("https://www.mozilla.org/hello.html", false) + advanceUntilIdle() + assertNull(store.state.tabs[0].content.webAppManifest) } @Test - fun engineObserverPassingHitResult() { + fun engineObserverPassingHitResult() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -942,76 +985,88 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val hitResult = HitResult.UNKNOWN("data://foobar") observer.onLongPress(hitResult) + advanceUntilIdle() assertEquals(hitResult, store.state.tabs[0].content.hitResult) } @Test - fun engineObserverClearsFindResults() { + fun engineObserverClearsFindResults() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("tab", store) + val observer = createEngineObserver(tabId = "tab", store = store, scope = this) observer.onFindResult(0, 1, false) + advanceUntilIdle() middleware.assertFirstAction(ContentAction.AddFindResultAction::class) { action -> assertEquals("tab", action.sessionId) assertEquals(FindResultState(0, 1, false), action.findResult) } observer.onFind("mozilla") + advanceUntilIdle() middleware.assertLastAction(ContentAction.ClearFindResultsAction::class) { action -> assertEquals("tab", action.sessionId) } } @Test - fun engineObserverClearsFindResultIfNewPageStartsLoading() { + fun engineObserverClearsFindResultIfNewPageStartsLoading() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("tab-id", store) + val observer = createEngineObserver(tabId = "tab-id", store = store, scope = this) observer.onFindResult(0, 1, false) + advanceUntilIdle() middleware.assertFirstAction(ContentAction.AddFindResultAction::class) { action -> assertEquals("tab-id", action.sessionId) assertEquals(FindResultState(0, 1, false), action.findResult) } observer.onFindResult(1, 2, true) + advanceUntilIdle() middleware.assertLastAction(ContentAction.AddFindResultAction::class) { action -> assertEquals("tab-id", action.sessionId) assertEquals(FindResultState(1, 2, true), action.findResult) } observer.onLoadingStateChange(true) + advanceUntilIdle() middleware.assertLastAction(ContentAction.ClearFindResultsAction::class) { action -> assertEquals("tab-id", action.sessionId) } } @Test - fun engineObserverClearsRefreshCanceledIfNewPageStartsLoading() { + fun engineObserverClearsRefreshCanceledIfNewPageStartsLoading() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onRepostPromptCancelled() + advanceUntilIdle() middleware.assertFirstAction(ContentAction.UpdateRefreshCanceledStateAction::class) { action -> assertEquals("tab-id", action.sessionId) assertTrue(action.refreshCanceled) } observer.onLoadingStateChange(true) + advanceUntilIdle() middleware.assertLastAction(ContentAction.UpdateRefreshCanceledStateAction::class) { action -> assertEquals("tab-id", action.sessionId) assertFalse(action.refreshCanceled) @@ -1019,38 +1074,55 @@ class EngineObserverTest { } @Test - fun engineObserverHandlesOnRepostPromptCancelled() { + fun engineObserverHandlesOnRepostPromptCancelled() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onRepostPromptCancelled() + advanceUntilIdle() + verify(store).dispatch(ContentAction.UpdateRefreshCanceledStateAction("tab-id", true)) } @Test - fun engineObserverHandlesOnBeforeUnloadDenied() { + fun engineObserverHandlesOnBeforeUnloadDenied() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onBeforeUnloadPromptDenied() + advanceUntilIdle() verify(store).dispatch(ContentAction.UpdateRefreshCanceledStateAction("tab-id", true)) } @Test - fun engineObserverNotifiesFullscreenMode() { + fun engineObserverNotifiesFullscreenMode() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onFullScreenChange(true) + advanceUntilIdle() middleware.assertFirstAction(ContentAction.FullScreenChangedAction::class) { action -> assertEquals("tab-id", action.sessionId) assertTrue(action.fullScreenEnabled) } observer.onFullScreenChange(false) + advanceUntilIdle() middleware.assertLastAction(ContentAction.FullScreenChangedAction::class) { action -> assertEquals("tab-id", action.sessionId) assertFalse(action.fullScreenEnabled) @@ -1058,20 +1130,26 @@ class EngineObserverTest { } @Test - fun engineObserverNotifiesDesktopMode() { + fun engineObserverNotifiesDesktopMode() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onDesktopModeChange(true) + advanceUntilIdle() middleware.assertFirstAction(ContentAction.UpdateTabDesktopMode::class) { action -> assertEquals("tab-id", action.sessionId) assertTrue(action.enabled) } observer.onDesktopModeChange(false) + advanceUntilIdle() middleware.assertLastAction(ContentAction.UpdateTabDesktopMode::class) { action -> assertEquals("tab-id", action.sessionId) assertFalse(action.enabled) @@ -1079,14 +1157,20 @@ class EngineObserverTest { } @Test - fun engineObserverNotifiesMetaViewportFitChange() { + fun engineObserverNotifiesMetaViewportFitChange() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) + advanceUntilIdle() + middleware.assertFirstAction(ContentAction.ViewportFitChangedAction::class) { action -> assertEquals("tab-id", action.sessionId) assertEquals( @@ -1096,6 +1180,8 @@ class EngineObserverTest { } observer.onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES) + advanceUntilIdle() + middleware.assertLastAction(ContentAction.ViewportFitChangedAction::class) { action -> assertEquals("tab-id", action.sessionId) assertEquals( @@ -1105,6 +1191,8 @@ class EngineObserverTest { } observer.onMetaViewportFitChanged(WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER) + advanceUntilIdle() + middleware.assertLastAction(ContentAction.ViewportFitChangedAction::class) { action -> assertEquals("tab-id", action.sessionId) assertEquals( @@ -1114,6 +1202,8 @@ class EngineObserverTest { } observer.onMetaViewportFitChanged(123) + advanceUntilIdle() + middleware.assertLastAction(ContentAction.ViewportFitChangedAction::class) { action -> assertEquals("tab-id", action.sessionId) assertEquals(123, action.layoutInDisplayCutoutMode) @@ -1121,7 +1211,7 @@ class EngineObserverTest { } @Test - fun engineObserverNotifiesWebAppManifest() { + fun engineObserverNotifiesWebAppManifest() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1133,13 +1223,14 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val manifest = WebAppManifest( name = "Minimal", startUrl = "/", ) observer.onWebAppManifestLoaded(manifest) + advanceUntilIdle() assertEquals(manifest, store.state.tabs[0].content.webAppManifest) } @@ -1148,12 +1239,18 @@ class EngineObserverTest { fun engineSessionObserverWithContentPermissionRequests() = runTest { val permissionRequest: PermissionRequest = mock() val store: BrowserStore = mock() - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) val action = ContentAction.UpdatePermissionsRequest( "tab-id", permissionRequest, ) observer.onContentPermissionRequest(permissionRequest) + advanceUntilIdle() + verify(store).dispatch(action) } @@ -1161,23 +1258,35 @@ class EngineObserverTest { fun engineSessionObserverWithAppPermissionRequests() = runTest { val permissionRequest: PermissionRequest = mock() val store: BrowserStore = mock() - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) val action = ContentAction.UpdateAppPermissionsRequest( "tab-id", permissionRequest, ) observer.onAppPermissionRequest(permissionRequest) + advanceUntilIdle() + verify(store).dispatch(action) } @Test - fun engineObserverHandlesPromptRequest() { + fun engineObserverHandlesPromptRequest() = runTest { val promptRequest: PromptRequest = mock<PromptRequest.SingleChoice>() val store: BrowserStore = mock() - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onPromptRequest(promptRequest) + advanceUntilIdle() + verify(store).dispatch( ContentAction.UpdatePromptRequestAction( "tab-id", @@ -1187,13 +1296,19 @@ class EngineObserverTest { } @Test - fun engineObserverHandlesOnPromptUpdate() { + fun engineObserverHandlesOnPromptUpdate() = runTest { val promptRequest: PromptRequest = mock<PromptRequest.SingleChoice>() val store: BrowserStore = mock() - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) val previousPromptUID = "prompt-uid" observer.onPromptUpdate(previousPromptUID, promptRequest) + advanceUntilIdle() + verify(store).dispatch( ContentAction.ReplacePromptRequestAction( "tab-id", @@ -1204,13 +1319,19 @@ class EngineObserverTest { } @Test - fun engineObserverHandlesWindowRequest() { + fun engineObserverHandlesWindowRequest() = runTest { val windowRequest: WindowRequest = mock() val store: BrowserStore = mock() whenever(store.state).thenReturn(mock()) - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onWindowRequest(windowRequest) + advanceUntilIdle() + verify(store).dispatch( ContentAction.UpdateWindowRequestAction( "tab-id", @@ -1220,12 +1341,18 @@ class EngineObserverTest { } @Test - fun engineObserverHandlesFirstContentfulPaint() { + fun engineObserverHandlesFirstContentfulPaint() = runTest { val store: BrowserStore = mock() whenever(store.state).thenReturn(mock()) - val observer = EngineObserver("tab-id", store) + val observer = createEngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onFirstContentfulPaint() + advanceUntilIdle() + verify(store).dispatch( ContentAction.UpdateFirstContentfulPaintStateAction( "tab-id", @@ -1235,12 +1362,18 @@ class EngineObserverTest { } @Test - fun engineObserverHandlesPaintStatusReset() { + fun engineObserverHandlesPaintStatusReset() = runTest { val store: BrowserStore = mock() whenever(store.state).thenReturn(mock()) - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onPaintStatusReset() + advanceUntilIdle() + verify(store).dispatch( ContentAction.UpdateFirstContentfulPaintStateAction( "tab-id", @@ -1250,16 +1383,22 @@ class EngineObserverTest { } @Test - fun engineObserverHandlesOnShowDynamicToolbar() { + fun engineObserverHandlesOnShowDynamicToolbar() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("tab-id", store) + val observer = EngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onShowDynamicToolbar() + advanceUntilIdle() + verify(store).dispatch(ContentAction.UpdateExpandedToolbarStateAction("tab-id", true)) } @Test - fun `onMediaActivated will update the store`() { + fun `onMediaActivated will update the store`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1271,12 +1410,17 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver( + tabId = "mozilla", + store = store, + scope = this, + ) val mediaSessionController: MediaSession.Controller = mock() assertNull(store.state.tabs[0].mediaSessionState) observer.onMediaActivated(mediaSessionController) + advanceUntilIdle() val observedMediaSessionState = store.state.tabs[0].mediaSessionState assertNotNull(observedMediaSessionState) @@ -1284,7 +1428,7 @@ class EngineObserverTest { } @Test - fun `onMediaDeactivated will update the store`() { + fun `onMediaDeactivated will update the store`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1299,18 +1443,19 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) assertNotNull(store.state.findTab("mozilla")?.mediaSessionState) observer.onMediaDeactivated() + advanceUntilIdle() val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState assertNull(observedMediaSessionState) } @Test - fun `onMediaMetadataChanged will update the store`() { + fun `onMediaMetadataChanged will update the store`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1325,12 +1470,13 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val mediaSessionController: MediaSession.Controller = mock() val metaData: MediaSession.Metadata = mock() observer.onMediaActivated(mediaSessionController) observer.onMediaMetadataChanged(metaData) + advanceUntilIdle() val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState assertNotNull(observedMediaSessionState) @@ -1339,7 +1485,7 @@ class EngineObserverTest { } @Test - fun `onMediaPlaybackStateChanged will update the store`() { + fun `onMediaPlaybackStateChanged will update the store`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1354,12 +1500,13 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val mediaSessionController: MediaSession.Controller = mock() val playbackState: MediaSession.PlaybackState = mock() observer.onMediaActivated(mediaSessionController) observer.onMediaPlaybackStateChanged(playbackState) + advanceUntilIdle() val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState assertNotNull(observedMediaSessionState) @@ -1368,7 +1515,7 @@ class EngineObserverTest { } @Test - fun `onMediaFeatureChanged will update the store`() { + fun `onMediaFeatureChanged will update the store`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1383,12 +1530,13 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val mediaSessionController: MediaSession.Controller = mock() val features: MediaSession.Feature = mock() observer.onMediaActivated(mediaSessionController) observer.onMediaFeatureChanged(features) + advanceUntilIdle() val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState assertNotNull(observedMediaSessionState) @@ -1397,7 +1545,7 @@ class EngineObserverTest { } @Test - fun `onMediaPositionStateChanged will update the store`() { + fun `onMediaPositionStateChanged will update the store`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1412,12 +1560,13 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val mediaSessionController: MediaSession.Controller = mock() val positionState: MediaSession.PositionState = mock() observer.onMediaActivated(mediaSessionController) observer.onMediaPositionStateChanged(positionState) + advanceUntilIdle() val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState assertNotNull(observedMediaSessionState) @@ -1426,7 +1575,7 @@ class EngineObserverTest { } @Test - fun `onMediaMuteChanged will update the store`() { + fun `onMediaMuteChanged will update the store`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1441,11 +1590,12 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val mediaSessionController: MediaSession.Controller = mock() observer.onMediaActivated(mediaSessionController) observer.onMediaMuteChanged(true) + advanceUntilIdle() val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState assertNotNull(observedMediaSessionState) @@ -1454,7 +1604,7 @@ class EngineObserverTest { } @Test - fun `onMediaFullscreenChanged will update the store`() { + fun `onMediaFullscreenChanged will update the store`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1469,12 +1619,13 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val mediaSessionController: MediaSession.Controller = mock() val elementMetadata: MediaSession.ElementMetadata = mock() observer.onMediaActivated(mediaSessionController) observer.onMediaFullscreenChanged(true, elementMetadata) + advanceUntilIdle() val observedMediaSessionState = store.state.findTab("mozilla")?.mediaSessionState assertNotNull(observedMediaSessionState) @@ -1484,7 +1635,7 @@ class EngineObserverTest { } @Test - fun `updates are ignored when media session is deactivated`() { + fun `updates are ignored when media session is deactivated`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1496,7 +1647,7 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) val elementMetadata: MediaSession.ElementMetadata = mock() observer.onMediaFullscreenChanged(true, elementMetadata) @@ -1508,7 +1659,7 @@ class EngineObserverTest { } @Test - fun `onExternalResource will update the store`() { + fun `onExternalResource will update the store`() = runTest { val response = mock<Response> { `when`(headers).thenReturn(MutableHeaders(listOf(Header(E_TAG, "12345")))) } @@ -1527,7 +1678,7 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("mozilla", store) + val observer = createEngineObserver(store = store, scope = this) observer.onExternalResource( url = "mozilla.org/file.txt", @@ -1538,6 +1689,7 @@ class EngineObserverTest { contentLength = 100L, response = response, ) + advanceUntilIdle() val tab = store.state.findTab("mozilla")!! @@ -1552,7 +1704,7 @@ class EngineObserverTest { } @Test - fun `onExternalResource with negative contentLength`() { + fun `onExternalResource with negative contentLength`() = runTest { val store = BrowserStore( initialState = BrowserState( tabs = listOf( @@ -1567,7 +1719,11 @@ class EngineObserverTest { ), ) - val observer = EngineObserver("test-tab", store) + val observer = createEngineObserver( + tabId = "test-tab", + store = store, + scope = this, + ) observer.onExternalResource(url = "mozilla.org/file.txt", contentLength = -1) @@ -1577,11 +1733,16 @@ class EngineObserverTest { } @Test - fun `onCrashStateChanged will update session and notify observer`() { + fun `onCrashStateChanged will update session and notify observer`() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onCrash() + advanceUntilIdle() verify(store).dispatch( CrashAction.SessionCrashedAction( @@ -1591,20 +1752,25 @@ class EngineObserverTest { } @Test - fun `onLocationChange does not clear search terms`() { + fun `onLocationChange does not clear search terms`() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onLocationChange("https://www.mozilla.org/en-US/", false) + advanceUntilIdle() middleware.assertNotDispatched(ContentAction.UpdateSearchTermsAction::class) } @Test - fun `onLoadRequest clears search terms for requests triggered by web content`() { + fun `onLoadRequest clears search terms for requests triggered by web content`() = runTest { val url = "https://www.mozilla.org" val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() @@ -1612,8 +1778,13 @@ class EngineObserverTest { middleware = listOf(middleware), ) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onLoadRequest(url = url, triggeredByRedirect = false, triggeredByWebContent = true) + advanceUntilIdle() middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action -> assertEquals("", action.searchTerms) @@ -1623,12 +1794,17 @@ class EngineObserverTest { @Test @Suppress("DEPRECATION") // Session observable is deprecated - fun `onLoadRequest notifies session observers`() { + fun `onLoadRequest notifies session observers`() = runTest { val url = "https://www.mozilla.org" val store: BrowserStore = mock() - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onLoadRequest(url = url, triggeredByRedirect = true, triggeredByWebContent = false) + advanceUntilIdle() verify(store) .dispatch( @@ -1640,7 +1816,7 @@ class EngineObserverTest { } @Test - fun `onLoadRequest does not clear search terms for requests not triggered by user interacting with web content`() { + fun `onLoadRequest does not clear search terms for requests not triggered by user interacting with web content`() = runTest { val url = "https://www.mozilla.org" val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() @@ -1648,33 +1824,48 @@ class EngineObserverTest { middleware = listOf(middleware), ) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onLoadRequest(url = url, triggeredByRedirect = false, triggeredByWebContent = false) + advanceUntilIdle() middleware.assertNotDispatched(ContentAction.UpdateSearchTermsAction::class) } @Test - fun `onLaunchIntentRequest dispatches UpdateAppIntentAction`() { + fun `onLaunchIntentRequest dispatches UpdateAppIntentAction`() = runTest { val url = "https://www.mozilla.org" val store: BrowserStore = mock() - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) val intent: Intent = mock() observer.onLaunchIntentRequest(url = url, appIntent = intent, fallbackUrl = null, appName = null) + advanceUntilIdle() verify(store).dispatch(ContentAction.UpdateAppIntentAction("test-id", AppIntentState(url, intent, null, null))) } @Test - fun `onNavigateBack clears search terms when navigating back`() { + fun `onNavigateBack clears search terms when navigating back`() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onNavigateBack() + advanceUntilIdle() middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action -> assertEquals("", action.searchTerms) @@ -1683,14 +1874,19 @@ class EngineObserverTest { } @Test - fun `WHEN navigating forward THEN search terms are cleared`() { + fun `WHEN navigating forward THEN search terms are cleared`() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onNavigateForward() + advanceUntilIdle() middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action -> assertEquals("", action.searchTerms) @@ -1699,14 +1895,19 @@ class EngineObserverTest { } @Test - fun `WHEN navigating to history index THEN search terms are cleared`() { + fun `WHEN navigating to history index THEN search terms are cleared`() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onGotoHistoryIndex() + advanceUntilIdle() middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action -> assertEquals("", action.searchTerms) @@ -1715,14 +1916,19 @@ class EngineObserverTest { } @Test - fun `WHEN loading data THEN the search terms are cleared`() { + fun `WHEN loading data THEN the search terms are cleared`() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( middleware = listOf(middleware), ) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onLoadData() + advanceUntilIdle() middleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action -> assertEquals("", action.searchTerms) @@ -1731,7 +1937,7 @@ class EngineObserverTest { } @Test - fun `GIVEN a search is not performed WHEN loading the URL THEN the search terms are cleared`() { + fun `GIVEN a search is not performed WHEN loading the URL THEN the search terms are cleared`() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( initialState = BrowserState( @@ -1744,8 +1950,13 @@ class EngineObserverTest { store.dispatch(ContentAction.UpdateIsSearchAction("mozilla", false)) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onLoadUrl() + advanceUntilIdle() middleware.assertLastAction(ContentAction.UpdateSearchTermsAction::class) { action -> assertEquals("", action.searchTerms) @@ -1754,7 +1965,7 @@ class EngineObserverTest { } @Test - fun `GIVEN a search is performed WHEN loading the URL THEN the search terms are cleared`() { + fun `GIVEN a search is performed WHEN loading the URL THEN the search terms are cleared`() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( initialState = BrowserState( @@ -1767,8 +1978,13 @@ class EngineObserverTest { store.dispatch(ContentAction.UpdateIsSearchAction("test-id", true)) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onLoadUrl() + advanceUntilIdle() middleware.assertLastAction(ContentAction.UpdateIsSearchAction::class) { action -> assertEquals(false, action.isSearch) @@ -1777,7 +1993,7 @@ class EngineObserverTest { } @Test - fun `GIVEN a search is performed WHEN the location is changed without user interaction THEN the search terms are not cleared`() { + fun `GIVEN a search is performed WHEN the location is changed without user interaction THEN the search terms are not cleared`() = runTest { val middleware = CaptureActionsMiddleware<BrowserState, BrowserAction>() val store = BrowserStore( initialState = BrowserState( @@ -1790,18 +2006,29 @@ class EngineObserverTest { store.dispatch(ContentAction.UpdateIsSearchAction("test-id", true)) - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onLocationChange("testUrl", false) + advanceUntilIdle() middleware.assertNotDispatched(ContentAction.UpdateSearchTermsAction::class) } @Test - fun `onHistoryStateChanged dispatches UpdateHistoryStateAction`() { + fun `onHistoryStateChanged dispatches UpdateHistoryStateAction`() = runTest { val store: BrowserStore = mock() - val observer = EngineObserver("test-id", store) + val observer = createEngineObserver( + tabId = "test-id", + store = store, + scope = this, + ) observer.onHistoryStateChanged(emptyList(), 0) + advanceUntilIdle() + verify(store).dispatch( ContentAction.UpdateHistoryStateAction( "test-id", @@ -1817,6 +2044,7 @@ class EngineObserverTest { ), 1, ) + advanceUntilIdle() verify(store).dispatch( ContentAction.UpdateHistoryStateAction( @@ -1831,12 +2059,18 @@ class EngineObserverTest { } @Test - fun `onScrollChange dispatches UpdateReaderScrollYAction`() { + fun `onScrollChange dispatches UpdateReaderScrollYAction`() = runTest { val store: BrowserStore = mock() whenever(store.state).thenReturn(mock()) - val observer = EngineObserver("tab-id", store) + val observer = createEngineObserver( + tabId = "tab-id", + store = store, + scope = this, + ) observer.onScrollChange(4321, 1234) + advanceUntilIdle() + verify(store).dispatch( ReaderAction.UpdateReaderScrollYAction( "tab-id", @@ -1885,4 +2119,10 @@ class EngineObserverTest { assertFalse(none == customNone) } + + private fun createEngineObserver( + tabId: String = "mozilla", + store: BrowserStore, + scope: CoroutineScope, + ): EngineObserver = EngineObserver(tabId, store, scope) } diff --git a/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt b/mobile/android/android-components/components/browser/state/src/test/java/mozilla/components/browser/state/engine/middleware/LinkingMiddlewareTest.kt @@ -5,7 +5,9 @@ package mozilla.components.browser.state.engine.middleware import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.action.TabListAction @@ -25,6 +27,7 @@ import org.mockito.Mockito.anyString import org.mockito.Mockito.never import org.mockito.Mockito.verify +@OptIn(ExperimentalCoroutinesApi::class) class LinkingMiddlewareTest { private val testDispatcher = StandardTestDispatcher() private val scope = CoroutineScope(testDispatcher) @@ -168,7 +171,7 @@ class LinkingMiddlewareTest { val tab1 = createTab("https://www.mozilla.org", id = "1") val tab2 = createTab("https://www.mozilla.org", id = "2") - val middleware = LinkingMiddleware(scope) + val middleware = LinkingMiddleware(this) val store = BrowserStore( initialState = BrowserState(tabs = listOf(tab1, tab2)), @@ -186,6 +189,7 @@ class LinkingMiddlewareTest { verify(engineSession2).register(engineObserver!!) engineObserver.onTitleChange("test") + advanceUntilIdle() assertEquals("test", store.state.tabs[1].content.title) } @@ -219,7 +223,7 @@ class LinkingMiddlewareTest { val tab1 = createTab("https://www.mozilla.org", id = "1") val tab2 = createTab("https://www.mozilla.org", id = "2", engineSession = engineSession) - val middleware = LinkingMiddleware(scope) + val middleware = LinkingMiddleware(this) val store = BrowserStore( initialState = BrowserState(), @@ -234,6 +238,7 @@ class LinkingMiddlewareTest { assertNotNull(engineObserver) verify(engineSession).register(engineObserver!!) engineObserver.onTitleChange("test") + advanceUntilIdle() assertEquals("test", store.state.tabs[1].content.title) }