1
0
Fork 0

For #3633 - Refactors the search screen to use lib-state

* For #3633 - Adds SearchStore

* For #3633 - Refactors AwesomeBarUIView

* For #3633 - Refactors ToolbarUIView to use lib-state

* For #3633 - Fixes a couple of state bugs

* For #3633 - Moves all user interaction to SearchInteractor

* For #3633 - Adds kdocs to SearchStore and SearchInteractor

* For #3633 - Adds documentation for the properties on SearchState
Also removes uneccessary property

* For #3633 - Creates `StateViewModel` to handle state restoration

* For #3633 - Adds a test for onTextChanged

* For #3633 - Adds tests for SearchInteractor

* For #3633 - Fixes bugs and adds documentation
master
Jeff Boek 2019-07-12 16:32:00 -07:00 committed by GitHub
parent 6b639b1a32
commit e4ff70c542
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 808 additions and 543 deletions

View File

@ -0,0 +1,35 @@
/* 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
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import androidx.lifecycle.get
import mozilla.components.lib.state.State
/**
* Generic ViewModel to wrap a State object for state restoration
*/
@Suppress("UNCHECKED_CAST")
class StateViewModel<T : State>(initialState: T) : ViewModel() {
var state: T = initialState
private set(value) { field = value }
fun update(state: T) { this.state = state }
companion object {
fun <S : State> get(fragment: Fragment, initialState: S): StateViewModel<S> {
val factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return StateViewModel(initialState) as T
}
}
return ViewModelProviders.of(fragment, factory).get()
}
}
}

View File

@ -17,11 +17,13 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.fragment.app.FragmentActivity
import mozilla.components.browser.search.SearchEngineManager
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.Log.Priority.WARN
import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricController
/**
* Get the BrowserApplication object from a context.
@ -35,6 +37,18 @@ val Context.application: FenixApplication
val Context.components: Components
get() = application.components
/**
* Helper function to get the MetricController off of context.
*/
val Context.metrics: MetricController
get() = this.components.analytics.metrics
/**
* Helper function to get the SearchEngineManager off of context.
*/
val Context.searchEngineManager: SearchEngineManager
get() = this.components.search.searchEngineManager
fun Context.asActivity() = (this as? ContextThemeWrapper)?.baseContext as? Activity
?: this as? Activity

View File

@ -9,6 +9,7 @@ import android.content.Context
import android.content.DialogInterface
import android.graphics.Typeface.BOLD
import android.graphics.Typeface.ITALIC
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.text.style.StyleSpan
import android.view.LayoutInflater
@ -16,59 +17,45 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.navigation.Navigation
import androidx.navigation.fragment.findNavController
import kotlinx.android.synthetic.main.fragment_search.*
import kotlinx.android.synthetic.main.fragment_search.view.*
import mozilla.components.browser.search.SearchEngine
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.qr.QrFeature
import mozilla.components.lib.state.Store
import mozilla.components.lib.state.ext.observe
import mozilla.components.support.base.feature.BackHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.content.hasCamera
import mozilla.components.support.ktx.android.content.isPermissionGranted
import mozilla.components.support.ktx.kotlin.isUrl
import org.jetbrains.anko.backgroundDrawable
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.components.StateViewModel
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.toolbar.SearchAction
import org.mozilla.fenix.components.toolbar.SearchChange
import org.mozilla.fenix.components.toolbar.SearchState
import org.mozilla.fenix.components.toolbar.ToolbarComponent
import org.mozilla.fenix.components.toolbar.ToolbarUIView
import org.mozilla.fenix.components.toolbar.ToolbarViewModel
import org.mozilla.fenix.ext.getSpannable
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.search.awesomebar.AwesomeBarAction
import org.mozilla.fenix.search.awesomebar.AwesomeBarChange
import org.mozilla.fenix.search.awesomebar.AwesomeBarComponent
import org.mozilla.fenix.search.awesomebar.AwesomeBarState
import org.mozilla.fenix.search.awesomebar.AwesomeBarUIView
import org.mozilla.fenix.search.awesomebar.AwesomeBarViewModel
import org.mozilla.fenix.search.awesomebar.AwesomeBarView
import org.mozilla.fenix.search.toolbar.ToolbarView
import org.mozilla.fenix.utils.Settings
@Suppress("TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
class SearchFragment : Fragment(), BackHandler {
private lateinit var toolbarComponent: ToolbarComponent
private lateinit var awesomeBarComponent: AwesomeBarComponent
private var sessionId: String? = null
private var isPrivate = false
private lateinit var toolbarView: ToolbarView
private lateinit var awesomeBarView: AwesomeBarView
private val qrFeature = ViewBoundFeatureWrapper<QrFeature>()
private var permissionDidUpdate = false
private lateinit var searchStore: SearchStore
private lateinit var searchInteractor: SearchInteractor
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Disabled while awaiting a better solution to #3209
// postponeEnterTransition()
// sharedElementEnterTransition =
// TransitionInflater.from(context).inflateTransition(android.R.transition.move).setDuration(
// SHARED_TRANSITION_MS
// )
requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea)
}
@ -77,43 +64,44 @@ class SearchFragment : Fragment(), BackHandler {
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
sessionId = SearchFragmentArgs.fromBundle(arguments!!).sessionId
isPrivate = (activity as HomeActivity).browsingModeManager.isPrivate
val session = arguments
?.let(SearchFragmentArgs.Companion::fromBundle)
?.let { it.sessionId }
?.let(requireComponents.core.sessionManager::findSessionById)
val session = sessionId?.let { requireComponents.core.sessionManager.findSessionById(it) }
val view = inflater.inflate(R.layout.fragment_search, container, false)
val url = session?.url ?: ""
toolbarComponent = ToolbarComponent(
view.toolbar_component_wrapper,
ActionBusFactory.get(this),
sessionId,
isPrivate,
true,
view.search_engine_icon,
FenixViewModelProvider.create(
this,
ToolbarViewModel::class.java
) {
ToolbarViewModel(SearchState(url, session?.searchTerms ?: "", isEditing = true))
}
).also {
// Remove background from toolbar view since it conflicts with the search UI.
it.uiView.view.background = null
it.uiView.view.layoutParams.height = CoordinatorLayout.LayoutParams.MATCH_PARENT
}
awesomeBarComponent = AwesomeBarComponent(
view.search_layout,
ActionBusFactory.get(this),
FenixViewModelProvider.create(
this,
AwesomeBarViewModel::class.java
) {
AwesomeBarViewModel(AwesomeBarState("", false))
}
val viewModel = StateViewModel.get(
this,
SearchState(
query = url,
showShortcutEnginePicker = false,
searchEngineSource = SearchEngineSource.Default(
requireComponents.search.searchEngineManager.getDefaultSearchEngine(requireContext())
),
showSuggestions = Settings.getInstance(requireContext()).showSearchSuggestions,
showVisitedSitesBookmarks = Settings.getInstance(requireContext()).shouldShowVisitedSitesBookmarks,
session = session
)
)
ActionBusFactory.get(this).logMergedObservables()
searchStore = Store(
viewModel.state,
::searchStateReducer
)
searchStore.observe(this) { viewModel.update(it) }
searchInteractor = SearchInteractor(
activity as HomeActivity,
findNavController(),
searchStore
)
toolbarView = ToolbarView(view.toolbar_component_wrapper, searchInteractor, historyStorageProvider())
awesomeBarView = AwesomeBarView(view.search_layout, searchInteractor)
return view
}
@ -151,7 +139,7 @@ class SearchFragment : Fragment(), BackHandler {
(activity as HomeActivity)
.openToBrowserAndLoad(
searchTermOrURL = result,
newTab = sessionId == null,
newTab = searchStore.state.session == null,
from = BrowserDirection.FromSearch
)
dialog.dismiss()
@ -166,19 +154,16 @@ class SearchFragment : Fragment(), BackHandler {
)
view.search_scan_button.setOnClickListener {
getManagedEmitter<SearchChange>().onNext(SearchChange.ToolbarClearedFocus)
toolbarView.view.clearFocus()
requireComponents.analytics.metrics.track(Event.QRScannerOpened)
qrFeature.get()?.scan(R.id.container)
}
lifecycle.addObserver((toolbarComponent.uiView as ToolbarUIView).toolbarIntegration)
view.toolbar_wrapper.clipToOutline = false
search_shortcuts_button.setOnClickListener {
val isOpen = (awesomeBarComponent.uiView as AwesomeBarUIView).state?.showShortcutEnginePicker ?: false
getManagedEmitter<AwesomeBarChange>().onNext(AwesomeBarChange.SearchShortcutEnginePicker(!isOpen))
val isOpen = searchStore.state.showShortcutEnginePicker
searchStore.dispatch(SearchAction.SearchShortcutEnginePicker(!isOpen))
if (isOpen) {
requireComponents.analytics.metrics.track(Event.SearchShortcutMenuClosed)
@ -187,124 +172,72 @@ class SearchFragment : Fragment(), BackHandler {
}
}
searchStore.observe(view) {
MainScope().launch {
awesomeBarView.update(it)
toolbarView.update(it)
updateSearchEngineIcon(it)
updateSearchShortuctsIcon(it)
updateSearchWithLabel(it)
}
}
startPostponedEnterTransition()
}
override fun onResume() {
super.onResume()
subscribeToSearchActions()
subscribeToAwesomeBarActions()
if (!permissionDidUpdate) {
getManagedEmitter<SearchChange>().onNext(SearchChange.ToolbarRequestedFocus)
toolbarView.view.requestFocus()
}
permissionDidUpdate = false
(activity as AppCompatActivity).supportActionBar?.hide()
}
override fun onPause() {
super.onPause()
getManagedEmitter<SearchChange>().onNext(SearchChange.ToolbarClearedFocus)
toolbarView.view.clearFocus()
}
override fun onBackPressed(): Boolean {
return when {
qrFeature.onBackPressed() -> {
view?.search_scan_button?.isChecked = false
getManagedEmitter<SearchChange>().onNext(SearchChange.ToolbarRequestedFocus)
toolbarView.view.requestFocus()
true
}
else -> false
}
}
private fun subscribeToSearchActions() {
getAutoDisposeObservable<SearchAction>()
.subscribe {
when (it) {
is SearchAction.UrlCommitted -> {
if (it.url.isNotBlank()) {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = it.url,
newTab = sessionId == null,
from = BrowserDirection.FromSearch,
engine = it.engine
)
val event = if (it.url.isUrl()) {
Event.EnteredUrl(false)
} else {
val engine = it.engine ?: requireComponents
.search.searchEngineManager.getDefaultSearchEngine(requireContext())
createSearchEvent(engine, false)
}
requireComponents.analytics.metrics.track(event)
}
}
is SearchAction.TextChanged -> {
getManagedEmitter<SearchChange>().onNext(SearchChange.QueryTextChanged(it.query))
getManagedEmitter<AwesomeBarChange>().onNext(AwesomeBarChange.UpdateQuery(it.query))
}
is SearchAction.EditingCanceled -> {
Navigation.findNavController(toolbar_wrapper).navigateUp()
}
}
}
private fun updateSearchEngineIcon(searchState: SearchState) {
val searchIcon = searchState.searchEngineSource.searchEngine.icon
val draw = BitmapDrawable(resources, searchIcon)
val iconSize = resources.getDimension(R.dimen.preference_icon_drawable_size).toInt()
draw.setBounds(0, 0, iconSize, iconSize)
search_engine_icon?.backgroundDrawable = draw
}
private fun subscribeToAwesomeBarActions() {
getAutoDisposeObservable<AwesomeBarAction>()
.subscribe {
when (it) {
is AwesomeBarAction.URLTapped -> {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = it.url,
newTab = sessionId == null,
from = BrowserDirection.FromSearch
)
requireComponents.analytics.metrics.track(Event.EnteredUrl(false))
}
is AwesomeBarAction.SearchTermsTapped -> {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = it.searchTerms,
newTab = sessionId == null,
from = BrowserDirection.FromSearch,
engine = it.engine,
forceSearch = true
)
val engine = it.engine ?: requireComponents
.search.searchEngineManager.getDefaultSearchEngine(requireContext())
val event = createSearchEvent(engine, true)
requireComponents.analytics.metrics.track(event)
}
is AwesomeBarAction.SearchShortcutEngineSelected -> {
getManagedEmitter<AwesomeBarChange>()
.onNext(AwesomeBarChange.SearchShortcutEngineSelected(it.engine))
getManagedEmitter<SearchChange>()
.onNext(SearchChange.SearchShortcutEngineSelected(it.engine))
requireComponents.analytics.metrics.track(Event.SearchShortcutSelected(it.engine.name))
}
}
}
private fun updateSearchWithLabel(searchState: SearchState) {
search_with_shortcuts.visibility = if (searchState.showShortcutEnginePicker) View.VISIBLE else View.GONE
}
private fun createSearchEvent(engine: SearchEngine, isSuggestion: Boolean): Event.PerformedSearch {
val isShortcut = engine != requireComponents.search.searchEngineManager.defaultSearchEngine
private fun updateSearchShortuctsIcon(searchState: SearchState) {
with(requireContext()) {
val showShortcuts = searchState.showShortcutEnginePicker
search_shortcuts_button?.isChecked = showShortcuts
val engineSource =
if (isShortcut) Event.PerformedSearch.EngineSource.Shortcut(engine)
else Event.PerformedSearch.EngineSource.Default(engine)
val color = if (showShortcuts) R.attr.foundation else R.attr.primaryText
val source =
if (isSuggestion) Event.PerformedSearch.EventSource.Suggestion(engineSource)
else Event.PerformedSearch.EventSource.Action(engineSource)
return Event.PerformedSearch(source)
search_shortcuts_button.compoundDrawables[0]?.setTint(
ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(color, this)
)
)
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
@ -324,8 +257,13 @@ class SearchFragment : Fragment(), BackHandler {
}
}
private fun historyStorageProvider(): HistoryStorage? {
return if (Settings.getInstance(requireContext()).shouldShowVisitedSitesBookmarks) {
requireComponents.core.historyStorage
} else null
}
companion object {
private const val SHARED_TRANSITION_MS = 150L
private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1
}
}

View File

@ -0,0 +1,101 @@
/* 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.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.searchEngineManager
import org.mozilla.fenix.search.awesomebar.AwesomeBarInteractor
import org.mozilla.fenix.search.toolbar.ToolbarInteractor
/**
* Interactor for the search screen
* Provides implementations for the AwesomeBarView and ToolbarView
*/
class SearchInteractor(
private val context: Context,
private val navController: NavController,
private val store: SearchStore
) : AwesomeBarInteractor, ToolbarInteractor {
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)
}
}
override fun onEditingCanceled() {
navController.navigateUp()
}
override fun onTextChanged(text: String) {
store.dispatch(SearchAction.UpdateQuery(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))
}
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)
}
override fun onSearchShortcutEngineSelected(searchEngine: SearchEngine) {
store.dispatch(SearchAction.SearchShortcutEngineSelected(searchEngine))
context.metrics.track(Event.SearchShortcutSelected(searchEngine.name))
}
override fun onClickSearchEngineSettings() {
val directions = SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment()
navController.navigate(directions)
}
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,70 @@
/* 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 mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.session.Session
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* An alias to make it easier to work with `Store<SearchState, SearchAction>`
*/
typealias SearchStore = Store<SearchState, SearchAction>
/**
* Wraps a `SearchEngine` to give consumers the context that it was selected as a shortcut
*/
sealed class SearchEngineSource {
abstract val searchEngine: SearchEngine
data class Default(override val searchEngine: SearchEngine) : SearchEngineSource()
data class Shortcut(override val searchEngine: SearchEngine) : SearchEngineSource()
}
/**
* The state for the Search Screen
* @property query The current search query string
* @property showShortcutEnginePicker Whether or not to show the available search engine view
* @property searchEngineSource The current selected search engine with the context of how it was selected
* @property showSuggestions Whether or not to show search suggestions for the selected search engine in the AwesomeBar
* @property showVisitedSitesBookmarks Whether or not to show history and bookmark suggestions in the AwesomeBar
* @property session The current session if available
*/
data class SearchState(
val query: String,
val showShortcutEnginePicker: Boolean,
val searchEngineSource: SearchEngineSource,
val showSuggestions: Boolean,
val showVisitedSitesBookmarks: Boolean,
val session: Session?
) : State
/**
* Actions to dispatch through the `SearchStore` to modify `SearchState` through the reducer.
*/
sealed class SearchAction : Action {
data class SearchShortcutEngineSelected(val engine: SearchEngine) : SearchAction()
data class SearchShortcutEnginePicker(val show: Boolean) : SearchAction()
data class UpdateQuery(val query: String) : SearchAction()
}
/**
* The SearchState Reducer.
*/
fun searchStateReducer(state: SearchState, action: SearchAction): SearchState {
return when (action) {
is SearchAction.SearchShortcutEngineSelected ->
state.copy(
searchEngineSource = SearchEngineSource.Shortcut(action.engine),
showShortcutEnginePicker = false
)
is SearchAction.SearchShortcutEnginePicker ->
state.copy(showShortcutEnginePicker = action.show)
is SearchAction.UpdateQuery ->
state.copy(query = action.query)
}
}

