/* 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.appservices.places.BookmarkRoot import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.ReaderState 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.tab.collections.TabCollection 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.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.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.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.home.Tab import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit @ExperimentalCoroutinesApi @RunWith(FenixRobolectricTestRunner::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 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 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 val readerModeController = mockk(relaxed = true) private val store: BrowserStore = BrowserStore(initialState = BrowserState( listOf( createTab("https://www.mozilla.org", id = "reader-inactive-tab"), createTab("https://www.mozilla.org", id = "reader-active-tab", readerState = ReaderState(active = true)) )) ) private lateinit var controller: DefaultBrowserToolbarController @Before fun setUp() { Dispatchers.setMain(mainThreadSurrogate) controller = DefaultBrowserToolbarController( activity = activity, navController = navController, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = null, openInFenixIntent = openInFenixIntent, scope = scope, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = readerModeController, sessionManager = mockk(), sharedViewModel = mockk(), onTabCounterClicked = { } ) 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 every { activity.components.core.store } returns store 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, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = null, openInFenixIntent = openInFenixIntent, scope = this, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = mockk(), sessionManager = mockk(), sharedViewModel = mockk(), onTabCounterClicked = { } ) 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 handleToolbarBookmarksPress() { val item = ToolbarMenu.Item.Bookmarks controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BOOKMARKS)) } verify { val directions = BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id) navController.nav(R.id.browserFragment, directions) } } @Test fun handleToolbarHistoryPress() { val item = ToolbarMenu.Item.History controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.HISTORY)) } verify { val directions = BrowserFragmentDirections.actionGlobalHistoryFragment() 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, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = null, openInFenixIntent = openInFenixIntent, scope = this, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = mockk(), sessionManager = mockk(), sharedViewModel = mockk(), onTabCounterClicked = { } ) 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 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.actionGlobalCollectionCreationFragment( 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.actionGlobalCollectionCreationFragment( 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, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = currentSession, openInFenixIntent = openInFenixIntent, scope = scope, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = mockk(), sessionManager = mockk(), sharedViewModel = mockk(), onTabCounterClicked = { } ) 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, findInPageLauncher = findInPageLauncher, engineView = engineView, browserAnimator = browserAnimator, customTabSession = null, openInFenixIntent = openInFenixIntent, scope = testScope, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, topSiteStorage = topSiteStorage, bookmarkTapped = mockk(), readerModeController = mockk(), sessionManager = mockk(), sharedViewModel = mockk(), onTabCounterClicked = { } ) controller.handleToolbarItemInteraction(item) verify { deleteAndQuit(activity, testScope, null) } } @Test fun handleToolbarReaderModePress() { val item = ToolbarMenu.Item.ReaderMode(false) every { currentSession.id } returns "reader-inactive-tab" controller.handleToolbarItemInteraction(item) verify { readerModeController.showReaderView() } every { currentSession.id } returns "reader-active-tab" controller.handleToolbarItemInteraction(item) verify { readerModeController.hideReaderView() } } }