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, activity as HomeActivity,
findNavController(), searchStore,
searchStore findNavController()
)
searchInteractor = SearchInteractor(
searchController
) )
awesomeBarView = AwesomeBarView(view.search_layout, searchInteractor) awesomeBarView = AwesomeBarView(view.search_layout, searchInteractor)

View File

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

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