View File

@ -1,66 +0,0 @@
/* 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.awesomebar
import android.view.ViewGroup
import mozilla.components.browser.search.SearchEngine
import org.mozilla.fenix.mvi.ViewState
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
data class AwesomeBarState(
val query: String,
val showShortcutEnginePicker: Boolean,
val suggestionEngine: SearchEngine? = null
) : ViewState
sealed class AwesomeBarAction : Action {
data class URLTapped(val url: String) : AwesomeBarAction()
data class SearchTermsTapped(val searchTerms: String, val engine: SearchEngine? = null) : AwesomeBarAction()
data class SearchShortcutEngineSelected(val engine: SearchEngine) : AwesomeBarAction()
}
sealed class AwesomeBarChange : Change {
data class SearchShortcutEngineSelected(val engine: SearchEngine) : AwesomeBarChange()
data class SearchShortcutEnginePicker(val show: Boolean) : AwesomeBarChange()
data class UpdateQuery(val query: String) : AwesomeBarChange()
}
class AwesomeBarComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
viewModelProvider: UIComponentViewModelProvider<AwesomeBarState, AwesomeBarChange>
) : UIComponent<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
bus.getManagedEmitter(AwesomeBarAction::class.java),
bus.getSafeManagedObservable(AwesomeBarChange::class.java),
viewModelProvider
) {
override fun initView() = AwesomeBarUIView(container, actionEmitter, changesObservable)
init {
bind()
}
}
class AwesomeBarViewModel(
initialState: AwesomeBarState
) : UIComponentViewModelBase<AwesomeBarState, AwesomeBarChange>(initialState, reducer) {
companion object {
val reducer: Reducer<AwesomeBarState, AwesomeBarChange> = { state, change ->
when (change) {
is AwesomeBarChange.SearchShortcutEngineSelected ->
state.copy(suggestionEngine = change.engine, showShortcutEnginePicker = false)
is AwesomeBarChange.SearchShortcutEnginePicker ->
state.copy(showShortcutEnginePicker = change.show)
is AwesomeBarChange.UpdateQuery -> state.copy(query = change.query)
}
}
}
}

View File

@ -1,219 +0,0 @@
/* 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.awesomebar
import android.graphics.PorterDuff.Mode.SRC_IN
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.search.SearchEngine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.mvi.UIView
import org.mozilla.fenix.utils.Settings
class AwesomeBarUIView(
private val container: ViewGroup,
actionEmitter: Observer<AwesomeBarAction>,
changesObservable: Observable<AwesomeBarChange>
) :
UIView<AwesomeBarState, AwesomeBarAction, AwesomeBarChange>(
container,
actionEmitter,
changesObservable
) {
override val view: BrowserAwesomeBar = LayoutInflater.from(container.context)
.inflate(R.layout.component_awesomebar, container, true)
.findViewById(R.id.awesomeBar)
var state: AwesomeBarState? = null
private set
private var clipboardSuggestionProvider: ClipboardSuggestionProvider? = null
private var sessionProvider: SessionSuggestionProvider? = null
private var historyStorageProvider: HistoryStorageSuggestionProvider? = null
private var shortcutsEnginePickerProvider: ShortcutsSuggestionProvider? = null
private var bookmarksStorageSuggestionProvider: BookmarksStorageSuggestionProvider? = null
private val searchSuggestionProvider: SearchSuggestionProvider?
get() = searchSuggestionFromShortcutProvider ?: defaultSearchSuggestionProvider!!
private var defaultSearchSuggestionProvider: SearchSuggestionProvider? = null
private var searchSuggestionFromShortcutProvider: SearchSuggestionProvider? = null
private val shortcutEngineManager by lazy {
ShortcutEngineManager(
this,
actionEmitter,
::setShortcutEngine,
::showSuggestionProviders,
::showSearchSuggestionProvider
)
}
private val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase {
override fun invoke(url: String, flags: EngineSession.LoadUrlFlags) {
actionEmitter.onNext(AwesomeBarAction.URLTapped(url))
}
}
private val searchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
actionEmitter.onNext(AwesomeBarAction.SearchTermsTapped(searchTerms, searchEngine))
}
}
private val shortcutSearchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
actionEmitter.onNext(
AwesomeBarAction.SearchTermsTapped(
searchTerms,
state?.suggestionEngine
)
)
}
}
init {
with(container.context) {
val primaryTextColor = ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
)
val draw = getDrawable(R.drawable.ic_link)
draw?.setColorFilter(primaryTextColor, SRC_IN)
clipboardSuggestionProvider = ClipboardSuggestionProvider(
this,
loadUrlUseCase,
draw!!.toBitmap(),
getString(R.string.awesomebar_clipboard_title)
)
sessionProvider =
SessionSuggestionProvider(
components.core.sessionManager,
components.useCases.tabsUseCases.selectTab,
components.core.icons
)
historyStorageProvider =
HistoryStorageSuggestionProvider(
components.core.historyStorage,
loadUrlUseCase,
components.core.icons
)
bookmarksStorageSuggestionProvider =
BookmarksStorageSuggestionProvider(
components.core.bookmarksStorage,
loadUrlUseCase,
components.core.icons
)
if (Settings.getInstance(container.context).showSearchSuggestions) {
val searchDrawable = getDrawable(R.drawable.ic_search)
searchDrawable?.setColorFilter(primaryTextColor, SRC_IN)
defaultSearchSuggestionProvider =
SearchSuggestionProvider(
searchEngine = components.search.searchEngineManager.getDefaultSearchEngine(
this
),
searchUseCase = searchUseCase,
fetchClient = components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
limit = 3,
icon = searchDrawable?.toBitmap()
)
}
shortcutsEnginePickerProvider =
ShortcutsSuggestionProvider(
components.search.searchEngineManager,
this,
shortcutEngineManager::selectShortcutEngine,
shortcutEngineManager::selectShortcutEngineSettings
)
shortcutEngineManager.shortcutsEnginePickerProvider = shortcutsEnginePickerProvider
val listener = object : RecyclerView.OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
view.hideKeyboard()
return false
}
}
view.onFlingListener = listener
}
}
private fun showSuggestionProviders() {
if (Settings.getInstance(container.context).showSearchSuggestions) {
view.addProviders(searchSuggestionProvider!!)
}
if (Settings.getInstance(container.context).shouldShowVisitedSitesBookmarks) {
view.addProviders(bookmarksStorageSuggestionProvider!!)
view.addProviders(historyStorageProvider!!)
}
view.addProviders(
clipboardSuggestionProvider!!,
sessionProvider!!
)
}
private fun showSearchSuggestionProvider() {
if (Settings.getInstance(container.context).showSearchSuggestions) {
view.addProviders(searchSuggestionProvider!!)
}
}
private fun setShortcutEngine(engine: SearchEngine) {
with(container.context) {
val draw = getDrawable(R.drawable.ic_search)
draw?.setColorFilter(
ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
), SRC_IN
)
searchSuggestionFromShortcutProvider =
SearchSuggestionProvider(
components.search.searchEngineManager.getDefaultSearchEngine(this, engine.name),
shortcutSearchUseCase,
components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
icon = draw?.toBitmap()
)
}
}
override fun updateView() = Consumer<AwesomeBarState> {
shortcutEngineManager.updateSelectedEngineIfNecessary(it)
shortcutEngineManager.updateEnginePickerVisibilityIfNecessary(it)
view.onInputChanged(it.query)
state = it
}
}

View File

@ -0,0 +1,207 @@
package org.mozilla.fenix.search.awesomebar
/* 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/. */
import android.graphics.PorterDuff.Mode.SRC_IN
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.search.SearchEngine
import mozilla.components.concept.engine.EngineSession
import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider
import mozilla.components.feature.awesomebar.provider.ClipboardSuggestionProvider
import mozilla.components.feature.awesomebar.provider.HistoryStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.BookmarksStorageSuggestionProvider
import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.search.SearchEngineSource
import org.mozilla.fenix.search.SearchState
/**
* Interface for the AwesomeBarView Interactor. This interface is implemented by objects that want
* to respond to user interaction on the AwesomebarView
*/
interface AwesomeBarInteractor {
/**
* Called whenever a suggestion containing a URL is tapped
* @param url the url the suggestion was providing
*/
fun onUrlTapped(url: String)
/**
* Called whenever a search engine suggestion is tapped
* @param searchTerms the query contained by the search suggestion
*/
fun onSearchTermsTapped(searchTerms: String)
/**
* Called whenever a search engine shortcut is tapped
* @param searchEngine the searchEngine that was selected
*/
fun onSearchShortcutEngineSelected(searchEngine: SearchEngine)
/**
* Called whenever the "Search Engine Settings" item is tapped
*/
fun onClickSearchEngineSettings()
}
/**
* View that contains and configures the BrowserAwesomeBar
*/
class AwesomeBarView(
private val container: ViewGroup,
val interactor: AwesomeBarInteractor
) : LayoutContainer {
val view: BrowserAwesomeBar = LayoutInflater.from(container.context)
.inflate(R.layout.component_awesomebar, container, true)
.findViewById(R.id.awesomeBar)
override val containerView: View?
get() = container
private val clipboardSuggestionProvider: ClipboardSuggestionProvider
private val sessionProvider: SessionSuggestionProvider
private val historyStorageProvider: HistoryStorageSuggestionProvider
private val shortcutsEnginePickerProvider: ShortcutsSuggestionProvider
private val bookmarksStorageSuggestionProvider: BookmarksStorageSuggestionProvider
private val defaultSearchSuggestionProvider: SearchSuggestionProvider
private val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase {
override fun invoke(url: String, flags: EngineSession.LoadUrlFlags) {
interactor.onUrlTapped(url)
}
}
private val searchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
interactor.onSearchTermsTapped(searchTerms)
}
}
private val shortcutSearchUseCase = object : SearchUseCases.SearchUseCase {
override fun invoke(searchTerms: String, searchEngine: SearchEngine?) {
interactor.onSearchTermsTapped(searchTerms)
}
}
init {
with(container.context) {
val primaryTextColor = ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
)
val draw = getDrawable(R.drawable.ic_link)!!
draw.setColorFilter(primaryTextColor, SRC_IN)
clipboardSuggestionProvider = ClipboardSuggestionProvider(
this,
loadUrlUseCase,
draw.toBitmap(),
getString(R.string.awesomebar_clipboard_title)
)
sessionProvider =
SessionSuggestionProvider(
components.core.sessionManager,
components.useCases.tabsUseCases.selectTab,
components.core.icons
)
historyStorageProvider =
HistoryStorageSuggestionProvider(
components.core.historyStorage,
loadUrlUseCase,
components.core.icons
)
bookmarksStorageSuggestionProvider =
BookmarksStorageSuggestionProvider(
components.core.bookmarksStorage,
loadUrlUseCase,
components.core.icons
)
val searchDrawable = getDrawable(R.drawable.ic_search)!!
searchDrawable.setColorFilter(primaryTextColor, SRC_IN)
defaultSearchSuggestionProvider =
SearchSuggestionProvider(
searchEngine = components.search.searchEngineManager.getDefaultSearchEngine(
this
),
searchUseCase = searchUseCase,
fetchClient = components.core.client,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
limit = 3,
icon = searchDrawable.toBitmap()
)
shortcutsEnginePickerProvider =
ShortcutsSuggestionProvider(
components.search.searchEngineManager,
this,
interactor::onSearchShortcutEngineSelected,
interactor::onClickSearchEngineSettings
)
}
}
fun update(state: SearchState) {
view.removeAllProviders()
if (state.showShortcutEnginePicker) {
view.addProviders(shortcutsEnginePickerProvider)
} else {
if (state.showSuggestions) {
view.addProviders(when (state.searchEngineSource) {
is SearchEngineSource.Default -> defaultSearchSuggestionProvider
is SearchEngineSource.Shortcut -> createSuggestionProviderForEngine(
state.searchEngineSource.searchEngine
)
})
}
if (state.showVisitedSitesBookmarks) {
view.addProviders(bookmarksStorageSuggestionProvider, historyStorageProvider)
}
view.addProviders(clipboardSuggestionProvider, sessionProvider)
}
view.onInputChanged(state.query)
}
private fun createSuggestionProviderForEngine(engine: SearchEngine): SearchSuggestionProvider {
return with(container.context) {
val draw = getDrawable(R.drawable.ic_search)
draw?.setColorFilter(
ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
), SRC_IN
)
SearchSuggestionProvider(
components.search.searchEngineManager.getDefaultSearchEngine(this, engine.name),
shortcutSearchUseCase,
components.core.client,
limit = 3,
mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS,
icon = draw?.toBitmap()
)
}
}
}

