/* 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.search import androidx.appcompat.app.AlertDialog import androidx.navigation.NavController import androidx.navigation.NavDirections import io.mockk.MockKAnnotations import io.mockk.Runs import io.mockk.every import io.mockk.impl.annotations.MockK import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject import io.mockk.spyk import io.mockk.unmockkObject import io.mockk.verify import kotlinx.coroutines.test.runTest import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.support.test.libstate.ext.waitUntilIdle import mozilla.components.support.test.middleware.CaptureActionsMiddleware import mozilla.components.support.test.robolectric.testContext 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.BrowserDirection import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.UnifiedSearch import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.Core import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.ext.application import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.search.SearchDialogFragmentDirections.Companion.actionGlobalAddonsManagementFragment import org.mozilla.fenix.search.SearchDialogFragmentDirections.Companion.actionGlobalSearchEngineFragment import org.mozilla.fenix.search.awesomebar.AwesomeBarView.Companion.GOOGLE_SEARCH_ENGINE_NAME import org.mozilla.fenix.search.toolbar.SearchSelectorMenu import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.utils.Settings @RunWith(FenixRobolectricTestRunner::class) // for gleanTestRule class SearchDialogControllerTest { @MockK(relaxed = true) private lateinit var activity: HomeActivity @MockK(relaxed = true) private lateinit var store: SearchDialogFragmentStore @MockK(relaxed = true) private lateinit var navController: NavController @MockK private lateinit var searchEngine: SearchEngine @MockK(relaxed = true) private lateinit var settings: Settings private lateinit var middleware: CaptureActionsMiddleware private lateinit var browserStore: BrowserStore @get:Rule val gleanTestRule = GleanTestRule(testContext) @Before fun setUp() { MockKAnnotations.init(this) mockkObject(MetricsUtils) middleware = CaptureActionsMiddleware() browserStore = BrowserStore( middleware = listOf(middleware), ) every { store.state.tabId } returns "test-tab-id" every { store.state.searchEngineSource.searchEngine } returns searchEngine every { searchEngine.name } returns "" every { searchEngine.type } returns SearchEngine.Type.BUNDLED every { navController.currentDestination } returns mockk { every { id } returns R.id.searchDialogFragment } every { MetricsUtils.recordSearchMetrics(searchEngine, any(), any()) } just Runs } @After fun teardown() { unmockkObject(MetricsUtils) } @Test fun `GIVEN default search engine is selected WHEN url is committed THEN load the url`() { val url = "https://www.google.com/" assertNull(Events.enteredUrl.testGetValue()) every { store.state.defaultEngine } returns searchEngine createController().handleUrlCommitted(url) verify { activity.openToBrowserAndLoad( searchTermOrURL = url, newTab = false, from = BrowserDirection.FromSearchDialog, engine = searchEngine, forceSearch = false, flags = EngineSession.LoadUrlFlags.none(), additionalHeaders = null, ) } assertNotNull(Events.enteredUrl.testGetValue()) val snapshot = Events.enteredUrl.testGetValue()!! assertEquals(1, snapshot.size) assertEquals("false", snapshot.single().extra?.getValue("autocomplete")) } @Test fun `GIVEN a general search engine is selected WHEN url is committed THEN perform search`() { val url = "https://www.google.com/" assertNull(Events.enteredUrl.testGetValue()) every { store.state.defaultEngine } returns mockk(relaxed = true) createController().handleUrlCommitted(url) verify { activity.openToBrowserAndLoad( searchTermOrURL = url, newTab = false, from = BrowserDirection.FromSearchDialog, engine = searchEngine, forceSearch = true, flags = EngineSession.LoadUrlFlags.none(), additionalHeaders = null, ) } assertNotNull(Events.enteredUrl.testGetValue()) val snapshot = Events.enteredUrl.testGetValue()!! assertEquals(1, snapshot.size) assertEquals("false", snapshot.single().extra?.getValue("autocomplete")) } @Test fun `GIVEN Google search engine is selected and device ram is above threshold WHEN url is committed THEN perform search`() { val searchTerm = "coffee" assertNull(Events.enteredUrl.testGetValue()) every { searchEngine.name } returns GOOGLE_SEARCH_ENGINE_NAME every { store.state.defaultEngine } returns searchEngine every { activity.applicationContext.application.isDeviceRamAboveThreshold } returns true createController().handleUrlCommitted(searchTerm) verify { activity.openToBrowserAndLoad( searchTermOrURL = searchTerm, newTab = false, from = BrowserDirection.FromSearchDialog, engine = searchEngine, forceSearch = false, flags = EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.ALLOW_ADDITIONAL_HEADERS), additionalHeaders = mapOf( "X-Search-Subdivision" to "1", ), ) } } @Test fun `GIVEN Google search engine is selected and device ram is below threshold WHEN url is committed THEN perform search`() { val searchTerm = "coffee" assertNull(Events.enteredUrl.testGetValue()) every { searchEngine.name } returns GOOGLE_SEARCH_ENGINE_NAME every { store.state.defaultEngine } returns searchEngine every { activity.applicationContext.application.isDeviceRamAboveThreshold } returns false createController().handleUrlCommitted(searchTerm) verify { activity.openToBrowserAndLoad( searchTermOrURL = searchTerm, newTab = false, from = BrowserDirection.FromSearchDialog, engine = searchEngine, forceSearch = false, flags = EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.ALLOW_ADDITIONAL_HEADERS), additionalHeaders = mapOf( "X-Search-Subdivision" to "0", ), ) } } @Test fun handleBlankUrlCommitted() { val url = "" var dismissDialogInvoked = false createController( dismissDialog = { dismissDialogInvoked = true }, ).handleUrlCommitted(url) assertTrue(dismissDialogInvoked) } @Test fun handleSearchCommitted() { val searchTerm = "Firefox" createController().handleUrlCommitted(searchTerm) verify { activity.openToBrowserAndLoad( searchTermOrURL = searchTerm, newTab = false, from = BrowserDirection.FromSearchDialog, engine = searchEngine, forceSearch = true, flags = EngineSession.LoadUrlFlags.none(), additionalHeaders = null, ) } } @Test fun `WHEN the search engine is added by the application THEN do not load URL`() { every { searchEngine.type } returns SearchEngine.Type.APPLICATION val searchTerm = "Firefox" var dismissDialogInvoked = false createController( dismissDialog = { dismissDialogInvoked = true }, ).handleUrlCommitted(searchTerm) verify(exactly = 0) { activity.openToBrowserAndLoad( searchTermOrURL = any(), newTab = any(), from = any(), engine = any(), ) } assertFalse(dismissDialogInvoked) } @Test fun handleCrashesUrlCommitted() { val url = "about:crashes" every { activity.packageName } returns "org.mozilla.fenix" createController().handleUrlCommitted(url) verify { activity.startActivity(any()) } } @Test fun handleAddonsUrlCommitted() { val url = "about:addons" val directions = actionGlobalAddonsManagementFragment() createController().handleUrlCommitted(url) verify { navController.navigate(directions) } } @Test fun handleMozillaUrlCommitted() { val url = "moz://a" assertNull(Events.enteredUrl.testGetValue()) every { store.state.defaultEngine } returns searchEngine createController().handleUrlCommitted(url) verify { activity.openToBrowserAndLoad( searchTermOrURL = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO), newTab = false, from = BrowserDirection.FromSearchDialog, engine = searchEngine, flags = EngineSession.LoadUrlFlags.none(), additionalHeaders = null, ) } assertNotNull(Events.enteredUrl.testGetValue()) val snapshot = Events.enteredUrl.testGetValue()!! assertEquals(1, snapshot.size) assertEquals("false", snapshot.single().extra?.getValue("autocomplete")) } @Test fun handleEditingCancelled() = runTest { var clearToolbarFocusInvoked = false var dismissAndGoBack = false createController( clearToolbarFocus = { clearToolbarFocusInvoked = true }, dismissDialogAndGoBack = { dismissAndGoBack = true }, ).handleEditingCancelled() assertTrue(clearToolbarFocusInvoked) assertTrue(dismissAndGoBack) } @Test fun handleTextChangedNonEmpty() { val text = "fenix" createController().handleTextChanged(text) verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) } } @Test fun handleTextChangedEmpty() { val text = "" createController().handleTextChanged(text) verify { store.dispatch(SearchFragmentAction.UpdateQuery(text)) } } @Test fun `show search shortcuts when setting enabled AND query empty`() { val text = "" every { settings.shouldShowSearchShortcuts } returns true createController().handleTextChanged(text) verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) } } @Test fun `show search shortcuts when setting enabled AND query equals url`() { val text = "mozilla.org" every { store.state.url } returns "mozilla.org" every { settings.shouldShowSearchShortcuts } returns true createController().handleTextChanged(text) verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) } } @Test fun `GIVEN show search shortcuts setting is enabled AND unified search is enabled WHEN query is empty THEN do not show search shortcuts`() { val text = "" every { settings.shouldShowSearchShortcuts } returns true every { settings.showUnifiedSearchFeature } returns true createController().handleTextChanged(text) verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } } @Test fun `GIVEN show search shortcuts setting is enabled AND unified search is enabled WHEN query is url THEN do not show search shortcuts`() { val text = "mozilla.org" every { store.state.url } returns "mozilla.org" every { settings.shouldShowSearchShortcuts } returns true every { settings.showUnifiedSearchFeature } returns true createController().handleTextChanged(text) verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } } @Test fun `do not show search shortcuts when setting enabled AND query non-empty`() { val text = "mozilla" createController().handleTextChanged(text) verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } } @Test fun `do not show search shortcuts when setting disabled AND query empty AND url not matching query`() { every { settings.shouldShowSearchShortcuts } returns false val text = "" createController().handleTextChanged(text) verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } } @Test fun `do not show search shortcuts when setting disabled AND query non-empty`() { every { settings.shouldShowSearchShortcuts } returns false val text = "mozilla" createController().handleTextChanged(text) verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } } @Test fun handleUrlTapped() { val url = "https://www.google.com/" val flags = EngineSession.LoadUrlFlags.all() assertNull(Events.enteredUrl.testGetValue()) createController().handleUrlTapped(url, flags) createController().handleUrlTapped(url) verify { activity.openToBrowserAndLoad( searchTermOrURL = url, newTab = false, from = BrowserDirection.FromSearchDialog, flags = flags, ) } assertNotNull(Events.enteredUrl.testGetValue()) val snapshot = Events.enteredUrl.testGetValue()!! assertEquals(2, snapshot.size) assertEquals("false", snapshot.first().extra?.getValue("autocomplete")) assertEquals("false", snapshot[1].extra?.getValue("autocomplete")) } @Test fun handleSearchTermsTapped() { val searchTerms = "fenix" createController().handleSearchTermsTapped(searchTerms) verify { activity.openToBrowserAndLoad( searchTermOrURL = searchTerms, newTab = false, from = BrowserDirection.FromSearchDialog, engine = searchEngine, forceSearch = true, flags = EngineSession.LoadUrlFlags.none(), additionalHeaders = null, ) } } @Test fun handleSearchShortcutEngineSelected() { val searchEngine: SearchEngine = mockk(relaxed = true) val browsingMode = BrowsingMode.Private every { activity.browsingModeManager.mode } returns browsingMode var focusToolbarInvoked = false createController( focusToolbar = { focusToolbarInvoked = true }, ).handleSearchShortcutEngineSelected(searchEngine) assertTrue(focusToolbarInvoked) verify { store.dispatch(SearchFragmentAction.SearchShortcutEngineSelected(searchEngine, browsingMode, settings)) } assertNotNull(SearchShortcuts.selected.testGetValue()) val recordedEvents = SearchShortcuts.selected.testGetValue()!! assertEquals(1, recordedEvents.size) val eventExtra = recordedEvents.single().extra assertNotNull(eventExtra) assertTrue(eventExtra!!.containsKey("engine")) assertEquals(searchEngine.name, eventExtra["engine"]) } @Test fun `WHEN history search engine is selected THEN dispatch correct action`() { val searchEngine: SearchEngine = mockk(relaxed = true) every { searchEngine.type } returns SearchEngine.Type.APPLICATION every { searchEngine.id } returns Core.HISTORY_SEARCH_ENGINE_ID every { settings.showUnifiedSearchFeature } returns true assertNull(UnifiedSearch.engineSelected.testGetValue()) var focusToolbarInvoked = false createController( focusToolbar = { focusToolbarInvoked = true }, ).handleSearchShortcutEngineSelected(searchEngine) assertTrue(focusToolbarInvoked) verify { store.dispatch(SearchFragmentAction.SearchHistoryEngineSelected(searchEngine)) } assertNotNull(UnifiedSearch.engineSelected.testGetValue()) val recordedEvents = UnifiedSearch.engineSelected.testGetValue()!! assertEquals(1, recordedEvents.size) val eventExtra = recordedEvents.single().extra assertNotNull(eventExtra) assertTrue(eventExtra!!.containsKey("engine")) assertEquals("history", eventExtra["engine"]) } @Test fun `WHEN bookmarks search engine is selected THEN dispatch correct action`() { val searchEngine: SearchEngine = mockk(relaxed = true) every { searchEngine.type } returns SearchEngine.Type.APPLICATION every { searchEngine.id } returns Core.BOOKMARKS_SEARCH_ENGINE_ID every { settings.showUnifiedSearchFeature } returns true assertNull(UnifiedSearch.engineSelected.testGetValue()) var focusToolbarInvoked = false createController( focusToolbar = { focusToolbarInvoked = true }, ).handleSearchShortcutEngineSelected(searchEngine) assertTrue(focusToolbarInvoked) verify { store.dispatch(SearchFragmentAction.SearchBookmarksEngineSelected(searchEngine)) } assertNotNull(UnifiedSearch.engineSelected.testGetValue()) val recordedEvents = UnifiedSearch.engineSelected.testGetValue()!! assertEquals(1, recordedEvents.size) val eventExtra = recordedEvents.single().extra assertNotNull(eventExtra) assertTrue(eventExtra!!.containsKey("engine")) assertEquals("bookmarks", eventExtra["engine"]) } @Test fun `WHEN tabs search engine is selected THEN dispatch correct action`() { val searchEngine: SearchEngine = mockk(relaxed = true) every { searchEngine.type } returns SearchEngine.Type.APPLICATION every { searchEngine.id } returns Core.TABS_SEARCH_ENGINE_ID every { settings.showUnifiedSearchFeature } returns true assertNull(UnifiedSearch.engineSelected.testGetValue()) var focusToolbarInvoked = false createController( focusToolbar = { focusToolbarInvoked = true }, ).handleSearchShortcutEngineSelected(searchEngine) assertTrue(focusToolbarInvoked) verify { store.dispatch(SearchFragmentAction.SearchTabsEngineSelected(searchEngine)) } assertNotNull(UnifiedSearch.engineSelected.testGetValue()) val recordedEvents = UnifiedSearch.engineSelected.testGetValue()!! assertEquals(1, recordedEvents.size) val eventExtra = recordedEvents.single().extra assertNotNull(eventExtra) assertTrue(eventExtra!!.containsKey("engine")) assertEquals("tabs", eventExtra["engine"]) } @Test fun handleClickSearchEngineSettings() { val directions: NavDirections = actionGlobalSearchEngineFragment() createController().handleClickSearchEngineSettings() verify { navController.navigate(directions) } } @Test fun handleSearchShortcutsButtonClicked_alreadyOpen() { every { store.state.showSearchShortcuts } returns true createController().handleSearchShortcutsButtonClicked() verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(false)) } } @Test fun handleSearchShortcutsButtonClicked_notYetOpen() { every { store.state.showSearchShortcuts } returns false createController().handleSearchShortcutsButtonClicked() verify { store.dispatch(SearchFragmentAction.ShowSearchShortcutEnginePicker(true)) } } @Test fun handleExistingSessionSelected() { createController().handleExistingSessionSelected("selected") browserStore.waitUntilIdle() middleware.assertFirstAction(TabListAction.SelectTabAction::class) { action -> assertEquals("selected", action.tabId) } verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) } } @Test fun handleExistingSessionSelected_tabId() { createController().handleExistingSessionSelected("tab-id") browserStore.waitUntilIdle() middleware.assertFirstAction(TabListAction.SelectTabAction::class) { action -> assertEquals("tab-id", action.tabId) } verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) } } @Test fun `show camera permissions needed dialog`() { val dialogBuilder: AlertDialog.Builder = mockk(relaxed = true) val spyController = spyk(createController()) every { spyController.buildDialog() } returns dialogBuilder spyController.handleCameraPermissionsNeeded() verify { dialogBuilder.show() } } @Test fun `GIVEN search settings menu item WHEN search selector menu item is tapped THEN show search engine settings`() { val controller = spyk(createController()) controller.handleMenuItemTapped(SearchSelectorMenu.Item.SearchSettings) verify { controller.handleClickSearchEngineSettings() } } private fun createController( clearToolbarFocus: () -> Unit = { }, focusToolbar: () -> Unit = { }, clearToolbar: () -> Unit = { }, dismissDialog: () -> Unit = { }, dismissDialogAndGoBack: () -> Unit = { }, ): SearchDialogController { return SearchDialogController( activity = activity, store = browserStore, tabsUseCases = TabsUseCases(browserStore), fragmentStore = store, navController = navController, settings = settings, dismissDialog = dismissDialog, clearToolbarFocus = clearToolbarFocus, focusToolbar = focusToolbar, clearToolbar = clearToolbar, dismissDialogAndGoBack = dismissDialogAndGoBack, ) } }