diff options
Diffstat (limited to 'mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/toolbar')
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) + } +} |