[fenix] For https://github.com/mozilla-mobile/fenix/issues/4399: Create LibState Controller for Search (https://github.com/mozilla-mobile/fenix/pull/4673)
* For https://github.com/mozilla-mobile/fenix/issues/4399: Create LibState Controller for Search * fix code format * add unit tests for DefaultSearchController * add more test * fix unit testspull/600/head
parent
2634b6dee4
commit
0e376bb266
@ -0,0 +1,141 @@
|
|||||||
|
/*
|
||||||
|
* 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 android.content.Context
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import mozilla.components.browser.search.SearchEngine
|
||||||
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.support.ktx.kotlin.isUrl
|
||||||
|
import org.mozilla.fenix.BrowserDirection
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.metrics
|
||||||
|
import org.mozilla.fenix.ext.nav
|
||||||
|
import org.mozilla.fenix.ext.searchEngineManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface that handles the view manipulation of the Search, triggered by the Interactor
|
||||||
|
*/
|
||||||
|
interface SearchController {
|
||||||
|
fun handleUrlCommitted(url: String)
|
||||||
|
fun handleEditingCancelled()
|
||||||
|
fun handleTextChanged(text: String)
|
||||||
|
fun handleUrlTapped(url: String)
|
||||||
|
fun handleSearchTermsTapped(searchTerms: String)
|
||||||
|
fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine)
|
||||||
|
fun handleClickSearchEngineSettings()
|
||||||
|
fun handleTurnOnStartedTyping()
|
||||||
|
fun handleExistingSessionSelected(session: Session)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DefaultSearchController(
|
||||||
|
private val context: Context,
|
||||||
|
private val store: SearchStore,
|
||||||
|
private val navController: NavController
|
||||||
|
) : SearchController {
|
||||||
|
|
||||||
|
data class UserTypingCheck(var ranOnTextChanged: Boolean, var userHasTyped: Boolean)
|
||||||
|
|
||||||
|
internal val userTypingCheck = UserTypingCheck(false, !store.state.showShortcutEnginePicker)
|
||||||
|
|
||||||
|
override fun handleUrlCommitted(url: String) {
|
||||||
|
if (url.isNotBlank()) {
|
||||||
|
(context as HomeActivity).openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = store.state.session == null,
|
||||||
|
from = BrowserDirection.FromSearch,
|
||||||
|
engine = store.state.searchEngineSource.searchEngine
|
||||||
|
)
|
||||||
|
|
||||||
|
val event = if (url.isUrl()) {
|
||||||
|
Event.EnteredUrl(false)
|
||||||
|
} else {
|
||||||
|
createSearchEvent(store.state.searchEngineSource.searchEngine, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.metrics.track(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleEditingCancelled() {
|
||||||
|
navController.navigateUp()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleTextChanged(text: String) {
|
||||||
|
store.dispatch(SearchAction.UpdateQuery(text))
|
||||||
|
|
||||||
|
if (userTypingCheck.ranOnTextChanged && !userTypingCheck.userHasTyped) {
|
||||||
|
store.dispatch(SearchAction.ShowSearchShortcutEnginePicker(false))
|
||||||
|
handleTurnOnStartedTyping()
|
||||||
|
}
|
||||||
|
|
||||||
|
userTypingCheck.ranOnTextChanged = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleUrlTapped(url: String) {
|
||||||
|
(context as HomeActivity).openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = store.state.session == null,
|
||||||
|
from = BrowserDirection.FromSearch
|
||||||
|
)
|
||||||
|
|
||||||
|
context.metrics.track(Event.EnteredUrl(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleSearchTermsTapped(searchTerms: String) {
|
||||||
|
(context as HomeActivity).openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = searchTerms,
|
||||||
|
newTab = store.state.session == null,
|
||||||
|
from = BrowserDirection.FromSearch,
|
||||||
|
engine = store.state.searchEngineSource.searchEngine,
|
||||||
|
forceSearch = true
|
||||||
|
)
|
||||||
|
|
||||||
|
val event = createSearchEvent(store.state.searchEngineSource.searchEngine, true)
|
||||||
|
context.metrics.track(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine) {
|
||||||
|
store.dispatch(SearchAction.SearchShortcutEngineSelected(searchEngine))
|
||||||
|
context.metrics.track(Event.SearchShortcutSelected(searchEngine.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleClickSearchEngineSettings() {
|
||||||
|
val directions = SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment()
|
||||||
|
navController.navigate(directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleTurnOnStartedTyping() {
|
||||||
|
userTypingCheck.ranOnTextChanged = true
|
||||||
|
userTypingCheck.userHasTyped = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleExistingSessionSelected(session: Session) {
|
||||||
|
val directions = SearchFragmentDirections.actionSearchFragmentToBrowserFragment(null)
|
||||||
|
navController.nav(R.id.searchFragment, directions)
|
||||||
|
context.components.core.sessionManager.select(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createSearchEvent(
|
||||||
|
engine: SearchEngine,
|
||||||
|
isSuggestion: Boolean
|
||||||
|
): Event.PerformedSearch {
|
||||||
|
val isShortcut = engine != context.searchEngineManager.defaultSearchEngine
|
||||||
|
|
||||||
|
val engineSource =
|
||||||
|
if (isShortcut) Event.PerformedSearch.EngineSource.Shortcut(engine)
|
||||||
|
else Event.PerformedSearch.EngineSource.Default(engine)
|
||||||
|
|
||||||
|
val source =
|
||||||
|
if (isSuggestion) Event.PerformedSearch.EventSource.Suggestion(engineSource)
|
||||||
|
else Event.PerformedSearch.EventSource.Action(engineSource)
|
||||||
|
|
||||||
|
return Event.PerformedSearch(source)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,158 @@
|
|||||||
|
/* 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.navigation.NavController
|
||||||
|
import androidx.navigation.NavDirections
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.browser.search.SearchEngine
|
||||||
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mozilla.fenix.BrowserDirection
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
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.metrics
|
||||||
|
import org.mozilla.fenix.ext.nav
|
||||||
|
import org.mozilla.fenix.ext.searchEngineManager
|
||||||
|
|
||||||
|
class DefaultSearchControllerTest {
|
||||||
|
|
||||||
|
private val context: HomeActivity = mockk(relaxed = true)
|
||||||
|
private val store: SearchStore = mockk(relaxed = true)
|
||||||
|
private val navController: NavController = mockk(relaxed = true)
|
||||||
|
private val defaultSearchEngine: SearchEngine? = mockk(relaxed = true)
|
||||||
|
private val session: Session? = mockk(relaxed = true)
|
||||||
|
private val searchEngine: SearchEngine = mockk(relaxed = true)
|
||||||
|
private val metrics: MetricController = mockk(relaxed = true)
|
||||||
|
private val sessionManager: SessionManager = mockk(relaxed = true)
|
||||||
|
|
||||||
|
private lateinit var controller: DefaultSearchController
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
every { store.state.showShortcutEnginePicker } returns false
|
||||||
|
every { context.searchEngineManager.defaultSearchEngine } returns defaultSearchEngine
|
||||||
|
every { store.state.session } returns session
|
||||||
|
every { store.state.searchEngineSource.searchEngine } returns searchEngine
|
||||||
|
every { context.metrics } returns metrics
|
||||||
|
every { context.components.core.sessionManager } returns sessionManager
|
||||||
|
|
||||||
|
controller = DefaultSearchController(
|
||||||
|
context = context,
|
||||||
|
store = store,
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleUrlCommitted() {
|
||||||
|
val url = "https://www.google.com/"
|
||||||
|
|
||||||
|
controller.handleUrlCommitted(url)
|
||||||
|
|
||||||
|
verify { context.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = session == null,
|
||||||
|
from = BrowserDirection.FromSearch,
|
||||||
|
engine = searchEngine
|
||||||
|
) }
|
||||||
|
verify { metrics.track(Event.EnteredUrl(false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleEditingCancelled() {
|
||||||
|
controller.handleEditingCancelled()
|
||||||
|
|
||||||
|
verify { navController.navigateUp() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleTextChanged() {
|
||||||
|
val text = "fenix"
|
||||||
|
|
||||||
|
controller.handleTextChanged(text)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchAction.UpdateQuery(text)) }
|
||||||
|
verify(inverse = true) {
|
||||||
|
store.dispatch(SearchAction.ShowSearchShortcutEnginePicker(false))
|
||||||
|
}
|
||||||
|
assertTrue(controller.userTypingCheck.ranOnTextChanged)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleUrlTapped() {
|
||||||
|
val url = "https://www.google.com/"
|
||||||
|
|
||||||
|
controller.handleUrlTapped(url)
|
||||||
|
|
||||||
|
verify { context.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = url,
|
||||||
|
newTab = session == null,
|
||||||
|
from = BrowserDirection.FromSearch
|
||||||
|
) }
|
||||||
|
verify { metrics.track(Event.EnteredUrl(false)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSearchTermsTapped() {
|
||||||
|
val searchTerms = "fenix"
|
||||||
|
|
||||||
|
controller.handleSearchTermsTapped(searchTerms)
|
||||||
|
|
||||||
|
verify { context.openToBrowserAndLoad(
|
||||||
|
searchTermOrURL = searchTerms,
|
||||||
|
newTab = session == null,
|
||||||
|
from = BrowserDirection.FromSearch,
|
||||||
|
engine = searchEngine,
|
||||||
|
forceSearch = true
|
||||||
|
) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleSearchShortcutEngineSelected() {
|
||||||
|
val searchEngine: SearchEngine = mockk(relaxed = true)
|
||||||
|
|
||||||
|
controller.handleSearchShortcutEngineSelected(searchEngine)
|
||||||
|
|
||||||
|
verify { store.dispatch(SearchAction.SearchShortcutEngineSelected(searchEngine)) }
|
||||||
|
verify { metrics.track(Event.SearchShortcutSelected(searchEngine.name)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleClickSearchEngineSettings() {
|
||||||
|
val directions: NavDirections = SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment()
|
||||||
|
|
||||||
|
controller.handleClickSearchEngineSettings()
|
||||||
|
|
||||||
|
verify { navController.navigate(directions) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleTurnOnStartedTyping() {
|
||||||
|
controller.handleTurnOnStartedTyping()
|
||||||
|
|
||||||
|
assertTrue(controller.userTypingCheck.ranOnTextChanged)
|
||||||
|
assertTrue(controller.userTypingCheck.userHasTyped)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleExistingSessionSelected() {
|
||||||
|
val session: Session = mockk(relaxed = true)
|
||||||
|
val directions = SearchFragmentDirections.actionSearchFragmentToBrowserFragment(null)
|
||||||
|
|
||||||
|
controller.handleExistingSessionSelected(session)
|
||||||
|
|
||||||
|
verify { navController.nav(R.id.searchFragment, directions) }
|
||||||
|
verify { sessionManager.select(session) }
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue