1
0
Fork 0

For #4399: Create LibState Controller for Search (#4673)

* For #4399: Create LibState Controller for Search

* fix code format

* add unit tests for DefaultSearchController

* add more test

* fix unit tests
master
Sourabh 2019-08-20 16:07:00 +00:00 committed by Sawyer Blatz
parent c3d981e5a3
commit 1afc0eacd8
5 changed files with 383 additions and 103 deletions

View File

@ -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)
}
}

View File

@ -98,10 +98,14 @@ class SearchFragment : Fragment(), BackHandler {
)
}
searchInteractor = SearchInteractor(
val searchController = DefaultSearchController(
activity as HomeActivity,
findNavController(),
searchStore
searchStore,
findNavController()
)
searchInteractor = SearchInteractor(
searchController
)
awesomeBarView = AwesomeBarView(view.search_layout, searchInteractor)

View File

@ -4,19 +4,8 @@
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
import org.mozilla.fenix.search.awesomebar.AwesomeBarInteractor
import org.mozilla.fenix.search.toolbar.ToolbarInteractor
@ -25,104 +14,42 @@ import org.mozilla.fenix.search.toolbar.ToolbarInteractor
* Provides implementations for the AwesomeBarView and ToolbarView
*/
class SearchInteractor(
private val context: Context,
private val navController: NavController,
private val store: SearchStore
private val searchController: SearchController
) : AwesomeBarInteractor, ToolbarInteractor {
data class UserTypingCheck(var ranOnTextChanged: Boolean, var userHasTyped: Boolean)
private val userTypingCheck = UserTypingCheck(false, !store.state.showShortcutEnginePicker)
override fun onUrlCommitted(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)
}
searchController.handleUrlCommitted(url)
}
override fun onEditingCanceled() {
navController.navigateUp()
searchController.handleEditingCancelled()
}
override fun onTextChanged(text: String) {
store.dispatch(SearchAction.UpdateQuery(text))
if (userTypingCheck.ranOnTextChanged && !userTypingCheck.userHasTyped) {
store.dispatch(SearchAction.ShowSearchShortcutEnginePicker(false))
turnOnStartedTyping()
}
userTypingCheck.ranOnTextChanged = true
searchController.handleTextChanged(text)
}
override fun onUrlTapped(url: String) {
(context as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = url,
newTab = store.state.session == null,
from = BrowserDirection.FromSearch
)
context.metrics.track(Event.EnteredUrl(false))
searchController.handleUrlTapped(url)
}
override fun onSearchTermsTapped(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)
searchController.handleSearchTermsTapped(searchTerms)
}
override fun onSearchShortcutEngineSelected(searchEngine: SearchEngine) {
store.dispatch(SearchAction.SearchShortcutEngineSelected(searchEngine))
context.metrics.track(Event.SearchShortcutSelected(searchEngine.name))
searchController.handleSearchShortcutEngineSelected(searchEngine)
}
override fun onClickSearchEngineSettings() {
val directions = SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment()
navController.navigate(directions)
searchController.handleClickSearchEngineSettings()
}
fun turnOnStartedTyping() {
userTypingCheck.ranOnTextChanged = true
userTypingCheck.userHasTyped = true
searchController.handleTurnOnStartedTyping()
}
override fun onExistingSessionSelected(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)
searchController.handleExistingSessionSelected(session)
}
}

View File

@ -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) }
}
}

View File

@ -42,7 +42,12 @@ class SearchInteractorTest {
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
val interactor = SearchInteractor(context, mockk(), store)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk()
)
val interactor = SearchInteractor(searchController)
interactor.onUrlCommitted("test")
@ -63,7 +68,12 @@ class SearchInteractorTest {
every { store.state } returns mockk(relaxed = true)
val interactor = SearchInteractor(mockk(), navController, store)
val searchController: SearchController = DefaultSearchController(
mockk(),
store,
navController
)
val interactor = SearchInteractor(searchController)
interactor.onEditingCanceled()
@ -78,7 +88,12 @@ class SearchInteractorTest {
every { store.state } returns mockk(relaxed = true)
val interactor = SearchInteractor(mockk(), mockk(), store)
val searchController: SearchController = DefaultSearchController(
mockk(),
store,
mockk()
)
val interactor = SearchInteractor(searchController)
interactor.onTextChanged("test")
@ -98,7 +113,12 @@ class SearchInteractorTest {
every { state.session } returns null
every { state.showShortcutEnginePicker } returns true
val interactor = SearchInteractor(context, mockk(), store)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk()
)
val interactor = SearchInteractor(searchController)
interactor.onUrlTapped("test")
@ -128,16 +148,24 @@ class SearchInteractorTest {
every { state.searchEngineSource } returns searchEngine
every { state.showShortcutEnginePicker } returns true
val interactor = SearchInteractor(context, mockk(), store)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk()
)
val interactor = SearchInteractor(searchController)
interactor.onSearchTermsTapped("test")
verify { context.openToBrowserAndLoad(
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine,
forceSearch = true
) }
verify {
context.openToBrowserAndLoad(
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine,
forceSearch = true
)
}
}
@Test
@ -151,7 +179,12 @@ class SearchInteractorTest {
every { store.state } returns state
val interactor = SearchInteractor(context, mockk(), store)
val searchController: SearchController = DefaultSearchController(
context,
store,
mockk()
)
val interactor = SearchInteractor(searchController)
val searchEngine: SearchEngine = mockk(relaxed = true)
interactor.onSearchShortcutEngineSelected(searchEngine)
@ -166,7 +199,12 @@ class SearchInteractorTest {
every { store.state } returns mockk(relaxed = true)
val interactor = SearchInteractor(mockk(), navController, store)
val searchController: SearchController = DefaultSearchController(
mockk(),
store,
navController
)
val interactor = SearchInteractor(searchController)
every { navController.navigate(any() as NavDirections) } just Runs
@ -180,19 +218,31 @@ class SearchInteractorTest {
@Test
fun onExistingSessionSelected() {
val navController: NavController = mockk(relaxed = true)
every { navController.currentDestination } returns NavDestination("").apply { id = R.id.searchFragment }
every { navController.currentDestination } returns NavDestination("").apply {
id = R.id.searchFragment
}
val context: Context = mockk(relaxed = true)
val applicationContext: FenixApplication = mockk(relaxed = true)
every { context.applicationContext } returns applicationContext
val store: SearchStore = mockk()
every { store.state } returns mockk(relaxed = true)
val interactor = SearchInteractor(context, navController, store)
val searchController: SearchController = DefaultSearchController(
context,
store,
navController
)
val interactor = SearchInteractor(searchController)
val session = Session("http://mozilla.org", false)
interactor.onExistingSessionSelected(session)
verify {
navController.navigate(SearchFragmentDirections.actionSearchFragmentToBrowserFragment(null))
navController.navigate(
SearchFragmentDirections.actionSearchFragmentToBrowserFragment(
null
)
)
}
}
}