View File

@ -1,99 +0,0 @@
/* 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.awesomebar
import android.view.View
import androidx.core.content.ContextCompat
import androidx.navigation.Navigation
import io.reactivex.Observer
import kotlinx.android.synthetic.main.fragment_search.*
import mozilla.components.browser.search.SearchEngine
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.search.SearchFragmentDirections
class ShortcutEngineManager(
private val awesomeBarUIView: AwesomeBarUIView,
private val actionEmitter: Observer<AwesomeBarAction>,
private val setShortcutEngine: (newEngine: SearchEngine) -> Unit,
private val showSuggestionProviders: () -> Unit,
private val showSearchSuggestionProvider: () -> Unit
) {
var shortcutsEnginePickerProvider: ShortcutsSuggestionProvider? = null
val context = awesomeBarUIView.containerView?.context!!
fun updateSelectedEngineIfNecessary(newState: AwesomeBarState) {
if (engineDidChange(newState)) {
newState.suggestionEngine?.let { newEngine ->
setShortcutEngine(newEngine)
}
}
}
fun updateEnginePickerVisibilityIfNecessary(newState: AwesomeBarState) {
if (shouldUpdateShortcutEnginePickerVisibility(newState)) {
if (newState.showShortcutEnginePicker) {
showShortcutEnginePicker()
updateSearchWithVisibility(true)
} else {
hideShortcutEnginePicker()
updateSearchWithVisibility(false)
newState.suggestionEngine?.also { showSearchSuggestionProvider() } ?: showSuggestionProviders()
}
}
}
fun selectShortcutEngine(engine: SearchEngine) {
actionEmitter.onNext(AwesomeBarAction.SearchShortcutEngineSelected(engine))
}
fun selectShortcutEngineSettings() {
val directions = SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment()
Navigation.findNavController(awesomeBarUIView.view).navigate(directions)
}
private fun engineDidChange(newState: AwesomeBarState): Boolean {
return awesomeBarUIView.state?.suggestionEngine != newState.suggestionEngine
}
private fun shouldUpdateShortcutEnginePickerVisibility(newState: AwesomeBarState): Boolean {
return awesomeBarUIView.state?.showShortcutEnginePicker != newState.showShortcutEnginePicker
}
private fun showShortcutEnginePicker() {
with(context) {
awesomeBarUIView.search_shortcuts_button?.isChecked = true
awesomeBarUIView.search_shortcuts_button.compoundDrawables[0]?.setTint(
ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.foundation, this)
)
)
awesomeBarUIView.view.removeAllProviders()
awesomeBarUIView.view.addProviders(shortcutsEnginePickerProvider!!)
}
}
private fun hideShortcutEnginePicker() {
with(context) {
awesomeBarUIView.search_shortcuts_button?.isChecked = false
awesomeBarUIView.search_shortcuts_button.compoundDrawables[0]?.setTint(
ContextCompat.getColor(
this,
ThemeManager.resolveAttribute(R.attr.primaryText, this)
)
)
awesomeBarUIView.view.removeProviders(shortcutsEnginePickerProvider!!)
}
}
private fun updateSearchWithVisibility(visible: Boolean) {
awesomeBarUIView.search_with_shortcuts.visibility = if (visible) View.VISIBLE else View.GONE
}
}

View File

@ -0,0 +1,126 @@
/* 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.toolbar
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
import mozilla.components.support.ktx.android.content.res.pxToDp
import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.search.SearchState
/**
* Interface for the Toolbar Interactor. This interface is implemented by objects that want
* to respond to user interaction on the ToolbarView
*/
interface ToolbarInteractor {
/**
* Called when a user hits the return key while ToolbarView has focus.
* @param url the text inside the ToolbarView when committed
*/
fun onUrlCommitted(url: String)
/**
* Called when a removes focus from the ToolbarView
*/
fun onEditingCanceled()
/**
* Called whenever the text inside the ToolbarView changes
* @param text the current text displayed by ToolbarView
*/
fun onTextChanged(text: String)
}
/**
* View that contains and configures the BrowserToolbar to only be used in its editing mode.
*/
class ToolbarView(
private val container: ViewGroup,
private val interactor: ToolbarInteractor,
private val historyStorage: HistoryStorage?
) : LayoutContainer {
override val containerView: View?
get() = container
val view: BrowserToolbar = LayoutInflater.from(container.context)
.inflate(R.layout.component_search, container, true)
.findViewById(R.id.toolbar)
private var isInitialized = false
init {
view.apply {
editMode()
elevation = resources.pxToDp(TOOLBAR_ELEVATION_IN_DP).toFloat()
setOnUrlCommitListener {
interactor.onUrlCommitted(it)
false
}
background = null
layoutParams.height = CoordinatorLayout.LayoutParams.MATCH_PARENT
hint = context.getString(R.string.search_hint)
textColor = ContextCompat.getColor(
container.context,
ThemeManager.resolveAttribute(R.attr.primaryText, container.context)
)
hintColor = ContextCompat.getColor(
container.context,
ThemeManager.resolveAttribute(R.attr.secondaryText, container.context)
)
suggestionBackgroundColor = ContextCompat.getColor(
container.context,
R.color.suggestion_highlight_color
)
setOnEditListener(object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
override fun onCancelEditing(): Boolean {
interactor.onEditingCanceled()
return false
}
override fun onTextChanged(text: String) {
url = text
this@ToolbarView.interactor.onTextChanged(text)
}
})
}
ToolbarAutocompleteFeature(view).apply {
addDomainProvider(ShippedDomainsProvider().also { it.initialize(view.context) })
historyStorage?.also(::addHistoryStorageProvider)
}
}
fun update(searchState: SearchState) {
if (!isInitialized) {
view.url = searchState.query
view.setSearchTerms(searchState.session?.searchTerms ?: "")
view.editMode()
isInitialized = true
}
}
companion object {
private const val TOOLBAR_ELEVATION_IN_DP = 16
}
}

