summaryrefslogtreecommitdiffstats
path: root/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar
diff options
context:
space:
mode:
Diffstat (limited to 'mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar')
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt534
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarViewTest.kt287
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt485
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarInteractorTest.kt104
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarMenuControllerTest.kt878
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultToolbarIntegrationTest.kt72
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/EngineViewClippingBehaviorTest.kt292
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/MenuPresenterTest.kt83
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/NavbarIntegrationTest.kt45
-rw-r--r--mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/TabCounterMenuTest.kt70
10 files changed, 2850 insertions, 0 deletions
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt
new file mode 100644
index 0000000000..c5bfe6124e
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarCFRPresenterTest.kt
@@ -0,0 +1,534 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import android.content.Context
+import android.view.View
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.CookieBannerAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import mozilla.telemetry.glean.testing.GleanTestRule
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.GleanMetrics.TrackingProtection
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.shopping.ShoppingExperienceFeature
+import org.mozilla.fenix.utils.Settings
+
+@RunWith(FenixRobolectricTestRunner::class)
+class BrowserToolbarCFRPresenterTest {
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @get:Rule
+ val gleanTestRule = GleanTestRule(testContext)
+
+ @Test
+ fun `GIVEN the TCP CFR should be shown for a custom tab WHEN the custom tab is fully loaded THEN the TCP CFR is shown`() {
+ val customTab = createCustomTab(url = "")
+ val browserStore = createBrowserStore(customTab = customTab)
+ val presenter = createPresenterThatShowsCFRs(
+ browserStore = browserStore,
+ sessionId = customTab.id,
+ )
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(customTab.id, 0)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(customTab.id, 33)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(customTab.id, 100)).joinBlocking()
+ verify { presenter.showTcpCfr() }
+ }
+
+ @Test
+ fun `GIVEN the cookie banners handling CFR should be shown for a custom tab WHEN the custom tab is fully loaded THEN the TCP CFR is shown`() {
+ val privateTab = createTab(url = "", private = true)
+ val browserStore = createBrowserStore(tab = privateTab, selectedTabId = privateTab.id)
+ val settings: Settings = mockk(relaxed = true) {
+ every { shouldShowTotalCookieProtectionCFR } returns false
+ every { shouldShowReviewQualityCheckCFR } returns false
+ every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis()
+ every { shouldShowEraseActionCFR } returns false
+ every { shouldShowCookieBannersCFR } returns true
+ every { shouldUseCookieBannerPrivateMode } returns true
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns 0L
+ }
+ val presenter = createPresenter(
+ isPrivate = true,
+ browserStore = browserStore,
+ settings = settings,
+ )
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(
+ CookieBannerAction.UpdateStatusAction(
+ privateTab.id,
+ EngineSession.CookieBannerHandlingStatus.HANDLED,
+ ),
+ ).joinBlocking()
+
+ verify { presenter.showCookieBannersCFR() }
+ verify { settings.shouldShowCookieBannersCFR = false }
+ }
+
+ @Test
+ fun `GIVEN the TCP CFR should be shown WHEN the current normal tab is fully loaded THEN the TCP CFR is shown`() {
+ val normalTab = createTab(url = "", private = false)
+ val browserStore = createBrowserStore(
+ tab = normalTab,
+ selectedTabId = normalTab.id,
+ )
+ val presenter = createPresenterThatShowsCFRs(browserStore = browserStore)
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(normalTab.id, 1)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(normalTab.id, 98)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(normalTab.id, 100)).joinBlocking()
+ verify { presenter.showTcpCfr() }
+ }
+
+ @Test
+ fun `GIVEN the TCP CFR should be shown WHEN the current private tab is fully loaded THEN the TCP CFR is shown`() {
+ val privateTab = createTab(url = "", private = true)
+ val browserStore = createBrowserStore(
+ tab = privateTab,
+ selectedTabId = privateTab.id,
+ )
+ val presenter = createPresenterThatShowsCFRs(browserStore = browserStore)
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(privateTab.id, 14)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(privateTab.id, 99)).joinBlocking()
+ verify(exactly = 0) { presenter.showTcpCfr() }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(privateTab.id, 100)).joinBlocking()
+ verify { presenter.showTcpCfr() }
+ }
+
+ @Test
+ fun `GIVEN the TCP CFR should be shown WHEN the current tab is fully loaded THEN the TCP CFR is only shown once`() {
+ val tab = createTab(url = "")
+ val browserStore = createBrowserStore(
+ tab = tab,
+ selectedTabId = tab.id,
+ )
+ val presenter = createPresenterThatShowsCFRs(browserStore = browserStore)
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 99)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ verify(exactly = 1) { presenter.showTcpCfr() }
+ }
+
+ @Test
+ fun `GIVEN the Erase CFR should be shown WHEN in private mode and the current tab is fully loaded THEN the Erase CFR is only shown once`() {
+ val tab = createTab(url = "", private = true)
+
+ val browserStore = createBrowserStore(
+ tab = tab,
+ selectedTabId = tab.id,
+ )
+
+ val presenter = createPresenterThatShowsCFRs(
+ browserStore = browserStore,
+ settings = mockk {
+ every { shouldShowEraseActionCFR } returns true
+ },
+ isPrivate = true,
+ )
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 99)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ verify { presenter.showEraseCfr() }
+ }
+
+ @Test
+ fun `GIVEN no CFR shown WHEN the feature starts THEN don't observe the store for updates`() {
+ val presenter = createPresenter(
+ settings = mockk {
+ every { shouldShowTotalCookieProtectionCFR } returns false
+ every { shouldShowReviewQualityCheckCFR } returns false
+ every { shouldShowEraseActionCFR } returns false
+ },
+ )
+
+ presenter.start()
+
+ assertNull(presenter.scope)
+ }
+
+ @Test
+ fun `GIVEN the store is observed for updates WHEN the presenter is stopped THEN stop observing the store`() {
+ val scope: CoroutineScope = mockk {
+ every { cancel() } just Runs
+ }
+ val presenter = createPresenter()
+ presenter.scope = scope
+
+ presenter.stop()
+
+ verify { scope.cancel() }
+ }
+
+ @Test
+ fun `WHEN the TCP CFR is to be shown THEN instantiate a new one and remember show it again unless explicitly dismissed`() {
+ val settings: Settings = mockk(relaxed = true)
+ val presenter = createPresenter(
+ anchor = mockk(relaxed = true),
+ settings = settings,
+ )
+
+ presenter.showTcpCfr()
+
+ verify(exactly = 0) { settings.shouldShowTotalCookieProtectionCFR = false }
+ assertNotNull(presenter.popup)
+ }
+
+ @Test
+ fun `WHEN the TCP CFR is shown THEN log telemetry`() {
+ val presenter = createPresenter(
+ anchor = mockk(relaxed = true),
+ )
+
+ assertNull(TrackingProtection.tcpCfrShown.testGetValue())
+
+ presenter.showTcpCfr()
+
+ assertNotNull(TrackingProtection.tcpCfrShown.testGetValue())
+ }
+
+ @Test
+ fun `GIVEN the current tab is showing a product page WHEN the tab is not loading THEN the CFR is shown`() {
+ val tab = createTab(url = "")
+ val browserStore = createBrowserStore(
+ tab = tab,
+ selectedTabId = tab.id,
+ )
+ val presenter = createPresenter(
+ browserStore = browserStore,
+ settings = mockk {
+ every { shouldShowTotalCookieProtectionCFR } returns false
+ every { shouldShowReviewQualityCheckCFR } returns true
+ every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis()
+ every { shouldShowEraseActionCFR } returns false
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns 0L
+ },
+ )
+ every { presenter.showShoppingCFR(any()) } just Runs
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(eq(false)) }
+
+ browserStore.dispatch(ContentAction.UpdateProductUrlStateAction(tab.id, true)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(eq(false)) }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(eq(false)) }
+
+ browserStore.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, false)).joinBlocking()
+ verify { presenter.showShoppingCFR(eq(false)) }
+ }
+
+ @Test
+ fun `GIVEN the current tab is showing a product page WHEN the tab is not loading AND another CFR is shown THEN the shopping CFR is not shown`() {
+ val tab = createTab(url = "")
+ val browserStore = createBrowserStore(
+ tab = tab,
+ selectedTabId = tab.id,
+ )
+ val presenter = createPresenter(
+ browserStore = browserStore,
+ settings = mockk {
+ every { shouldShowTotalCookieProtectionCFR } returns false
+ every { shouldShowReviewQualityCheckCFR } returns true
+ every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis()
+ every { shouldShowEraseActionCFR } returns false
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns 0L
+ },
+ )
+ every { presenter.popup } returns mockk()
+ every { presenter.showShoppingCFR(any()) } just Runs
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateProductUrlStateAction(tab.id, true)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(eq(false)) }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(eq(false)) }
+ }
+
+ @Test
+ fun `GIVEN the user opted in the shopping feature AND the opted in shopping CFR should be shown WHEN the tab finishes loading THEN the CFR is shown`() {
+ val tab = createTab(url = "")
+ val browserStore = createBrowserStore(
+ tab = tab,
+ selectedTabId = tab.id,
+ )
+
+ val presenter = createPresenter(
+ settings = mockk {
+ every { shouldShowTotalCookieProtectionCFR } returns false
+ every { shouldShowReviewQualityCheckCFR } returns true
+ every { shouldShowEraseActionCFR } returns false
+ every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis() - Settings.TWO_DAYS_MS
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns System.currentTimeMillis() - Settings.TWO_DAYS_MS
+ },
+ browserStore = browserStore,
+ )
+ every { presenter.showShoppingCFR(any()) } just Runs
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(eq(true)) }
+
+ browserStore.dispatch(ContentAction.UpdateProductUrlStateAction(tab.id, true)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(eq(true)) }
+
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab.id, 100)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(eq(true)) }
+
+ browserStore.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, false)).joinBlocking()
+ verify { presenter.showShoppingCFR(eq(true)) }
+ }
+
+ @Test
+ fun `GIVEN the user opted in the shopping feature AND the opted in shopping CFR should be shown WHEN opening a loaded product page THEN the CFR is shown`() {
+ val tab1 = createTab(url = "", id = "tab1")
+ val tab2 = createTab(url = "", id = "tab2")
+ val browserStore = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab1, tab2),
+ selectedTabId = tab2.id,
+ ),
+ )
+
+ val presenter = createPresenter(
+ settings = mockk {
+ every { shouldShowTotalCookieProtectionCFR } returns false
+ every { shouldShowReviewQualityCheckCFR } returns true
+ every { shouldShowEraseActionCFR } returns false
+ every { reviewQualityCheckOptInTimeInMillis } returns System.currentTimeMillis() - Settings.TWO_DAYS_MS
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns System.currentTimeMillis() - Settings.TWO_DAYS_MS
+ },
+ browserStore = browserStore,
+ )
+ every { presenter.showShoppingCFR(any()) } just Runs
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateProductUrlStateAction(tab1.id, true)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab1.id, 100)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(any()) }
+
+ browserStore.dispatch(TabListAction.SelectTabAction(tab1.id)).joinBlocking()
+ verify(exactly = 1) { presenter.showShoppingCFR(true) }
+ }
+
+ @Test
+ fun `GIVEN the first CFR was displayed less than 12h ago AND the user did not opt in to the shopping feature WHEN opening a loaded product page THEN no shopping CFR is shown`() {
+ val tab1 = createTab(url = "", id = "tab1")
+ val browserStore = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab1),
+ selectedTabId = tab1.id,
+ ),
+ )
+
+ val presenter = createPresenter(
+ settings = mockk {
+ every { shouldShowTotalCookieProtectionCFR } returns false
+ every { shouldShowReviewQualityCheckCFR } returns true
+ every { shouldShowEraseActionCFR } returns false
+ every { reviewQualityCheckOptInTimeInMillis } returns 0L
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns System.currentTimeMillis() - (11 * 60 * 60 * 1000L)
+ },
+ browserStore = browserStore,
+ )
+ every { presenter.showShoppingCFR(any()) } just Runs
+
+ presenter.start()
+
+ assertNull(presenter.scope)
+ }
+
+ @Test
+ fun `GIVEN the first CFR was displayed 12h ago AND the user did not opt in to the shopping feature WHEN opening a loaded product page THEN the first shopping CFR is shown`() {
+ val tab1 = createTab(url = "", id = "tab1")
+ val tab2 = createTab(url = "", id = "tab2")
+ val browserStore = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(tab1, tab2),
+ selectedTabId = tab2.id,
+ ),
+ )
+
+ val presenter = createPresenter(
+ settings = mockk {
+ every { shouldShowTotalCookieProtectionCFR } returns false
+ every { shouldShowReviewQualityCheckCFR } returns true
+ every { shouldShowEraseActionCFR } returns false
+ every { reviewQualityCheckOptInTimeInMillis } returns 0L
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns System.currentTimeMillis() - Settings.TWELVE_HOURS_MS
+ },
+ browserStore = browserStore,
+ )
+ every { presenter.showShoppingCFR(any()) } just Runs
+
+ presenter.start()
+
+ assertNotNull(presenter.scope)
+
+ browserStore.dispatch(ContentAction.UpdateProductUrlStateAction(tab1.id, true)).joinBlocking()
+ browserStore.dispatch(ContentAction.UpdateProgressAction(tab1.id, 100)).joinBlocking()
+ verify(exactly = 0) { presenter.showShoppingCFR(any()) }
+
+ browserStore.dispatch(TabListAction.SelectTabAction(tab1.id)).joinBlocking()
+ verify(exactly = 1) { presenter.showShoppingCFR(false) }
+ }
+
+ /**
+ * Creates and return a [spyk] of a [BrowserToolbarCFRPresenter] that can handle actually showing CFRs.
+ */
+ private fun createPresenterThatShowsCFRs(
+ context: Context = mockk(),
+ anchor: View = mockk(),
+ browserStore: BrowserStore = mockk(),
+ settings: Settings = mockk {
+ every { shouldShowTotalCookieProtectionCFR } returns true
+ every { openTabsCount } returns 5
+ every { shouldShowReviewQualityCheckCFR } returns false
+ every { shouldShowEraseActionCFR } returns false
+ },
+ toolbar: BrowserToolbar = mockk(),
+ isPrivate: Boolean = false,
+ sessionId: String? = null,
+ ) = spyk(createPresenter(context, anchor, browserStore, settings, toolbar, sessionId, isPrivate)) {
+ every { showTcpCfr() } just Runs
+ every { showShoppingCFR(any()) } just Runs
+ every { showEraseCfr() } just Runs
+ }
+
+ /**
+ * Create and return a [BrowserToolbarCFRPresenter] with all constructor properties mocked by default.
+ * Calls to show a CFR will fail. If this behavior is needed to work use [createPresenterThatShowsCFRs].
+ */
+ private fun createPresenter(
+ context: Context = mockk {
+ every { getString(R.string.tcp_cfr_message) } returns "Test"
+ every { getColor(any()) } returns 0
+ every { getString(R.string.pref_key_should_show_review_quality_cfr) } returns "test"
+ },
+ anchor: View = mockk(relaxed = true),
+ browserStore: BrowserStore = mockk(),
+ settings: Settings = mockk(relaxed = true) {
+ every { shouldShowTotalCookieProtectionCFR } returns true
+ every { shouldShowEraseActionCFR } returns true
+ every { openTabsCount } returns 5
+ every { shouldShowCookieBannersCFR } returns true
+ every { shouldShowReviewQualityCheckCFR } returns true
+ },
+ toolbar: BrowserToolbar = mockk {
+ every { findViewById<View>(R.id.mozac_browser_toolbar_security_indicator) } returns anchor
+ every { findViewById<View>(R.id.mozac_browser_toolbar_page_actions) } returns anchor
+ every { findViewById<View>(R.id.mozac_browser_toolbar_navigation_actions) } returns anchor
+ },
+ sessionId: String? = null,
+ isPrivate: Boolean = false,
+ shoppingExperienceFeature: ShoppingExperienceFeature = mockk {
+ every { isEnabled } returns true
+ },
+ ) = spyk(
+ BrowserToolbarCFRPresenter(
+ context = context,
+ browserStore = browserStore,
+ settings = settings,
+ toolbar = toolbar,
+ sessionId = sessionId,
+ isPrivate = isPrivate,
+ onShoppingCfrActionClicked = {},
+ onShoppingCfrDisplayed = {},
+ shoppingExperienceFeature = shoppingExperienceFeature,
+ ),
+ )
+
+ private fun createBrowserStore(
+ tab: TabSessionState? = null,
+ customTab: CustomTabSessionState? = null,
+ selectedTabId: String? = null,
+ ) = BrowserStore(
+ initialState = BrowserState(
+ tabs = if (tab != null) listOf(tab) else listOf(),
+ customTabs = if (customTab != null) listOf(customTab) else listOf(),
+ selectedTabId = selectedTabId,
+ ),
+ )
+}
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarViewTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarViewTest.kt
new file mode 100644
index 0000000000..a58f00130e
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarViewTest.kt
@@ -0,0 +1,287 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import androidx.coordinatorlayout.widget.CoordinatorLayout
+import io.mockk.confirmVerified
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.lib.publicsuffixlist.PublicSuffixList
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.ui.widgets.behavior.EngineViewScrollingBehavior
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.utils.Settings
+import mozilla.components.ui.widgets.behavior.ViewPosition as MozacToolbarPosition
+
+@RunWith(FenixRobolectricTestRunner::class)
+class BrowserToolbarViewTest {
+ private lateinit var toolbarView: BrowserToolbarView
+ private lateinit var toolbar: BrowserToolbar
+ private lateinit var behavior: EngineViewScrollingBehavior
+ private lateinit var settings: Settings
+
+ @Before
+ fun setup() {
+ toolbar = BrowserToolbar(testContext)
+ toolbar.layoutParams = CoordinatorLayout.LayoutParams(100, 100)
+
+ settings = mockk(relaxed = true)
+ every { testContext.components.useCases } returns mockk(relaxed = true)
+ every { testContext.components.core } returns mockk(relaxed = true)
+ every { testContext.components.publicSuffixList } returns PublicSuffixList(testContext)
+ every { testContext.settings() } returns settings
+ toolbarView = BrowserToolbarView(
+ context = testContext,
+ settings = settings,
+ container = CoordinatorLayout(testContext),
+ interactor = mockk(),
+ customTabSession = mockk(relaxed = true),
+ lifecycleOwner = mockk(),
+ tabStripContent = {},
+ )
+
+ toolbarView.view = toolbar
+ behavior = spyk(EngineViewScrollingBehavior(testContext, null, MozacToolbarPosition.BOTTOM))
+ (toolbarView.layout.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior
+ }
+
+ @Test
+ fun `setToolbarBehavior(false) should setDynamicToolbarBehavior if no a11y, bottom toolbar is dynamic and the tab is not for a PWA or TWA`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.BOTTOM
+ every { settings.isDynamicToolbarEnabled } returns true
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns false
+ every { settings.shouldUseFixedTopToolbar } returns false
+
+ toolbarViewSpy.setToolbarBehavior(false)
+
+ verify { toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) }
+ }
+
+ @Test
+ fun `setToolbarBehavior(false) should expandToolbarAndMakeItFixed if bottom toolbar is not set as dynamic`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.BOTTOM
+ every { settings.isDynamicToolbarEnabled } returns false
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns false
+ every { settings.shouldUseFixedTopToolbar } returns false
+
+ toolbarViewSpy.setToolbarBehavior(false)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `setToolbarBehavior(false) should expandToolbarAndMakeItFixed if bottom toolbar is dynamic but the tab is for a PWA or TWA`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.BOTTOM
+ every { settings.isDynamicToolbarEnabled } returns true
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns true
+ every { settings.shouldUseFixedTopToolbar } returns false
+
+ toolbarViewSpy.setToolbarBehavior(false)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `setToolbarBehavior(false) should expandToolbarAndMakeItFixed if bottom toolbar is dynamic tab is not for a PWA or TWA but a11y is enabled`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.BOTTOM
+ every { settings.isDynamicToolbarEnabled } returns true
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns false
+ every { settings.shouldUseFixedTopToolbar } returns true
+
+ toolbarViewSpy.setToolbarBehavior(false)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed bottom toolbar is dynamic, the tab is not for a PWA or TWA and a11y is disabled`() {
+ // All intrinsic checks are met but the method was called with `shouldDisableScroll` = true
+
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.BOTTOM
+ every { settings.isDynamicToolbarEnabled } returns true
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns false
+ every { settings.shouldUseFixedTopToolbar } returns false
+
+ toolbarViewSpy.setToolbarBehavior(false)
+
+ verify { toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) }
+ }
+
+ @Test
+ fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed if bottom toolbar is not set as dynamic`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.BOTTOM
+ every { settings.isDynamicToolbarEnabled } returns false
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns false
+ every { settings.shouldUseFixedTopToolbar } returns false
+
+ toolbarViewSpy.setToolbarBehavior(false)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed if bottom toolbar is dynamic but the tab is for a PWA or TWA`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.BOTTOM
+ every { settings.isDynamicToolbarEnabled } returns true
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns true
+ every { settings.shouldUseFixedTopToolbar } returns false
+
+ toolbarViewSpy.setToolbarBehavior(false)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed if bottom toolbar is dynamic, the tab is for a PWA or TWA and a11 is enabled`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.BOTTOM
+ every { settings.isDynamicToolbarEnabled } returns true
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns false
+ every { settings.shouldUseFixedTopToolbar } returns true
+
+ toolbarViewSpy.setToolbarBehavior(false)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed for top toolbar if shouldUseFixedTopToolbar`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.TOP
+ every { settings.shouldUseFixedTopToolbar } returns true
+
+ toolbarViewSpy.setToolbarBehavior(true)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed for top toolbar if it is not dynamic`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.TOP
+ every { settings.isDynamicToolbarEnabled } returns false
+
+ toolbarViewSpy.setToolbarBehavior(true)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed for top toolbar if shouldDisableScroll`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.TOP
+
+ toolbarViewSpy.setToolbarBehavior(true)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `setToolbarBehavior(false) should setDynamicToolbarBehavior for top toolbar`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { settings.toolbarPosition } returns ToolbarPosition.TOP
+ every { settings.shouldUseFixedTopToolbar } returns true
+ every { settings.isDynamicToolbarEnabled } returns true
+
+ toolbarViewSpy.setToolbarBehavior(true)
+
+ verify { toolbarViewSpy.expandToolbarAndMakeItFixed() }
+ }
+
+ @Test
+ fun `expandToolbarAndMakeItFixed should expand the toolbar and and disable the dynamic behavior`() {
+ val toolbarViewSpy = spyk(toolbarView)
+
+ assertNotNull((toolbarView.layout.layoutParams as CoordinatorLayout.LayoutParams).behavior)
+
+ toolbarViewSpy.expandToolbarAndMakeItFixed()
+
+ verify { toolbarViewSpy.expand() }
+ assertNull((toolbarView.layout.layoutParams as CoordinatorLayout.LayoutParams).behavior)
+ }
+
+ @Test
+ fun `setDynamicToolbarBehavior should set a ViewHideOnScrollBehavior for the bottom toolbar`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ (toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior = null
+
+ toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM)
+
+ assertNotNull((toolbarView.layout.layoutParams as CoordinatorLayout.LayoutParams).behavior)
+ }
+
+ @Test
+ fun `setDynamicToolbarBehavior should set a ViewHideOnScrollBehavior for the top toolbar`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ (toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior = null
+
+ toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.TOP)
+
+ assertNotNull((toolbarView.layout.layoutParams as CoordinatorLayout.LayoutParams).behavior)
+ }
+
+ @Test
+ fun `expand should not do anything if isPwaTabOrTwaTab`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns true
+
+ toolbarViewSpy.expand()
+
+ verify { toolbarViewSpy.expand() }
+ verify { toolbarViewSpy.isPwaTabOrTwaTab }
+ // verify that no other interactions than the expected ones took place
+ confirmVerified(toolbarViewSpy)
+ }
+
+ @Test
+ fun `expand should call forceExpand if not isPwaTabOrTwaTab`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns false
+
+ toolbarViewSpy.expand()
+
+ verify { behavior.forceExpand(toolbarView.layout) }
+ }
+
+ @Test
+ fun `collapse should not do anything if isPwaTabOrTwaTab`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns true
+
+ toolbarViewSpy.collapse()
+
+ verify { toolbarViewSpy.collapse() }
+ verify { toolbarViewSpy.isPwaTabOrTwaTab }
+ // verify that no other interactions than the expected ones took place
+ confirmVerified(toolbarViewSpy)
+ }
+
+ @Test
+ fun `collapse should call forceExpand if not isPwaTabOrTwaTab`() {
+ val toolbarViewSpy = spyk(toolbarView)
+ every { toolbarViewSpy.isPwaTabOrTwaTab } returns false
+
+ toolbarViewSpy.collapse()
+
+ verify { behavior.forceCollapse(toolbarView.layout) }
+ }
+}
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt
new file mode 100644
index 0000000000..8b1819ac1c
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt
@@ -0,0 +1,485 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import androidx.navigation.NavController
+import androidx.navigation.NavOptions
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.impl.annotations.RelaxedMockK
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.verify
+import mozilla.components.browser.state.action.BrowserAction
+import mozilla.components.browser.state.action.ContentAction
+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.concept.engine.EngineView
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.feature.top.sites.TopSitesUseCases
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.libstate.ext.waitUntilIdle
+import mozilla.components.support.test.middleware.CaptureActionsMiddleware
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.ui.tabcounter.TabCounterMenu
+import mozilla.telemetry.glean.testing.GleanTestRule
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.GleanMetrics.Events
+import org.mozilla.fenix.GleanMetrics.ReaderMode
+import org.mozilla.fenix.GleanMetrics.Translations
+import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.R
+import org.mozilla.fenix.browser.BrowserAnimator
+import org.mozilla.fenix.browser.BrowserFragmentDirections
+import org.mozilla.fenix.browser.browsingmode.BrowsingMode
+import org.mozilla.fenix.browser.browsingmode.SimpleBrowsingModeManager
+import org.mozilla.fenix.browser.readermode.ReaderModeController
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.home.HomeFragment
+import org.mozilla.fenix.home.HomeScreenViewModel
+import org.mozilla.fenix.utils.Settings
+
+@RunWith(FenixRobolectricTestRunner::class)
+class DefaultBrowserToolbarControllerTest {
+
+ @RelaxedMockK
+ private lateinit var activity: HomeActivity
+
+ @MockK(relaxUnitFun = true)
+ private lateinit var navController: NavController
+
+ private var tabCounterClicked = false
+
+ @MockK(relaxUnitFun = true)
+ private lateinit var engineView: EngineView
+
+ @RelaxedMockK
+ private lateinit var searchUseCases: SearchUseCases
+
+ @RelaxedMockK
+ private lateinit var sessionUseCases: SessionUseCases
+
+ @RelaxedMockK
+ private lateinit var tabsUseCases: TabsUseCases
+
+ @RelaxedMockK
+ private lateinit var browserAnimator: BrowserAnimator
+
+ @RelaxedMockK
+ private lateinit var topSitesUseCase: TopSitesUseCases
+
+ @RelaxedMockK
+ private lateinit var readerModeController: ReaderModeController
+
+ @RelaxedMockK
+ private lateinit var homeViewModel: HomeScreenViewModel
+
+ private lateinit var store: BrowserStore
+ private val captureMiddleware = CaptureActionsMiddleware<BrowserState, BrowserAction>()
+
+ @get:Rule
+ val gleanTestRule = GleanTestRule(testContext)
+
+ @Before
+ fun setUp() {
+ MockKAnnotations.init(this)
+
+ every { activity.components.useCases.sessionUseCases } returns sessionUseCases
+ every { activity.components.useCases.searchUseCases } returns searchUseCases
+ every { activity.components.useCases.topSitesUseCase } returns topSitesUseCase
+ every { navController.currentDestination } returns mockk {
+ every { id } returns R.id.browserFragment
+ }
+
+ every {
+ browserAnimator.captureEngineViewAndDrawStatically(any(), any())
+ } answers {
+ secondArg<(Boolean) -> Unit>()(true)
+ }
+
+ tabCounterClicked = false
+
+ store = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(
+ createTab("https://www.mozilla.org", id = "1"),
+ ),
+ selectedTabId = "1",
+ ),
+ middleware = listOf(captureMiddleware),
+ )
+ }
+
+ @After
+ fun tearDown() {
+ captureMiddleware.reset()
+ }
+
+ @Test
+ fun handleBrowserToolbarPaste() {
+ val pastedText = "Mozilla"
+ val controller = createController()
+ controller.handleToolbarPaste(pastedText)
+
+ val directions = BrowserFragmentDirections.actionGlobalSearchDialog(
+ sessionId = "1",
+ pastedText = pastedText,
+ )
+
+ verify { navController.navigate(directions, any<NavOptions>()) }
+ }
+
+ @Test
+ fun handleBrowserToolbarPaste_useNewSearchExperience() {
+ val pastedText = "Mozilla"
+ val controller = createController()
+ controller.handleToolbarPaste(pastedText)
+
+ val directions = BrowserFragmentDirections.actionGlobalSearchDialog(
+ sessionId = "1",
+ pastedText = pastedText,
+ )
+
+ verify { navController.navigate(directions, any<NavOptions>()) }
+ }
+
+ @Test
+ fun handleBrowserToolbarPasteAndGoSearch() {
+ val pastedText = "Mozilla"
+
+ val controller = createController()
+ controller.handleToolbarPasteAndGo(pastedText)
+
+ verify {
+ searchUseCases.defaultSearch.invoke(pastedText, "1")
+ }
+
+ store.waitUntilIdle()
+
+ captureMiddleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action ->
+ assertEquals("1", action.sessionId)
+ assertEquals(pastedText, action.searchTerms)
+ }
+ }
+
+ @Test
+ fun handleBrowserToolbarPasteAndGoUrl() {
+ val pastedText = "https://mozilla.org"
+
+ val controller = createController()
+ controller.handleToolbarPasteAndGo(pastedText)
+
+ verify {
+ sessionUseCases.loadUrl(pastedText)
+ }
+
+ store.waitUntilIdle()
+
+ captureMiddleware.assertFirstAction(ContentAction.UpdateSearchTermsAction::class) { action ->
+ assertEquals("1", action.sessionId)
+ assertEquals("", action.searchTerms)
+ }
+ }
+
+ @Test
+ fun handleTabCounterClick() {
+ assertFalse(tabCounterClicked)
+
+ val controller = createController()
+ controller.handleTabCounterClick()
+
+ assertTrue(tabCounterClicked)
+ }
+
+ @Test
+ fun `handle reader mode enabled`() {
+ val controller = createController()
+ assertNull(ReaderMode.opened.testGetValue())
+
+ controller.handleReaderModePressed(enabled = true)
+
+ verify { readerModeController.showReaderView() }
+ assertNotNull(ReaderMode.opened.testGetValue())
+ assertNull(ReaderMode.opened.testGetValue()!!.single().extra)
+ }
+
+ @Test
+ fun `handle reader mode disabled`() {
+ val controller = createController()
+ assertNull(ReaderMode.closed.testGetValue())
+
+ controller.handleReaderModePressed(enabled = false)
+
+ verify { readerModeController.hideReaderView() }
+ assertNotNull(ReaderMode.closed.testGetValue())
+ assertNull(ReaderMode.closed.testGetValue()!!.single().extra)
+ }
+
+ @Test
+ fun handleToolbarClick() {
+ val controller = createController()
+ assertNull(Events.searchBarTapped.testGetValue())
+
+ controller.handleToolbarClick()
+
+ val homeDirections = BrowserFragmentDirections.actionGlobalHome()
+ val searchDialogDirections = BrowserFragmentDirections.actionGlobalSearchDialog(
+ sessionId = "1",
+ )
+
+ assertNotNull(Events.searchBarTapped.testGetValue())
+ val snapshot = Events.searchBarTapped.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("BROWSER", snapshot.single().extra?.getValue("source"))
+
+ verify {
+ // shows the home screen "behind" the search dialog
+ navController.navigate(homeDirections)
+ navController.navigate(searchDialogDirections, any<NavOptions>())
+ }
+ }
+
+ @Test
+ fun handleToolbackClickWithSearchTerms() {
+ val searchResultsTab = createTab("https://google.com?q=mozilla+website", searchTerms = "mozilla website")
+ store.dispatch(TabListAction.AddTabAction(searchResultsTab, select = true)).joinBlocking()
+
+ assertNull(Events.searchBarTapped.testGetValue())
+
+ val controller = createController()
+ controller.handleToolbarClick()
+
+ val homeDirections = BrowserFragmentDirections.actionGlobalHome()
+ val searchDialogDirections = BrowserFragmentDirections.actionGlobalSearchDialog(
+ sessionId = searchResultsTab.id,
+ )
+
+ assertNotNull(Events.searchBarTapped.testGetValue())
+ val snapshot = Events.searchBarTapped.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("BROWSER", snapshot.single().extra?.getValue("source"))
+
+ // Does not show the home screen "behind" the search dialog if the current session has search terms.
+ verify(exactly = 0) {
+ navController.navigate(homeDirections)
+ }
+ verify {
+ navController.navigate(searchDialogDirections, any<NavOptions>())
+ }
+ }
+
+ @Test
+ fun handleToolbarCloseTabPressWithLastPrivateSession() {
+ val item = TabCounterMenu.Item.CloseTab
+
+ val controller = createController()
+ controller.handleTabCounterItemInteraction(item)
+ verify {
+ homeViewModel.sessionToDelete = "1"
+ navController.navigate(BrowserFragmentDirections.actionGlobalHome())
+ }
+ }
+
+ @Test
+ fun handleToolbarCloseTabPress() {
+ val item = TabCounterMenu.Item.CloseTab
+
+ val testTab = createTab("https://www.firefox.com")
+ store.dispatch(TabListAction.AddTabAction(testTab)).joinBlocking()
+ store.dispatch(TabListAction.SelectTabAction(testTab.id)).joinBlocking()
+
+ val controller = createController()
+ controller.handleTabCounterItemInteraction(item)
+ verify { tabsUseCases.removeTab(testTab.id, selectParentIfExists = true) }
+ }
+
+ @Test
+ fun handleToolbarNewTabPress() {
+ val browsingModeManager = SimpleBrowsingModeManager(BrowsingMode.Private)
+ val item = TabCounterMenu.Item.NewTab
+
+ every { activity.browsingModeManager } returns browsingModeManager
+ every { navController.navigate(BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) } just Runs
+
+ val controller = createController()
+ controller.handleTabCounterItemInteraction(item)
+ assertEquals(BrowsingMode.Normal, browsingModeManager.mode)
+ verify { navController.navigate(BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) }
+ }
+
+ @Test
+ fun handleToolbarNewPrivateTabPress() {
+ val browsingModeManager = SimpleBrowsingModeManager(BrowsingMode.Normal)
+ val item = TabCounterMenu.Item.NewPrivateTab
+
+ every { activity.browsingModeManager } returns browsingModeManager
+ every { navController.navigate(BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) } just Runs
+
+ val controller = createController()
+ controller.handleTabCounterItemInteraction(item)
+ assertEquals(BrowsingMode.Private, browsingModeManager.mode)
+ verify { navController.navigate(BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) }
+ }
+
+ @Test
+ fun `handleScroll for dynamic toolbars`() {
+ val controller = createController()
+ every { activity.settings().isDynamicToolbarEnabled } returns true
+
+ controller.handleScroll(10)
+ verify { engineView.setVerticalClipping(10) }
+ }
+
+ @Test
+ fun `handleScroll for static toolbars`() {
+ val controller = createController()
+ every { activity.settings().isDynamicToolbarEnabled } returns false
+
+ controller.handleScroll(10)
+ verify(exactly = 0) { engineView.setVerticalClipping(10) }
+ }
+
+ @Test
+ fun handleHomeButtonClick() {
+ assertNull(Events.browserToolbarHomeTapped.testGetValue())
+
+ val controller = createController()
+ controller.handleHomeButtonClick()
+
+ verify { navController.navigate(BrowserFragmentDirections.actionGlobalHome()) }
+ assertNotNull(Events.browserToolbarHomeTapped.testGetValue())
+ }
+
+ @Test
+ fun handleEraseButtonClicked() {
+ assertNull(Events.browserToolbarEraseTapped.testGetValue())
+ val controller = createController()
+ controller.handleEraseButtonClick()
+
+ verify {
+ homeViewModel.sessionToDelete = HomeFragment.ALL_PRIVATE_TABS
+ navController.navigate(BrowserFragmentDirections.actionGlobalHome())
+ }
+ assertNotNull(Events.browserToolbarEraseTapped.testGetValue())
+ }
+
+ @Test
+ fun handleShoppingCfrActionClick() {
+ val controller = createController()
+
+ controller.handleShoppingCfrActionClick()
+
+ verify {
+ navController.navigate(BrowserFragmentDirections.actionBrowserFragmentToReviewQualityCheckDialogFragment())
+ }
+ }
+
+ @Test
+ fun handleShoppingCfrDisplayedOnce() {
+ val controller = createController()
+ val mockSettings = mockk<Settings> {
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns System.currentTimeMillis()
+ every { reviewQualityCheckCfrDisplayTimeInMillis = any() } just Runs
+ every { reviewQualityCheckCFRClosedCounter } returns 1
+ every { reviewQualityCheckCFRClosedCounter = 2 } just Runs
+ every { shouldShowReviewQualityCheckCFR } returns true
+ }
+ every { activity.settings() } returns mockSettings
+
+ controller.handleShoppingCfrDisplayed()
+
+ verify(exactly = 0) { mockSettings.shouldShowReviewQualityCheckCFR = false }
+ verify { mockSettings.reviewQualityCheckCfrDisplayTimeInMillis = any() }
+ }
+
+ @Test
+ fun handleShoppingCfrDisplayedTwice() {
+ val controller = createController()
+ val mockSettings = mockk<Settings> {
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns System.currentTimeMillis()
+ every { reviewQualityCheckCfrDisplayTimeInMillis = any() } just Runs
+ every { reviewQualityCheckCFRClosedCounter } returns 2
+ every { reviewQualityCheckCFRClosedCounter = 3 } just Runs
+ every { shouldShowReviewQualityCheckCFR } returns true
+ }
+ every { activity.settings() } returns mockSettings
+
+ controller.handleShoppingCfrDisplayed()
+
+ verify(exactly = 0) { mockSettings.shouldShowReviewQualityCheckCFR = false }
+ verify { mockSettings.reviewQualityCheckCfrDisplayTimeInMillis = any() }
+ }
+
+ @Test
+ fun handleShoppingCfrDisplayedThreeTimes() {
+ val controller = createController()
+ val mockSettings = mockk<Settings> {
+ every { reviewQualityCheckCfrDisplayTimeInMillis } returns System.currentTimeMillis()
+ every { reviewQualityCheckCFRClosedCounter } returns 3
+ every { reviewQualityCheckCFRClosedCounter = 4 } just Runs
+ every { shouldShowReviewQualityCheckCFR } returns true
+ every { shouldShowReviewQualityCheckCFR = any() } just Runs
+ }
+ every { activity.settings() } returns mockSettings
+
+ controller.handleShoppingCfrDisplayed()
+
+ verify { mockSettings.shouldShowReviewQualityCheckCFR = false }
+ verify(exactly = 0) { mockSettings.reviewQualityCheckCfrDisplayTimeInMillis = any() }
+ }
+
+ @Test
+ fun handleTranslationsButtonClick() {
+ val controller = createController()
+ controller.handleTranslationsButtonClick()
+
+ verify {
+ navController.navigate(
+ BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment(
+ sessionId = "1",
+ ),
+ )
+ }
+
+ val telemetry = Translations.action.testGetValue()?.firstOrNull()
+ assertEquals("main_flow_toolbar", telemetry?.extra?.get("item"))
+ }
+
+ private fun createController(
+ activity: HomeActivity = this.activity,
+ customTabSessionId: String? = null,
+ ) = DefaultBrowserToolbarController(
+ store = store,
+ tabsUseCases = tabsUseCases,
+ activity = activity,
+ navController = navController,
+ engineView = engineView,
+ homeViewModel = homeViewModel,
+ customTabSessionId = customTabSessionId,
+ readerModeController = readerModeController,
+ browserAnimator = browserAnimator,
+ onTabCounterClicked = {
+ tabCounterClicked = true
+ },
+ onCloseTab = {},
+ )
+}
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarInteractorTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarInteractorTest.kt
new file mode 100644
index 0000000000..b6d9f80923
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarInteractorTest.kt
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import io.mockk.MockKAnnotations
+import io.mockk.impl.annotations.RelaxedMockK
+import io.mockk.mockk
+import io.mockk.verify
+import mozilla.components.ui.tabcounter.TabCounterMenu
+import org.junit.Before
+import org.junit.Test
+import org.mozilla.fenix.components.toolbar.interactor.DefaultBrowserToolbarInteractor
+
+class DefaultBrowserToolbarInteractorTest {
+
+ @RelaxedMockK lateinit var browserToolbarController: BrowserToolbarController
+
+ @RelaxedMockK lateinit var browserToolbarMenuController: BrowserToolbarMenuController
+ lateinit var interactor: DefaultBrowserToolbarInteractor
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ interactor = DefaultBrowserToolbarInteractor(
+ browserToolbarController,
+ browserToolbarMenuController,
+ )
+ }
+
+ @Test
+ fun onTabCounterClicked() {
+ interactor.onTabCounterClicked()
+ verify { browserToolbarController.handleTabCounterClick() }
+ }
+
+ @Test
+ fun onTabCounterMenuItemTapped() {
+ val item: TabCounterMenu.Item = mockk()
+
+ interactor.onTabCounterMenuItemTapped(item)
+ verify { browserToolbarController.handleTabCounterItemInteraction(item) }
+ }
+
+ @Test
+ fun onBrowserToolbarPaste() {
+ val pastedText = "Mozilla"
+ interactor.onBrowserToolbarPaste(pastedText)
+ verify { browserToolbarController.handleToolbarPaste(pastedText) }
+ }
+
+ @Test
+ fun onBrowserToolbarPasteAndGo() {
+ val pastedText = "Mozilla"
+
+ interactor.onBrowserToolbarPasteAndGo(pastedText)
+ verify { browserToolbarController.handleToolbarPasteAndGo(pastedText) }
+ }
+
+ @Test
+ fun onBrowserToolbarClicked() {
+ interactor.onBrowserToolbarClicked()
+
+ verify { browserToolbarController.handleToolbarClick() }
+ }
+
+ @Test
+ fun onBrowserToolbarMenuItemTapped() {
+ val item: ToolbarMenu.Item = mockk()
+
+ interactor.onBrowserToolbarMenuItemTapped(item)
+
+ verify { browserToolbarMenuController.handleToolbarItemInteraction(item) }
+ }
+
+ @Test
+ fun onHomeButtonClicked() {
+ interactor.onHomeButtonClicked()
+
+ verify { browserToolbarController.handleHomeButtonClick() }
+ }
+
+ @Test
+ fun onEraseButtonClicked() {
+ interactor.onEraseButtonClicked()
+
+ verify { browserToolbarController.handleEraseButtonClick() }
+ }
+
+ @Test
+ fun onShoppingCfrActionClicked() {
+ interactor.onShoppingCfrActionClicked()
+
+ verify { browserToolbarController.handleShoppingCfrActionClick() }
+ }
+
+ @Test
+ fun onTranslationsButtonClicked() {
+ interactor.onTranslationsButtonClicked()
+
+ verify { browserToolbarController.handleTranslationsButtonClick() }
+ }
+}
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarMenuControllerTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarMenuControllerTest.kt
new file mode 100644
index 0000000000..781cb8b3cf
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarMenuControllerTest.kt
@@ -0,0 +1,878 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import android.content.Intent
+import android.view.ViewGroup
+import androidx.navigation.NavController
+import io.mockk.MockKAnnotations
+import io.mockk.Runs
+import io.mockk.coEvery
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.impl.annotations.RelaxedMockK
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.mockkObject
+import io.mockk.mockkStatic
+import io.mockk.slot
+import io.mockk.unmockkObject
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runTest
+import mozilla.appservices.places.BookmarkRoot
+import mozilla.components.browser.state.action.CustomTabListAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ReaderState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.concept.engine.EngineSession
+import mozilla.components.concept.engine.prompt.ShareData
+import mozilla.components.feature.search.SearchUseCases
+import mozilla.components.feature.session.SessionFeature
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.feature.tab.collections.TabCollection
+import mozilla.components.feature.tabs.CustomTabsUseCases
+import mozilla.components.feature.top.sites.DefaultTopSitesStorage
+import mozilla.components.feature.top.sites.PinnedSiteStorage
+import mozilla.components.feature.top.sites.TopSite
+import mozilla.components.feature.top.sites.TopSitesUseCases
+import mozilla.components.service.glean.testing.GleanTestRule
+import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
+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.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.GleanMetrics.Collections
+import org.mozilla.fenix.GleanMetrics.Events
+import org.mozilla.fenix.GleanMetrics.ReaderMode
+import org.mozilla.fenix.GleanMetrics.Translations
+import org.mozilla.fenix.HomeActivity
+import org.mozilla.fenix.NavGraphDirections
+import org.mozilla.fenix.R
+import org.mozilla.fenix.browser.BrowserAnimator
+import org.mozilla.fenix.browser.BrowserFragmentDirections
+import org.mozilla.fenix.browser.readermode.ReaderModeController
+import org.mozilla.fenix.collections.SaveCollectionStep
+import org.mozilla.fenix.components.FenixSnackbar
+import org.mozilla.fenix.components.TabCollectionStorage
+import org.mozilla.fenix.components.accounts.AccountState
+import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.ext.directionsEq
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
+import org.mozilla.fenix.utils.Settings
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(FenixRobolectricTestRunner::class)
+class DefaultBrowserToolbarMenuControllerTest {
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @get:Rule
+ val gleanTestRule = GleanTestRule(testContext)
+
+ @MockK private lateinit var snackbarParent: ViewGroup
+
+ @RelaxedMockK private lateinit var activity: HomeActivity
+
+ @RelaxedMockK private lateinit var navController: NavController
+
+ @RelaxedMockK private lateinit var openInFenixIntent: Intent
+
+ @RelaxedMockK private lateinit var settings: Settings
+
+ @RelaxedMockK private lateinit var searchUseCases: SearchUseCases
+
+ @RelaxedMockK private lateinit var sessionUseCases: SessionUseCases
+
+ @RelaxedMockK private lateinit var customTabUseCases: CustomTabsUseCases
+
+ @RelaxedMockK private lateinit var browserAnimator: BrowserAnimator
+
+ @RelaxedMockK private lateinit var snackbar: FenixSnackbar
+
+ @RelaxedMockK private lateinit var tabCollectionStorage: TabCollectionStorage
+
+ @RelaxedMockK private lateinit var topSitesUseCase: TopSitesUseCases
+
+ @RelaxedMockK private lateinit var readerModeController: ReaderModeController
+
+ @MockK private lateinit var sessionFeatureWrapper: ViewBoundFeatureWrapper<SessionFeature>
+
+ @RelaxedMockK private lateinit var sessionFeature: SessionFeature
+
+ @RelaxedMockK private lateinit var topSitesStorage: DefaultTopSitesStorage
+
+ @RelaxedMockK private lateinit var pinnedSiteStorage: PinnedSiteStorage
+
+ private lateinit var browserStore: BrowserStore
+ private lateinit var selectedTab: TabSessionState
+
+ @Before
+ fun setUp() {
+ MockKAnnotations.init(this)
+
+ mockkStatic(
+ "org.mozilla.fenix.settings.deletebrowsingdata.DeleteAndQuitKt",
+ )
+ every { deleteAndQuit(any(), any(), any()) } just Runs
+
+ mockkObject(FenixSnackbar.Companion)
+ every { FenixSnackbar.make(any(), any(), any(), any()) } returns snackbar
+
+ every { activity.components.useCases.sessionUseCases } returns sessionUseCases
+ every { activity.components.useCases.customTabsUseCases } returns customTabUseCases
+ every { activity.components.useCases.searchUseCases } returns searchUseCases
+ every { activity.components.useCases.topSitesUseCase } returns topSitesUseCase
+ every { sessionFeatureWrapper.get() } returns sessionFeature
+ every { navController.currentDestination } returns mockk {
+ every { id } returns R.id.browserFragment
+ }
+ every { settings.topSitesMaxLimit } returns 16
+
+ val onComplete = slot<(Boolean) -> Unit>()
+ every { browserAnimator.captureEngineViewAndDrawStatically(any(), capture(onComplete)) } answers { onComplete.captured.invoke(true) }
+
+ selectedTab = createTab("https://www.mozilla.org", id = "1")
+ browserStore = BrowserStore(
+ initialState = BrowserState(
+ tabs = listOf(selectedTab),
+ selectedTabId = selectedTab.id,
+ ),
+ )
+ }
+
+ @After
+ fun tearDown() {
+ unmockkStatic("org.mozilla.fenix.settings.deletebrowsingdata.DeleteAndQuitKt")
+ unmockkObject(FenixSnackbar.Companion)
+ }
+
+ @Test
+ fun handleToolbarBookmarkPressWithReaderModeInactive() = runTest {
+ val item = ToolbarMenu.Item.Bookmark
+
+ val expectedTitle = "Mozilla"
+ val expectedUrl = "https://mozilla.org"
+ val regularTab = createTab(
+ url = expectedUrl,
+ readerState = ReaderState(active = false, activeUrl = "https://1234.org"),
+ title = expectedTitle,
+ )
+ val store =
+ BrowserStore(BrowserState(tabs = listOf(regularTab), selectedTabId = regularTab.id))
+
+ var bookmarkTappedInvoked = false
+ val controller = createController(
+ scope = this,
+ store = store,
+ bookmarkTapped = { url, title ->
+ assertEquals(expectedTitle, title)
+ assertEquals(expectedUrl, url)
+ bookmarkTappedInvoked = true
+ },
+ )
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("bookmark", snapshot.single().extra?.getValue("item"))
+
+ assertTrue(bookmarkTappedInvoked)
+ }
+
+ @Test
+ fun `IF reader mode is active WHEN bookmark menu item is pressed THEN menu item is handled`() = runTest {
+ val item = ToolbarMenu.Item.Bookmark
+ val expectedTitle = "Mozilla"
+ val readerUrl = "moz-extension://1234"
+ val readerTab = createTab(
+ url = readerUrl,
+ readerState = ReaderState(active = true, activeUrl = "https://mozilla.org"),
+ title = expectedTitle,
+ )
+ browserStore =
+ BrowserStore(BrowserState(tabs = listOf(readerTab), selectedTabId = readerTab.id))
+
+ var bookmarkTappedInvoked = false
+ val controller = createController(
+ scope = this,
+ store = browserStore,
+ bookmarkTapped = { url, title ->
+ assertEquals(expectedTitle, title)
+ assertEquals(readerTab.readerState.activeUrl, url)
+ bookmarkTappedInvoked = true
+ },
+ )
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("bookmark", snapshot.single().extra?.getValue("item"))
+
+ assertTrue(bookmarkTappedInvoked)
+ }
+
+ @Test
+ fun `WHEN open in Fenix menu item is pressed THEN menu item is handled correctly`() = runTest {
+ val customTab = createCustomTab("https://mozilla.org")
+ browserStore.dispatch(CustomTabListAction.AddCustomTabAction(customTab)).joinBlocking()
+ val controller = createController(
+ scope = this,
+ store = browserStore,
+ customTabSessionId = customTab.id,
+ )
+
+ val item = ToolbarMenu.Item.OpenInFenix
+
+ every { activity.startActivity(any()) } just Runs
+ controller.handleToolbarItemInteraction(item)
+
+ verify { sessionFeature.release() }
+ verify { customTabUseCases.migrate(customTab.id, true) }
+ verify { activity.startActivity(openInFenixIntent) }
+ verify { activity.finishAndRemoveTask() }
+ }
+
+ @Test
+ fun `WHEN reader mode menu item is pressed THEN handle appearance change`() = runTest {
+ val item = ToolbarMenu.Item.CustomizeReaderView
+ assertNull(ReaderMode.appearance.testGetValue())
+
+ val controller = createController(scope = this, store = browserStore)
+
+ controller.handleToolbarItemInteraction(item)
+
+ verify { readerModeController.showControls() }
+ assertNotNull(ReaderMode.appearance.testGetValue())
+ assertNull(ReaderMode.appearance.testGetValue()!!.single().extra)
+ }
+
+ @Test
+ fun `WHEN quit menu item is pressed THEN menu item is handled correctly`() = runTest {
+ val item = ToolbarMenu.Item.Quit
+ val testScope = this
+
+ val controller = createController(scope = this, store = browserStore)
+
+ controller.handleToolbarItemInteraction(item)
+
+ verify { deleteAndQuit(activity, testScope, null) }
+ }
+
+ @Test
+ fun `WHEN backwards nav menu item is pressed THEN the session navigates back with active session`() = runTest {
+ val item = ToolbarMenu.Item.Back(false)
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("back", snapshot.single().extra?.getValue("item"))
+
+ verify { sessionUseCases.goBack(browserStore.state.selectedTabId!!) }
+ }
+
+ @Test
+ fun `WHEN backwards nav menu item is long pressed THEN the session navigates back with no active session`() = runTest {
+ val item = ToolbarMenu.Item.Back(true)
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("back_long_press", snapshot.single().extra?.getValue("item"))
+ val directions = BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(null)
+
+ verify { navController.navigate(directions) }
+ }
+
+ @Test
+ fun `WHEN forward nav menu item is pressed THEN the session navigates forward to active session`() = runTest {
+ val item = ToolbarMenu.Item.Forward(false)
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("forward", snapshot.single().extra?.getValue("item"))
+
+ verify { sessionUseCases.goForward(selectedTab.id) }
+ }
+
+ @Test
+ fun `WHEN forward nav menu item is long pressed THEN the browser navigates forward with no active session`() = runTest {
+ val item = ToolbarMenu.Item.Forward(true)
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("forward_long_press", snapshot.single().extra?.getValue("item"))
+
+ val directions = BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(null)
+
+ verify { navController.navigate(directions) }
+ }
+
+ @Test
+ fun `WHEN reload nav menu item is pressed THEN the session reloads from cache`() = runTest {
+ val item = ToolbarMenu.Item.Reload(false)
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("reload", snapshot.single().extra?.getValue("item"))
+
+ verify { sessionUseCases.reload(selectedTab.id) }
+ }
+
+ @Test
+ fun `WHEN reload nav menu item is long pressed THEN the session reloads with no cache`() = runTest {
+ val item = ToolbarMenu.Item.Reload(true)
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("reload", snapshot.single().extra?.getValue("item"))
+
+ verify {
+ sessionUseCases.reload(
+ selectedTab.id,
+ EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.BYPASS_CACHE),
+ )
+ }
+ }
+
+ @Test
+ fun `WHEN stop nav menu item is pressed THEN the session stops loading`() = runTest {
+ val item = ToolbarMenu.Item.Stop
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("stop", snapshot.single().extra?.getValue("item"))
+
+ verify { sessionUseCases.stopLoading(selectedTab.id) }
+ }
+
+ @Test
+ fun `WHEN settings menu item is pressed THEN menu item is handled`() = runTest {
+ val item = ToolbarMenu.Item.Settings
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("settings", snapshot.single().extra?.getValue("item"))
+ val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
+
+ verify { navController.navigate(directions, null) }
+ }
+
+ @Test
+ fun `WHEN bookmark menu item is pressed THEN navigate to bookmarks page`() = runTest {
+ val item = ToolbarMenu.Item.Bookmarks
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("bookmarks", snapshot.single().extra?.getValue("item"))
+ val directions = BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
+
+ verify { navController.navigate(directions, null) }
+ }
+
+ @Test
+ fun `WHEN history menu item is pressed THEN navigate to history page`() = runTest {
+ val item = ToolbarMenu.Item.History
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("history", snapshot.single().extra?.getValue("item"))
+ val directions = BrowserFragmentDirections.actionGlobalHistoryFragment()
+
+ verify { navController.navigate(directions, null) }
+ }
+
+ @Test
+ fun `WHEN request desktop menu item is toggled On THEN desktop site is requested for the session`() = runTest {
+ val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase =
+ mockk(relaxed = true)
+ val item = ToolbarMenu.Item.RequestDesktop(true)
+
+ every { sessionUseCases.requestDesktopSite } returns requestDesktopSiteUseCase
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("desktop_view_on", snapshot.single().extra?.getValue("item"))
+
+ verify {
+ requestDesktopSiteUseCase.invoke(
+ true,
+ selectedTab.id,
+ )
+ }
+ }
+
+ @Test
+ fun `WHEN request desktop menu item is toggled Off THEN mobile site is requested for the session`() = runTest {
+ val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase =
+ mockk(relaxed = true)
+ val item = ToolbarMenu.Item.RequestDesktop(false)
+
+ every { sessionUseCases.requestDesktopSite } returns requestDesktopSiteUseCase
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("desktop_view_off", snapshot.single().extra?.getValue("item"))
+
+ verify {
+ requestDesktopSiteUseCase.invoke(
+ false,
+ selectedTab.id,
+ )
+ }
+ }
+
+ @Test
+ fun `WHEN add to shortcuts menu item is pressed THEN add site AND show snackbar`() = runTestOnMain {
+ val item = ToolbarMenu.Item.AddToTopSites
+ val addPinnedSiteUseCase: TopSitesUseCases.AddPinnedSiteUseCase = mockk(relaxed = true)
+
+ every { topSitesUseCase.addPinnedSites } returns addPinnedSiteUseCase
+ every {
+ snackbarParent.context.getString(R.string.snackbar_added_to_shortcuts)
+ } returns "Added to shortcuts!"
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("add_to_top_sites", snapshot.single().extra?.getValue("item"))
+
+ verify { addPinnedSiteUseCase.invoke(selectedTab.content.title, selectedTab.content.url) }
+ verify { snackbar.setText("Added to shortcuts!") }
+ }
+
+ @Test
+ fun `GIVEN a shortcut page is open WHEN remove from shortcuts is pressed THEN show snackbar`() = runTestOnMain {
+ val snackbarMessage = "Site removed"
+ val item = ToolbarMenu.Item.RemoveFromTopSites
+ val removePinnedSiteUseCase: TopSitesUseCases.RemoveTopSiteUseCase =
+ mockk(relaxed = true)
+ val topSite: TopSite = mockk()
+ every { topSite.url } returns selectedTab.content.url
+ coEvery { pinnedSiteStorage.getPinnedSites() } returns listOf(topSite)
+ every { topSitesUseCase.removeTopSites } returns removePinnedSiteUseCase
+ every {
+ snackbarParent.context.getString(R.string.snackbar_top_site_removed)
+ } returns snackbarMessage
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("remove_from_top_sites", snapshot.single().extra?.getValue("item"))
+
+ verify { snackbar.setText(snackbarMessage) }
+ verify { removePinnedSiteUseCase.invoke(topSite) }
+ }
+
+ @Test
+ fun `WHEN addon extensions menu item is pressed THEN navigate to addons manager`() = runTest {
+ val item = ToolbarMenu.Item.AddonsManager
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("addons_manager", snapshot.single().extra?.getValue("item"))
+ }
+
+ @Test
+ fun `WHEN Add To Home Screen menu item is pressed THEN add site`() = runTest {
+ val item = ToolbarMenu.Item.AddToHomeScreen
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("add_to_homescreen", snapshot.single().extra?.getValue("item"))
+ }
+
+ @Test
+ fun `IF reader mode is inactive WHEN share menu item is pressed THEN navigate to share screen`() = runTest {
+ val item = ToolbarMenu.Item.Share
+ val title = "Mozilla"
+ val url = "https://mozilla.org"
+ val regularTab = createTab(
+ url = url,
+ readerState = ReaderState(active = false, activeUrl = "https://1234.org"),
+ title = title,
+ )
+ browserStore = BrowserStore(BrowserState(tabs = listOf(regularTab), selectedTabId = regularTab.id))
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("share", snapshot.single().extra?.getValue("item"))
+
+ verify {
+ navController.navigate(
+ directionsEq(
+ NavGraphDirections.actionGlobalShareFragment(
+ sessionId = browserStore.state.selectedTabId,
+ data = arrayOf(ShareData(url = "https://mozilla.org", title = "Mozilla")),
+ showPage = true,
+ ),
+ ),
+ )
+ }
+ }
+
+ @Test
+ fun `IF reader mode is active WHEN share menu item is pressed THEN navigate to share screen`() = runTest {
+ val item = ToolbarMenu.Item.Share
+ val title = "Mozilla"
+ val readerUrl = "moz-extension://1234"
+ val readerTab = createTab(
+ url = readerUrl,
+ readerState = ReaderState(active = true, activeUrl = "https://mozilla.org"),
+ title = title,
+ )
+ browserStore = BrowserStore(BrowserState(tabs = listOf(readerTab), selectedTabId = readerTab.id))
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("share", snapshot.single().extra?.getValue("item"))
+
+ verify {
+ navController.navigate(
+ directionsEq(
+ NavGraphDirections.actionGlobalShareFragment(
+ sessionId = browserStore.state.selectedTabId,
+ data = arrayOf(ShareData(url = "https://mozilla.org", title = "Mozilla")),
+ showPage = true,
+ ),
+ ),
+ )
+ }
+ }
+
+ @Test
+ fun `WHEN Find In Page menu item is pressed THEN launch finder`() = runTest {
+ val item = ToolbarMenu.Item.FindInPage
+
+ var launcherInvoked = false
+ val controller = createController(
+ scope = this,
+ store = browserStore,
+ findInPageLauncher = {
+ launcherInvoked = true
+ },
+ )
+ controller.handleToolbarItemInteraction(item)
+
+ assertTrue(launcherInvoked)
+ }
+
+ @Test
+ fun `IF one or more collection exists WHEN Save To Collection menu item is pressed THEN navigate to save collection page`() = runTest {
+ val item = ToolbarMenu.Item.SaveToCollection
+ val cachedTabCollections: List<TabCollection> = mockk(relaxed = true)
+ every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollections
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("save_to_collection", snapshot.single().extra?.getValue("item"))
+
+ assertNotNull(Collections.saveButton.testGetValue())
+ val recordedEvents = Collections.saveButton.testGetValue()!!
+ assertEquals(1, recordedEvents.size)
+ val eventExtra = recordedEvents.single().extra
+ assertNotNull(eventExtra)
+ assertTrue(eventExtra!!.containsKey("from_screen"))
+ assertEquals(
+ DefaultBrowserToolbarMenuController.TELEMETRY_BROWSER_IDENTIFIER,
+ eventExtra["from_screen"],
+ )
+
+ val directions = BrowserFragmentDirections.actionGlobalCollectionCreationFragment(
+ saveCollectionStep = SaveCollectionStep.SelectCollection,
+ tabIds = arrayOf(selectedTab.id),
+ selectedTabIds = arrayOf(selectedTab.id),
+ )
+ verify { navController.navigate(directionsEq(directions), null) }
+ }
+
+ @Test
+ fun `IF no collection exists WHEN Save To Collection menu item is pressed THEN navigate to create collection page`() = runTest {
+ val item = ToolbarMenu.Item.SaveToCollection
+ val cachedTabCollectionsEmpty: List<TabCollection> = emptyList()
+ every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollectionsEmpty
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("save_to_collection", snapshot.single().extra?.getValue("item"))
+
+ assertNotNull(Collections.saveButton.testGetValue())
+ val recordedEvents = Collections.saveButton.testGetValue()!!
+ assertEquals(1, recordedEvents.size)
+ val eventExtra = recordedEvents.single().extra
+ assertNotNull(eventExtra)
+ assertTrue(eventExtra!!.containsKey("from_screen"))
+ assertEquals(
+ DefaultBrowserToolbarMenuController.TELEMETRY_BROWSER_IDENTIFIER,
+ eventExtra["from_screen"],
+ )
+ val directions = BrowserFragmentDirections.actionGlobalCollectionCreationFragment(
+ saveCollectionStep = SaveCollectionStep.NameCollection,
+ tabIds = arrayOf(selectedTab.id),
+ selectedTabIds = arrayOf(selectedTab.id),
+ )
+ verify { navController.navigate(directionsEq(directions), null) }
+ }
+
+ @Test
+ fun `WHEN print menu item is pressed THEN request print`() = runTest {
+ val item = ToolbarMenu.Item.PrintContent
+
+ val controller = createController(scope = this, store = browserStore)
+ assertNull(Events.browserMenuAction.testGetValue())
+
+ controller.handleToolbarItemInteraction(item)
+
+ assertNotNull(Events.browserMenuAction.testGetValue())
+ val snapshot = Events.browserMenuAction.testGetValue()!!
+ assertEquals(1, snapshot.size)
+ assertEquals("print_content", snapshot.single().extra?.getValue("item"))
+ }
+
+ @Test
+ fun `WHEN New Tab menu item is pressed THEN navigate to a new tab home`() = runTest {
+ val item = ToolbarMenu.Item.NewTab
+
+ val controller = createController(scope = this, store = browserStore)
+
+ controller.handleToolbarItemInteraction(item)
+
+ verify {
+ navController.navigate(
+ directionsEq(
+ NavGraphDirections.actionGlobalHome(
+ focusOnAddressBar = true,
+ ),
+ ),
+ )
+ }
+ }
+
+ @Test
+ fun `GIVEN account exists and the user is signed in WHEN sign in to sync menu item is pressed THEN navigate to account settings`() = runTest {
+ val item = ToolbarMenu.Item.SyncAccount(AccountState.AUTHENTICATED)
+ val accountSettingsDirections = BrowserFragmentDirections.actionGlobalAccountSettingsFragment()
+ val controller = createController(scope = this, store = browserStore)
+
+ controller.handleToolbarItemInteraction(item)
+
+ verify { navController.navigate(accountSettingsDirections, null) }
+ }
+
+ @Test
+ fun `GIVEN account exists and the user is not signed in WHEN sign in to sync menu item is pressed THEN navigate to account problem fragment`() = runTest {
+ val item = ToolbarMenu.Item.SyncAccount(AccountState.NEEDS_REAUTHENTICATION)
+ val accountProblemDirections = BrowserFragmentDirections.actionGlobalAccountProblemFragment(
+ entrypoint = FenixFxAEntryPoint.BrowserToolbar,
+ )
+ val controller = createController(scope = this, store = browserStore)
+
+ controller.handleToolbarItemInteraction(item)
+
+ verify { navController.navigate(accountProblemDirections, null) }
+ }
+
+ @Test
+ fun `GIVEN account doesn't exist WHEN sign in to sync menu item is pressed THEN navigate to sign in`() = runTest {
+ val item = ToolbarMenu.Item.SyncAccount(AccountState.NO_ACCOUNT)
+ val turnOnSyncDirections = BrowserFragmentDirections.actionGlobalTurnOnSync(
+ entrypoint = FenixFxAEntryPoint.BrowserToolbar,
+ )
+ val controller = createController(scope = this, store = browserStore)
+
+ controller.handleToolbarItemInteraction(item)
+
+ verify { navController.navigate(turnOnSyncDirections, null) }
+ }
+
+ @Test
+ fun `WHEN the Translations menu item is pressed THEN navigate to translations flow AND post telemetry`() =
+ runTest {
+ val item = ToolbarMenu.Item.Translate
+
+ val controller = createController(scope = this, store = browserStore)
+
+ controller.handleToolbarItemInteraction(item)
+
+ verify {
+ navController.navigate(
+ directionsEq(
+ BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment(
+ sessionId = selectedTab.id,
+ ),
+ ),
+ )
+ }
+
+ val telemetry = Translations.action.testGetValue()?.firstOrNull()
+ assertEquals("main_flow_browser", telemetry?.extra?.get("item"))
+ }
+
+ private fun createController(
+ scope: CoroutineScope,
+ store: BrowserStore,
+ activity: HomeActivity = this.activity,
+ customTabSessionId: String? = null,
+ findInPageLauncher: () -> Unit = { },
+ bookmarkTapped: (String, String) -> Unit = { _, _ -> },
+ ) = DefaultBrowserToolbarMenuController(
+ store = store,
+ activity = activity,
+ navController = navController,
+ settings = settings,
+ findInPageLauncher = findInPageLauncher,
+ browserAnimator = browserAnimator,
+ customTabSessionId = customTabSessionId,
+ openInFenixIntent = openInFenixIntent,
+ scope = scope,
+ snackbarParent = snackbarParent,
+ tabCollectionStorage = tabCollectionStorage,
+ bookmarkTapped = bookmarkTapped,
+ readerModeController = readerModeController,
+ sessionFeature = sessionFeatureWrapper,
+ topSitesStorage = topSitesStorage,
+ pinnedSiteStorage = pinnedSiteStorage,
+ browserStore = browserStore,
+ ).apply {
+ ioScope = scope
+ }
+}
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultToolbarIntegrationTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultToolbarIntegrationTest.kt
new file mode 100644
index 0000000000..ec1624d15c
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultToolbarIntegrationTest.kt
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import android.content.Context
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.unmockkStatic
+import io.mockk.verify
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.ext.components
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+
+@RunWith(FenixRobolectricTestRunner::class)
+class DefaultToolbarIntegrationTest {
+ private lateinit var feature: DefaultToolbarIntegration
+
+ @Before
+ fun setup() {
+ mockkStatic("org.mozilla.fenix.ext.ContextKt")
+ every { any<Context>().components } returns mockk {
+ every { core } returns mockk {
+ every { store } returns BrowserStore()
+ }
+ every { publicSuffixList } returns mockk()
+ every { settings } returns mockk(relaxed = true)
+ }
+
+ feature = DefaultToolbarIntegration(
+ context = testContext,
+ toolbar = mockk(relaxed = true),
+ scrollableToolbar = mockk(relaxed = true),
+ toolbarMenu = mockk(relaxed = true),
+ lifecycleOwner = mockk(),
+ sessionId = null,
+ isPrivate = false,
+ interactor = mockk(),
+ isNavBarEnabled = false,
+ )
+ }
+
+ @After
+ fun teardown() {
+ unmockkStatic("org.mozilla.fenix.ext.ContextKt")
+ }
+
+ @Test
+ fun `WHEN the feature starts THEN start the cfr presenter`() {
+ feature.cfrPresenter = mockk(relaxed = true)
+
+ feature.start()
+
+ verify { feature.cfrPresenter.start() }
+ }
+
+ @Test
+ fun `WHEN the feature stops THEN stop the cfr presenter`() {
+ feature.cfrPresenter = mockk(relaxed = true)
+
+ feature.stop()
+
+ verify { feature.cfrPresenter.stop() }
+ }
+}
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/EngineViewClippingBehaviorTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/EngineViewClippingBehaviorTest.kt
new file mode 100644
index 0000000000..688a3c1d9a
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/EngineViewClippingBehaviorTest.kt
@@ -0,0 +1,292 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import android.view.View
+import android.widget.EditText
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.concept.engine.EngineView
+import mozilla.components.support.test.fakes.engine.FakeEngineView
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.verify
+import org.mozilla.fenix.components.toolbar.navbar.EngineViewClippingBehavior
+import org.mozilla.fenix.components.toolbar.navbar.ToolbarContainerView
+
+@RunWith(AndroidJUnit4::class)
+class EngineViewClippingBehaviorTest {
+
+ // Bottom toolbar position tests
+ @Test
+ fun `GIVEN the toolbar is at the bottom WHEN toolbar is being shifted THEN EngineView adjusts bottom clipping && EngineViewParent position doesn't change`() {
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val engineParentView: View = spy(View(testContext))
+ val toolbar: BrowserToolbar = mock()
+ doReturn(Y_DOWN_TRANSITION).`when`(toolbar).translationY
+
+ assertEquals(0f, engineParentView.translationY)
+
+ EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = engineParentView,
+ topToolbarHeight = 0,
+ ).apply {
+ this.engineView = engineView
+ }.run {
+ onDependentViewChanged(mock(), mock(), toolbar)
+ }
+
+ // We want to position the engine view popup content
+ // right above the bottom toolbar when the toolbar
+ // is being shifted down. The top of the bottom toolbar
+ // is either positive or zero, but for clipping
+ // the values should be negative because the baseline
+ // for clipping is bottom toolbar height.
+ val bottomClipping = -Y_DOWN_TRANSITION.toInt()
+ verify(engineView).setVerticalClipping(bottomClipping)
+
+ assertEquals(0f, engineParentView.translationY)
+ }
+
+ @Test
+ fun `GIVEN the toolbar is at the bottom && the navbar is enabled WHEN toolbar is being shifted THEN EngineView adjusts bottom clipping && EngineViewParent position doesn't change`() {
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val engineParentView: View = spy(View(testContext))
+ val toolbar: ToolbarContainerView = mock()
+ doReturn(Y_DOWN_TRANSITION).`when`(toolbar).translationY
+
+ assertEquals(0f, engineParentView.translationY)
+
+ EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = engineParentView,
+ topToolbarHeight = 0,
+ ).apply {
+ this.engineView = engineView
+ }.run {
+ onDependentViewChanged(mock(), mock(), toolbar)
+ }
+
+ // We want to position the engine view popup content
+ // right above the bottom toolbar when the toolbar
+ // is being shifted down. The top of the bottom toolbar
+ // is either positive or zero, but for clipping
+ // the values should be negative because the baseline
+ // for clipping is bottom toolbar height.
+ val bottomClipping = -Y_DOWN_TRANSITION.toInt()
+ verify(engineView).setVerticalClipping(bottomClipping)
+
+ assertEquals(0f, engineParentView.translationY)
+ }
+
+ // Top toolbar position tests
+ @Test
+ fun `GIVEN the toolbar is at the top WHEN toolbar is being shifted THEN EngineView adjusts bottom clipping && EngineViewParent shifts as well`() {
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val engineParentView: View = spy(View(testContext))
+ val toolbar: BrowserToolbar = mock()
+ doReturn(Y_UP_TRANSITION).`when`(toolbar).translationY
+
+ assertEquals(0f, engineParentView.translationY)
+
+ EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = engineParentView,
+ topToolbarHeight = TOOLBAR_HEIGHT.toInt(),
+ ).apply {
+ this.engineView = engineView
+ }.run {
+ onDependentViewChanged(mock(), mock(), toolbar)
+ }
+
+ verify(engineView).setVerticalClipping(Y_UP_TRANSITION.toInt())
+
+ // Here we are adjusting the vertical position of
+ // the engine view container to be directly under
+ // the toolbar. The top toolbar is shifting up, so
+ // its translation will be either negative or zero.
+ val bottomClipping = Y_UP_TRANSITION + TOOLBAR_HEIGHT
+ assertEquals(bottomClipping, engineParentView.translationY)
+ }
+
+ // Combined toolbar position tests
+ @Test
+ fun `WHEN both of the toolbars are being shifted GIVEN the toolbar is at the top && the navbar is enabled THEN EngineView adjusts bottom clipping`() {
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val engineParentView: View = spy(View(testContext))
+ val toolbar: BrowserToolbar = mock()
+ val toolbarContainerView: ToolbarContainerView = mock()
+ doReturn(Y_UP_TRANSITION).`when`(toolbar).translationY
+ doReturn(Y_DOWN_TRANSITION).`when`(toolbarContainerView).translationY
+
+ EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = engineParentView,
+ topToolbarHeight = TOOLBAR_HEIGHT.toInt(),
+ ).apply {
+ this.engineView = engineView
+ }.run {
+ onDependentViewChanged(mock(), mock(), toolbar)
+ onDependentViewChanged(mock(), mock(), toolbarContainerView)
+ }
+
+ val doubleClipping = Y_UP_TRANSITION - Y_DOWN_TRANSITION
+ verify(engineView).setVerticalClipping(doubleClipping.toInt())
+ }
+
+ @Test
+ fun `WHEN both of the toolbars are being shifted GIVEN the toolbar is at the top && the navbar is enabled THEN EngineViewParent shifts as well`() {
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val engineParentView: View = spy(View(testContext))
+ val toolbar: BrowserToolbar = mock()
+ val toolbarContainerView: ToolbarContainerView = mock()
+ doReturn(Y_UP_TRANSITION).`when`(toolbar).translationY
+ doReturn(Y_DOWN_TRANSITION).`when`(toolbarContainerView).translationY
+
+ EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = engineParentView,
+ topToolbarHeight = TOOLBAR_HEIGHT.toInt(),
+ ).apply {
+ this.engineView = engineView
+ }.run {
+ onDependentViewChanged(mock(), mock(), toolbar)
+ onDependentViewChanged(mock(), mock(), toolbarContainerView)
+ }
+
+ // The top of the parent should be positioned right below the toolbar,
+ // so when we are given the new Y position of the top of the toolbar,
+ // which is always negative as the element is being "scrolled" out of
+ // the screen, the bottom of the toolbar is just a toolbar height away
+ // from it.
+ val parentTranslation = Y_UP_TRANSITION + TOOLBAR_HEIGHT
+ assertEquals(parentTranslation, engineParentView.translationY)
+ }
+
+ // Edge cases
+ @Test
+ fun `GIVEN top toolbar is much bigger than bottom WHEN bottom stopped shifting && top is shifting THEN bottom clipping && engineParentView shifting is still accurate`() {
+ val largeYUpTransition = -500f
+ val largeTopToolbarHeight = 500
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val engineParentView: View = spy(View(testContext))
+ val toolbar: BrowserToolbar = mock()
+ doReturn(largeYUpTransition).`when`(toolbar).translationY
+
+ EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = engineParentView,
+ topToolbarHeight = largeTopToolbarHeight,
+ ).apply {
+ this.engineView = engineView
+ this.recentBottomToolbarTranslation = Y_DOWN_TRANSITION
+ }.run {
+ onDependentViewChanged(mock(), mock(), toolbar)
+ }
+
+ val doubleClipping = largeYUpTransition - Y_DOWN_TRANSITION
+ verify(engineView).setVerticalClipping(doubleClipping.toInt())
+
+ val parentTranslation = largeYUpTransition + largeTopToolbarHeight
+ assertEquals(parentTranslation, engineParentView.translationY)
+ }
+
+ @Test
+ fun `GIVEN bottom toolbar is much bigger than top WHEN top stopped shifting && bottom is shifting THEN bottom clipping && engineParentView shifting is still accurate`() {
+ val largeYBottomTransition = 500f
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val engineParentView: View = spy(View(testContext))
+ val toolbarContainerView: ToolbarContainerView = mock()
+ doReturn(largeYBottomTransition).`when`(toolbarContainerView).translationY
+
+ EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = engineParentView,
+ topToolbarHeight = TOOLBAR_HEIGHT.toInt(),
+ ).apply {
+ this.engineView = engineView
+ this.recentTopToolbarTranslation = Y_UP_TRANSITION
+ }.run {
+ onDependentViewChanged(mock(), mock(), toolbarContainerView)
+ }
+
+ val doubleClipping = Y_UP_TRANSITION - largeYBottomTransition
+ verify(engineView).setVerticalClipping(doubleClipping.toInt())
+
+ val parentTranslation = Y_UP_TRANSITION + TOOLBAR_HEIGHT
+ assertEquals(parentTranslation, engineParentView.translationY)
+ }
+
+ @Test
+ fun `GIVEN a bottom toolbar WHEN translation returns NaN THEN no exception thrown`() {
+ val engineView: EngineView = spy(FakeEngineView(testContext))
+ val engineParentView: View = spy(View(testContext))
+ val toolbar: View = mock()
+ doReturn(100).`when`(toolbar).height
+ doReturn(Float.NaN).`when`(toolbar).translationY
+
+ EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = engineParentView,
+ topToolbarHeight = 0,
+ ).apply {
+ this.engineView = engineView
+ }.run {
+ onDependentViewChanged(mock(), mock(), toolbar)
+ }
+
+ assertEquals(0f, engineView.asView().translationY)
+ }
+
+ // General tests
+ @Test
+ fun `WHEN layoutDependsOn receives a class that isn't a ScrollableToolbar THEN it ignores it`() {
+ val behavior = EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = mock(),
+ topToolbarHeight = 0,
+ )
+
+ assertFalse(behavior.layoutDependsOn(mock(), mock(), TextView(testContext)))
+ assertFalse(behavior.layoutDependsOn(mock(), mock(), EditText(testContext)))
+ assertFalse(behavior.layoutDependsOn(mock(), mock(), ImageView(testContext)))
+ }
+
+ @Test
+ fun `WHEN layoutDependsOn receives a class that is a ScrollableToolbar THEN it recognizes it as a dependency`() {
+ val behavior = EngineViewClippingBehavior(
+ context = mock(),
+ attrs = null,
+ engineViewParent = mock(),
+ topToolbarHeight = 0,
+ )
+
+ assertTrue(behavior.layoutDependsOn(mock(), mock(), BrowserToolbar(testContext)))
+ assertTrue(behavior.layoutDependsOn(mock(), mock(), ToolbarContainerView(testContext)))
+ }
+}
+
+private const val TOOLBAR_HEIGHT = 100f
+private const val Y_UP_TRANSITION = -42f
+private const val Y_DOWN_TRANSITION = -42f
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/MenuPresenterTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/MenuPresenterTest.kt
new file mode 100644
index 0000000000..46e111c2b0
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/MenuPresenterTest.kt
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import io.mockk.clearMocks
+import io.mockk.mockk
+import io.mockk.verify
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.browser.toolbar.BrowserToolbar
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+
+class MenuPresenterTest {
+
+ private lateinit var store: BrowserStore
+ private lateinit var testTab: TabSessionState
+ private lateinit var menuPresenter: MenuPresenter
+ private lateinit var menuToolbar: BrowserToolbar
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule()
+
+ @Before
+ fun setup() {
+ testTab = createTab(url = "https://mozilla.org")
+ store = BrowserStore(initialState = BrowserState(tabs = listOf(testTab), selectedTabId = testTab.id))
+ menuToolbar = mockk(relaxed = true)
+ menuPresenter = MenuPresenter(menuToolbar, store).also {
+ it.start()
+ }
+ clearMocks(menuToolbar)
+ }
+
+ @Test
+ fun `WHEN loading state is updated THEN toolbar is invalidated`() {
+ verify(exactly = 0) { menuToolbar.invalidateActions() }
+
+ store.dispatch(ContentAction.UpdateLoadingStateAction(testTab.id, true)).joinBlocking()
+ verify(exactly = 1) { menuToolbar.invalidateActions() }
+
+ store.dispatch(ContentAction.UpdateLoadingStateAction(testTab.id, false)).joinBlocking()
+ verify(exactly = 2) { menuToolbar.invalidateActions() }
+ }
+
+ @Test
+ fun `WHEN back navigation state is updated THEN toolbar is invalidated`() {
+ verify(exactly = 0) { menuToolbar.invalidateActions() }
+
+ store.dispatch(ContentAction.UpdateBackNavigationStateAction(testTab.id, true)).joinBlocking()
+ verify(exactly = 1) { menuToolbar.invalidateActions() }
+
+ store.dispatch(ContentAction.UpdateBackNavigationStateAction(testTab.id, false)).joinBlocking()
+ verify(exactly = 2) { menuToolbar.invalidateActions() }
+ }
+
+ @Test
+ fun `WHEN forward navigation state is updated THEN toolbar is invalidated`() {
+ verify(exactly = 0) { menuToolbar.invalidateActions() }
+
+ store.dispatch(ContentAction.UpdateForwardNavigationStateAction(testTab.id, true)).joinBlocking()
+ verify(exactly = 1) { menuToolbar.invalidateActions() }
+
+ store.dispatch(ContentAction.UpdateForwardNavigationStateAction(testTab.id, false)).joinBlocking()
+ verify(exactly = 2) { menuToolbar.invalidateActions() }
+ }
+
+ @Test
+ fun `WHEN web app manifest is updated THEN toolbar is invalidated`() {
+ verify(exactly = 0) { menuToolbar.invalidateActions() }
+
+ store.dispatch(ContentAction.UpdateWebAppManifestAction(testTab.id, mockk())).joinBlocking()
+ verify(exactly = 1) { menuToolbar.invalidateActions() }
+ }
+}
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/NavbarIntegrationTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/NavbarIntegrationTest.kt
new file mode 100644
index 0000000000..a402cdbfa0
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/NavbarIntegrationTest.kt
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import io.mockk.mockk
+import io.mockk.verify
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.components.toolbar.navbar.NavbarIntegration
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+
+@RunWith(FenixRobolectricTestRunner::class)
+class NavbarIntegrationTest {
+ private lateinit var feature: NavbarIntegration
+
+ @Before
+ fun setup() {
+ feature = NavbarIntegration(
+ toolbar = mockk(),
+ store = mockk(),
+ appStore = mockk(),
+ bottomToolbarContainerView = mockk(),
+ sessionId = null,
+ ).apply {
+ toolbarController = mockk(relaxed = true)
+ }
+ }
+
+ @Test
+ fun `WHEN the feature starts THEN toolbar controllers starts as well`() {
+ feature.start()
+
+ verify { feature.toolbarController.start() }
+ }
+
+ @Test
+ fun `WHEN the feature stops THEN toolbar controllers stops as well`() {
+ feature.stop()
+
+ verify { feature.toolbarController.stop() }
+ }
+}
diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/TabCounterMenuTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/TabCounterMenuTest.kt
new file mode 100644
index 0000000000..a9316f266e
--- /dev/null
+++ b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar/TabCounterMenuTest.kt
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components.toolbar
+
+import android.content.Context
+import androidx.appcompat.view.ContextThemeWrapper
+import io.mockk.mockk
+import io.mockk.verify
+import mozilla.components.concept.menu.candidate.DividerMenuCandidate
+import mozilla.components.concept.menu.candidate.TextMenuCandidate
+import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.ui.tabcounter.TabCounterMenu
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.browser.browsingmode.BrowsingMode
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+
+@RunWith(FenixRobolectricTestRunner::class)
+class TabCounterMenuTest {
+
+ private lateinit var context: Context
+ private lateinit var onItemTapped: (TabCounterMenu.Item) -> Unit
+ private lateinit var menu: FenixTabCounterMenu
+
+ @Before
+ fun setup() {
+ context = ContextThemeWrapper(testContext, R.style.NormalTheme)
+ onItemTapped = mockk(relaxed = true)
+ menu = FenixTabCounterMenu(context, onItemTapped)
+ }
+
+ @Test
+ fun `return only the new tab item`() {
+ val items = menu.menuItems(showOnly = BrowsingMode.Normal)
+ assertEquals(1, items.size)
+
+ val item = items[0] as TextMenuCandidate
+ assertEquals("New tab", item.text)
+ item.onClick()
+
+ verify { onItemTapped(TabCounterMenu.Item.NewTab) }
+ }
+
+ @Test
+ fun `return only the new private tab item`() {
+ val items = menu.menuItems(showOnly = BrowsingMode.Private)
+ assertEquals(1, items.size)
+
+ val item = items[0] as TextMenuCandidate
+ assertEquals("New private tab", item.text)
+ item.onClick()
+
+ verify { onItemTapped(TabCounterMenu.Item.NewPrivateTab) }
+ }
+
+ @Test
+ fun `return two new tab items and a close button`() {
+ val (newTab, newPrivateTab, divider, closeTab) = menu.menuItems(ToolbarPosition.TOP)
+
+ assertEquals("New tab", (newTab as TextMenuCandidate).text)
+ assertEquals("New private tab", (newPrivateTab as TextMenuCandidate).text)
+ assertEquals("Close tab", (closeTab as TextMenuCandidate).text)
+ assertEquals(DividerMenuCandidate(), divider)
+ }
+}