/* 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.content.Intent import androidx.lifecycle.LifecycleCoroutineScope import androidx.navigation.NavController import androidx.navigation.NavDirections import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import io.mockk.Runs import io.mockk.every import io.mockk.just import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.slot import io.mockk.verify import io.mockk.verifyOrder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.setMain import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.concept.engine.EngineView import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.TabsUseCases import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.TestApplication import org.mozilla.fenix.browser.BrowserAnimator import org.mozilla.fenix.browser.BrowserFragment import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.Analytics import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TopSiteStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.toTab import org.mozilla.fenix.home.Tab import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config @ExperimentalCoroutinesApi @RunWith(RobolectricTestRunner::class) @Config(application = TestApplication::class) class DefaultBrowserToolbarControllerTest { private val mainThreadSurrogate = newSingleThreadContext("UI thread") private var swipeRefreshLayout: SwipeRefreshLayout = mockk(relaxed = true) private var activity: HomeActivity = mockk(relaxed = true) private var analytics: Analytics = mockk(relaxed = true) private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true) private var navController: NavController = mockk(relaxed = true) private var findInPageLauncher: () -> Unit = mockk(relaxed = true) private val engineView: EngineView = mockk(relaxed = true) private val currentSession: Session = mockk(relaxed = true) private val getSupportUrl: () -> String = { "https://supportUrl.org" } private val openInFenixIntent: Intent = mockk(relaxed = true) private val currentSessionAsTab: Tab = mockk(relaxed = true) private val metrics: MetricController = mockk(relaxed = true) private val searchUseCases: SearchUseCases = mockk(relaxed = true) private val sessionUseCases: SessionUseCases = mockk(relaxed = true) private val scope: LifecycleCoroutineScope = mockk(relaxed = true) private val browserAnimator: BrowserAnimator = mockk(relaxed = true) private val snackbar = mockk(relaxed = true) private val tabCollectionStorage = mockk(relaxed = true) private val topSiteStorage = mockk(relaxed = true) private lateinit var controller: DefaultBrowserToolbarController @Before fun setUp() { Dispatchers.setMain(mainThreadSurrogate) controller = DefaultBrowserToolbarController( activity = activity, navController = navController, browsingModeManager = browsingModeManager, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = null, getSupportUrl = getSupportUrl, openInFenixIntent = openInFenixIntent, scope = scope, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = mockk(), sessionManager = mockk(), store = mockk(), sharedViewModel = mockk() ) mockkStatic( "org.mozilla.fenix.ext.SessionKt" ) every { any().toTab(any()) } returns currentSessionAsTab mockkStatic( "org.mozilla.fenix.settings.deletebrowsingdata.DeleteAndQuitKt" ) every { deleteAndQuit(any(), any(), snackbar) } just Runs every { activity.components.analytics } returns analytics every { analytics.metrics } returns metrics every { activity.components.useCases.sessionUseCases } returns sessionUseCases every { activity.components.useCases.searchUseCases } returns searchUseCases every { activity.components.core.sessionManager.selectedSession } returns currentSession val onComplete = slot<() -> Unit>() every { browserAnimator.captureEngineViewAndDrawStatically(capture(onComplete)) } answers { onComplete.captured.invoke() } } @After fun tearDown() { Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher mainThreadSurrogate.close() } @Test fun handleBrowserToolbarPaste() { every { currentSession.id } returns "1" val pastedText = "Mozilla" controller.handleToolbarPaste(pastedText) verify { val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment( sessionId = "1", pastedText = pastedText ) navController.nav(R.id.browserFragment, directions) } } @Test fun handleBrowserToolbarPasteAndGoSearch() { val pastedText = "Mozilla" controller.handleToolbarPasteAndGo(pastedText) verifyOrder { currentSession.searchTerms = "Mozilla" searchUseCases.defaultSearch.invoke(pastedText) } } @Test fun handleBrowserToolbarPasteAndGoUrl() { val pastedText = "https://mozilla.org" controller.handleToolbarPasteAndGo(pastedText) verifyOrder { currentSession.searchTerms = "" sessionUseCases.loadUrl(pastedText) } } @Test fun `handle BrowserMenu dismissed with all options available`() = runBlockingTest { val itemList: List = listOf( ToolbarMenu.Item.AddToHomeScreen, ToolbarMenu.Item.ReaderMode(true), ToolbarMenu.Item.OpenInApp ) val activity = HomeActivity() controller = DefaultBrowserToolbarController( activity = activity, navController = navController, browsingModeManager = browsingModeManager, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = null, getSupportUrl = getSupportUrl, openInFenixIntent = openInFenixIntent, scope = this, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = mockk(), sessionManager = mockk(), store = mockk(), sharedViewModel = mockk() ) controller.handleBrowserMenuDismissed(itemList) assertEquals(true, activity.settings().installPwaOpened) assertEquals(true, activity.settings().readerModeOpened) assertEquals(true, activity.settings().openInAppOpened) } @Test fun handleToolbarClick() { every { currentSession.id } returns "1" controller.handleToolbarClick() verify { metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER)) } verify { val directions = BrowserFragmentDirections.actionBrowserFragmentToSearchFragment( sessionId = "1" ) navController.nav(R.id.browserFragment, directions) } } @Test fun handleToolbarBackPress() { val item = ToolbarMenu.Item.Back controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BACK)) } verify { sessionUseCases.goBack } } @Test fun handleToolbarForwardPress() { val item = ToolbarMenu.Item.Forward controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.FORWARD)) } verify { sessionUseCases.goForward } } @Test fun handleToolbarReloadPress() { val item = ToolbarMenu.Item.Reload every { activity.components.useCases.sessionUseCases } returns sessionUseCases controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.RELOAD)) } verify { sessionUseCases.reload } } @Test fun handleToolbarStopPress() { val item = ToolbarMenu.Item.Stop controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.STOP)) } verify { sessionUseCases.stopLoading } } @Test fun handleToolbarSettingsPress() = runBlocking { val item = ToolbarMenu.Item.Settings controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SETTINGS)) } verify { val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() navController.nav(R.id.browserFragment, directions) } } @Test fun handleToolbarLibraryPress() { val item = ToolbarMenu.Item.Library controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.LIBRARY)) } verify { val directions = BrowserFragmentDirections.actionBrowserFragmentToLibraryFragment() navController.nav(R.id.browserFragment, directions) } } @Test fun handleToolbarRequestDesktopOnPress() { val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase = mockk(relaxed = true) val item = ToolbarMenu.Item.RequestDesktop(true) every { sessionUseCases.requestDesktopSite } returns requestDesktopSiteUseCase controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON)) } verify { requestDesktopSiteUseCase.invoke( true, currentSession ) } } @Test fun handleToolbarRequestDesktopOffPress() { val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase = mockk(relaxed = true) val item = ToolbarMenu.Item.RequestDesktop(false) every { sessionUseCases.requestDesktopSite } returns requestDesktopSiteUseCase controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF)) } verify { requestDesktopSiteUseCase.invoke( false, currentSession ) } } @Test fun handleToolbarAddToTopSitesPressed() = runBlockingTest { val item = ToolbarMenu.Item.AddToTopSites controller = DefaultBrowserToolbarController( activity = activity, navController = navController, browsingModeManager = browsingModeManager, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = null, getSupportUrl = getSupportUrl, openInFenixIntent = openInFenixIntent, scope = this, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = mockk(), sessionManager = mockk(), store = mockk(), sharedViewModel = mockk() ) controller.ioScope = this controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES)) } } @Test fun handleToolbarAddonsManagerPress() = runBlockingTest { val item = ToolbarMenu.Item.AddonsManager controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER)) } } @Test fun handleToolbarAddToHomeScreenPress() { val item = ToolbarMenu.Item.AddToHomeScreen controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN)) } } @Test fun handleToolbarSharePress() { val item = ToolbarMenu.Item.Share every { currentSession.url } returns "https://mozilla.org" controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SHARE)) } verify { navController.navigate(any()) } } @Test fun handleToolbarFindInPagePress() { val item = ToolbarMenu.Item.FindInPage controller.handleToolbarItemInteraction(item) verify { findInPageLauncher() } verify { metrics.track(Event.FindInPageOpened) } } @Test fun handleToolbarReportIssuePress() { val tabsUseCases: TabsUseCases = mockk(relaxed = true) val addTabUseCase: TabsUseCases.AddNewTabUseCase = mockk(relaxed = true) val item = ToolbarMenu.Item.ReportIssue every { currentSession.id } returns "1" every { currentSession.url } returns "https://mozilla.org" every { activity.components.useCases.tabsUseCases } returns tabsUseCases every { tabsUseCases.addTab } returns addTabUseCase controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.REPORT_SITE_ISSUE)) } verify { // Hardcoded URL because this function modifies the URL with an apply addTabUseCase.invoke( String.format( BrowserFragment.REPORT_SITE_ISSUE_URL, "https://mozilla.org" ) ) } } @Test fun handleToolbarSaveToCollectionPressWhenAtLeastOneCollectionExists() { val item = ToolbarMenu.Item.SaveToCollection val cachedTabCollections: List = mockk(relaxed = true) every { activity.components.useCases.sessionUseCases } returns sessionUseCases every { activity.components.core.tabCollectionStorage.cachedTabCollections } returns cachedTabCollections controller.handleToolbarItemInteraction(item) verify { metrics.track( Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION) ) } verify { metrics.track( Event.CollectionSaveButtonPressed(DefaultBrowserToolbarController.TELEMETRY_BROWSER_IDENTIFIER) ) } verify { val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment( previousFragmentId = R.id.browserFragment, saveCollectionStep = SaveCollectionStep.SelectCollection, tabIds = arrayOf(currentSession.id), selectedTabIds = arrayOf(currentSession.id) ) navController.nav(R.id.browserFragment, directions) } } @Test fun handleToolbarSaveToCollectionPressWhenNoCollectionsExists() { val item = ToolbarMenu.Item.SaveToCollection val cachedTabCollectionsEmpty: List = emptyList() every { activity.components.useCases.sessionUseCases } returns sessionUseCases every { activity.components.core.tabCollectionStorage.cachedTabCollections } returns cachedTabCollectionsEmpty controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION)) } verify { metrics.track( Event.CollectionSaveButtonPressed( DefaultBrowserToolbarController.TELEMETRY_BROWSER_IDENTIFIER ) ) } verify { val directions = BrowserFragmentDirections.actionBrowserFragmentToCreateCollectionFragment( previousFragmentId = R.id.browserFragment, saveCollectionStep = SaveCollectionStep.NameCollection, tabIds = arrayOf(currentSession.id), selectedTabIds = arrayOf(currentSession.id) ) navController.nav(R.id.browserFragment, directions) } } @Test fun handleToolbarOpenInFenixPress() { controller = DefaultBrowserToolbarController( activity = activity, navController = navController, browsingModeManager = browsingModeManager, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = currentSession, getSupportUrl = getSupportUrl, openInFenixIntent = openInFenixIntent, scope = scope, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = mockk(), sessionManager = mockk(), store = mockk(), sharedViewModel = mockk() ) val sessionManager: SessionManager = mockk(relaxed = true) val item = ToolbarMenu.Item.OpenInFenix every { activity.components.core.sessionManager } returns sessionManager every { currentSession.customTabConfig } returns mockk() every { activity.startActivity(any()) } just Runs controller.handleToolbarItemInteraction(item) verify { engineView.release() } verify { currentSession.customTabConfig = null } verify { sessionManager.select(currentSession) } verify { activity.startActivity(openInFenixIntent) } verify { activity.finish() } } @Test fun handleToolbarQuitPress() = runBlockingTest { val item = ToolbarMenu.Item.Quit val testScope = this controller = DefaultBrowserToolbarController( activity = activity, navController = navController, browsingModeManager = browsingModeManager, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = null, getSupportUrl = getSupportUrl, openInFenixIntent = openInFenixIntent, scope = testScope, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = mockk(), sessionManager = mockk(), store = mockk(), sharedViewModel = mockk() ) controller.handleToolbarItemInteraction(item) verify { deleteAndQuit(activity, testScope, null) } } }