View File

@ -0,0 +1,156 @@
/* 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.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.verify
import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.SearchEngineManager
import org.junit.Test
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.searchEngineManager
class SearchInteractorTest {
@Test
fun onUrlCommitted() {
val context: HomeActivity = mockk()
val store: SearchStore = mockk()
val state: SearchState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk())
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
val interactor = SearchInteractor(context, mockk(), store)
interactor.onUrlCommitted("test")
verify {
context.openToBrowserAndLoad(
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine
)
}
}
@Test
fun onEditingCanceled() {
val navController: NavController = mockk(relaxed = true)
val interactor = SearchInteractor(mockk(), navController, mockk())
interactor.onEditingCanceled()
verify {
navController.navigateUp()
}
}
@Test
fun onTextChanged() {
val store: SearchStore = mockk(relaxed = true)
val interactor = SearchInteractor(mockk(), mockk(), store)
interactor.onTextChanged("test")
verify { store.dispatch(SearchAction.UpdateQuery("test")) }
}
@Test
fun onUrlTapped() {
val context: HomeActivity = mockk()
val store: SearchStore = mockk()
val state: SearchState = mockk()
every { context.metrics } returns mockk(relaxed = true)
every { context.openToBrowserAndLoad(any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
val interactor = SearchInteractor(context, mockk(), store)
interactor.onUrlTapped("test")
verify {
context.openToBrowserAndLoad(
"test",
true,
BrowserDirection.FromSearch
)
}
}
@Test
fun onSearchTermsTapped() {
val context: HomeActivity = mockk()
val store: SearchStore = mockk()
val state: SearchState = mockk()
val searchEngineManager: SearchEngineManager = mockk(relaxed = true)
val searchEngine = SearchEngineSource.Default(mockk())
every { context.metrics } returns mockk(relaxed = true)
every { context.searchEngineManager } returns searchEngineManager
every { context.openToBrowserAndLoad(any(), any(), any(), any(), any(), any()) } just Runs
every { store.state } returns state
every { state.session } returns null
every { state.searchEngineSource } returns searchEngine
val interactor = SearchInteractor(context, mockk(), store)
interactor.onSearchTermsTapped("test")
verify { context.openToBrowserAndLoad(
searchTermOrURL = "test",
newTab = true,
from = BrowserDirection.FromSearch,
engine = searchEngine.searchEngine,
forceSearch = true
) }
}
@Test
fun onSearchShortcutEngineSelected() {
val context: HomeActivity = mockk(relaxed = true)
every { context.metrics } returns mockk(relaxed = true)
val store: SearchStore = mockk(relaxed = true)
val interactor = SearchInteractor(context, mockk(), store)
val searchEngine: SearchEngine = mockk(relaxed = true)
interactor.onSearchShortcutEngineSelected(searchEngine)
verify { store.dispatch(SearchAction.SearchShortcutEngineSelected(searchEngine)) }
}
@Test
fun onClickSearchEngineSettings() {
val navController: NavController = mockk()
val interactor = SearchInteractor(mockk(), navController, mockk())
every { navController.navigate(any() as NavDirections) } just Runs
interactor.onClickSearchEngineSettings()
verify {
navController.navigate(SearchFragmentDirections.actionSearchFragmentToSearchEngineFragment())
}
}
}

View File

@ -0,0 +1,2 @@
mock-maker-inline
// This allows mocking final classes (classes are final by default in Kotlin)

View File

@ -46,7 +46,7 @@ private object Versions {
const val installreferrer = "1.0"
const val junit = "4.12"
const val mockito = "2.23.0"
const val mockito = "2.24.5"
const val mockk = "1.9.kotlin12"
const val glide = "4.9.0"
const val flipper = "0.21.0"