Bug 1849073 - Part 2: Remove the History Search Dialog
parent
08be473cbf
commit
46b27b6ad9
@ -1,47 +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.library.history
|
|
||||||
|
|
||||||
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
|
|
||||||
import mozilla.components.service.glean.private.NoExtras
|
|
||||||
import org.mozilla.fenix.BrowserDirection
|
|
||||||
import org.mozilla.fenix.GleanMetrics.History
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface that handles the view manipulation of the History Search, triggered by the Interactor
|
|
||||||
*/
|
|
||||||
interface HistorySearchController {
|
|
||||||
fun handleEditingCancelled()
|
|
||||||
fun handleTextChanged(text: String)
|
|
||||||
fun handleUrlTapped(url: String, flags: LoadUrlFlags = LoadUrlFlags.none())
|
|
||||||
}
|
|
||||||
|
|
||||||
class HistorySearchDialogController(
|
|
||||||
private val activity: HomeActivity,
|
|
||||||
private val fragmentStore: HistorySearchFragmentStore,
|
|
||||||
private val clearToolbarFocus: () -> Unit,
|
|
||||||
) : HistorySearchController {
|
|
||||||
|
|
||||||
override fun handleEditingCancelled() {
|
|
||||||
clearToolbarFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleTextChanged(text: String) {
|
|
||||||
fragmentStore.dispatch(HistorySearchFragmentAction.UpdateQuery(text))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleUrlTapped(url: String, flags: LoadUrlFlags) {
|
|
||||||
History.searchResultTapped.record(NoExtras())
|
|
||||||
clearToolbarFocus()
|
|
||||||
|
|
||||||
activity.openToBrowserAndLoad(
|
|
||||||
searchTermOrURL = url,
|
|
||||||
newTab = true,
|
|
||||||
from = BrowserDirection.FromHistorySearchDialog,
|
|
||||||
flags = flags,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,314 +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.library.history
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.speech.RecognizerIntent
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewStub
|
|
||||||
import android.view.accessibility.AccessibilityEvent
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import androidx.activity.result.ActivityResult
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatDialogFragment
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
|
||||||
import androidx.constraintlayout.widget.ConstraintProperties.BOTTOM
|
|
||||||
import androidx.constraintlayout.widget.ConstraintProperties.PARENT_ID
|
|
||||||
import androidx.constraintlayout.widget.ConstraintProperties.TOP
|
|
||||||
import androidx.constraintlayout.widget.ConstraintSet
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import mozilla.components.browser.toolbar.BrowserToolbar
|
|
||||||
import mozilla.components.lib.state.ext.consumeFlow
|
|
||||||
import mozilla.components.lib.state.ext.consumeFrom
|
|
||||||
import mozilla.components.support.base.feature.UserInteractionHandler
|
|
||||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
|
||||||
import org.mozilla.fenix.BrowserDirection
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.components.toolbar.ToolbarPosition
|
|
||||||
import org.mozilla.fenix.databinding.FragmentHistorySearchDialogBinding
|
|
||||||
import org.mozilla.fenix.databinding.SearchSuggestionsHintBinding
|
|
||||||
import org.mozilla.fenix.ext.settings
|
|
||||||
import org.mozilla.fenix.library.history.awesomebar.AwesomeBarView
|
|
||||||
import org.mozilla.fenix.library.history.toolbar.ToolbarView
|
|
||||||
import org.mozilla.fenix.settings.SupportUtils
|
|
||||||
|
|
||||||
@Suppress("TooManyFunctions", "LargeClass")
|
|
||||||
class HistorySearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
|
|
||||||
private var _binding: FragmentHistorySearchDialogBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private lateinit var interactor: HistorySearchDialogInteractor
|
|
||||||
private lateinit var store: HistorySearchFragmentStore
|
|
||||||
private lateinit var toolbarView: ToolbarView
|
|
||||||
private lateinit var awesomeBarView: AwesomeBarView
|
|
||||||
|
|
||||||
private val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
|
|
||||||
private var voiceSearchButtonAlreadyAdded = false
|
|
||||||
private var dialogHandledAction = false
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
setStyle(STYLE_NO_TITLE, R.style.SearchDialogStyle)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
|
||||||
return object : Dialog(requireContext(), this.theme) {
|
|
||||||
@Deprecated("Deprecated in Java")
|
|
||||||
override fun onBackPressed() {
|
|
||||||
this@HistorySearchDialogFragment.onBackPressed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?,
|
|
||||||
): View {
|
|
||||||
_binding = FragmentHistorySearchDialogBinding.inflate(inflater, container, false)
|
|
||||||
val activity = requireActivity() as HomeActivity
|
|
||||||
|
|
||||||
store = HistorySearchFragmentStore(
|
|
||||||
createInitialHistorySearchFragmentState(),
|
|
||||||
)
|
|
||||||
|
|
||||||
interactor = HistorySearchDialogInteractor(
|
|
||||||
HistorySearchDialogController(
|
|
||||||
activity = activity,
|
|
||||||
fragmentStore = store,
|
|
||||||
clearToolbarFocus = {
|
|
||||||
dialogHandledAction = true
|
|
||||||
toolbarView.view.hideKeyboard()
|
|
||||||
toolbarView.view.clearFocus()
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
toolbarView = ToolbarView(
|
|
||||||
context = requireContext(),
|
|
||||||
interactor = interactor,
|
|
||||||
isPrivate = false,
|
|
||||||
view = binding.toolbar,
|
|
||||||
)
|
|
||||||
|
|
||||||
val awesomeBar = binding.awesomeBar
|
|
||||||
|
|
||||||
awesomeBarView = AwesomeBarView(
|
|
||||||
activity,
|
|
||||||
interactor,
|
|
||||||
awesomeBar,
|
|
||||||
)
|
|
||||||
|
|
||||||
awesomeBarView.view.setOnEditSuggestionListener(toolbarView.view::setSearchTerms)
|
|
||||||
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
setupConstraints(view)
|
|
||||||
|
|
||||||
binding.searchWrapper.setOnTouchListener { _, _ ->
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
val stubListener = ViewStub.OnInflateListener { _, inflated ->
|
|
||||||
val searchSuggestionHintBinding = SearchSuggestionsHintBinding.bind(inflated)
|
|
||||||
|
|
||||||
searchSuggestionHintBinding.learnMore.setOnClickListener {
|
|
||||||
(activity as HomeActivity)
|
|
||||||
.openToBrowserAndLoad(
|
|
||||||
searchTermOrURL = SupportUtils.getGenericSumoURLForTopic(
|
|
||||||
SupportUtils.SumoTopic.SEARCH_SUGGESTION,
|
|
||||||
),
|
|
||||||
newTab = true,
|
|
||||||
from = BrowserDirection.FromHistorySearchDialog,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
searchSuggestionHintBinding.allow.setOnClickListener {
|
|
||||||
inflated.visibility = View.GONE
|
|
||||||
requireContext().settings().also {
|
|
||||||
it.shouldShowSearchSuggestionsInPrivate = true
|
|
||||||
it.showSearchSuggestionsInPrivateOnboardingFinished = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchSuggestionHintBinding.dismiss.setOnClickListener {
|
|
||||||
inflated.visibility = View.GONE
|
|
||||||
requireContext().settings().also {
|
|
||||||
it.shouldShowSearchSuggestionsInPrivate = false
|
|
||||||
it.showSearchSuggestionsInPrivateOnboardingFinished = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchSuggestionHintBinding.text.text =
|
|
||||||
getString(R.string.search_suggestions_onboarding_text, getString(R.string.app_name))
|
|
||||||
|
|
||||||
searchSuggestionHintBinding.title.text =
|
|
||||||
getString(R.string.search_suggestions_onboarding_title)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.searchSuggestionsHintDivider.isVisible = false
|
|
||||||
binding.searchSuggestionsHint.isVisible = false
|
|
||||||
binding.searchSuggestionsHint.setOnInflateListener((stubListener))
|
|
||||||
if (view.context.settings().accessibilityServicesEnabled) {
|
|
||||||
updateAccessibilityTraversalOrder()
|
|
||||||
}
|
|
||||||
|
|
||||||
addVoiceSearchButton()
|
|
||||||
observeAwesomeBarState()
|
|
||||||
|
|
||||||
consumeFrom(store) {
|
|
||||||
toolbarView.update(it)
|
|
||||||
awesomeBarView.update(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeAwesomeBarState() = consumeFlow(store) { flow ->
|
|
||||||
flow.map { state -> state.query.isNotBlank() }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.collect { shouldShowAwesomebar ->
|
|
||||||
binding.awesomeBar.visibility = if (shouldShowAwesomebar) {
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
View.INVISIBLE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateAccessibilityTraversalOrder() {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) {
|
|
||||||
viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
binding.searchWrapper.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
super.onPause()
|
|
||||||
view?.hideKeyboard()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This way of dismissing the keyboard is needed to smoothly dismiss the keyboard while the dialog
|
|
||||||
* is also dismissing.
|
|
||||||
*/
|
|
||||||
private fun hideDeviceKeyboard() {
|
|
||||||
// If the interactor/controller has handled a search event itself, it will hide the keyboard.
|
|
||||||
if (!dialogHandledAction) {
|
|
||||||
val imm =
|
|
||||||
requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
|
||||||
imm.hideSoftInputFromWindow(view?.windowToken, InputMethodManager.HIDE_IMPLICIT_ONLY)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
hideDeviceKeyboard()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean {
|
|
||||||
view?.hideKeyboard()
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupConstraints(view: View) {
|
|
||||||
if (view.context.settings().toolbarPosition == ToolbarPosition.BOTTOM) {
|
|
||||||
ConstraintSet().apply {
|
|
||||||
clone(binding.searchWrapper)
|
|
||||||
|
|
||||||
clear(binding.toolbar.id, TOP)
|
|
||||||
connect(binding.toolbar.id, BOTTOM, PARENT_ID, BOTTOM)
|
|
||||||
|
|
||||||
clear(binding.pillWrapper.id, BOTTOM)
|
|
||||||
connect(binding.pillWrapper.id, BOTTOM, binding.toolbar.id, TOP)
|
|
||||||
|
|
||||||
clear(binding.awesomeBar.id, TOP)
|
|
||||||
clear(binding.awesomeBar.id, BOTTOM)
|
|
||||||
connect(binding.awesomeBar.id, TOP, binding.searchSuggestionsHint.id, BOTTOM)
|
|
||||||
connect(binding.awesomeBar.id, BOTTOM, binding.pillWrapper.id, TOP)
|
|
||||||
|
|
||||||
clear(binding.searchSuggestionsHint.id, TOP)
|
|
||||||
clear(binding.searchSuggestionsHint.id, BOTTOM)
|
|
||||||
connect(binding.searchSuggestionsHint.id, TOP, PARENT_ID, TOP)
|
|
||||||
connect(binding.searchSuggestionsHint.id, BOTTOM, binding.searchHintBottomBarrier.id, TOP)
|
|
||||||
|
|
||||||
applyTo(binding.searchWrapper)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val startVoiceSearchForResult =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
|
||||||
if (result.resultCode == Activity.RESULT_OK) {
|
|
||||||
val intent = result.data
|
|
||||||
intent?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first()?.also {
|
|
||||||
toolbarView.view.edit.updateUrl(url = it, shouldHighlight = true)
|
|
||||||
interactor.onTextChanged(it)
|
|
||||||
toolbarView.view.edit.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addVoiceSearchButton() {
|
|
||||||
val shouldShowVoiceSearch = isSpeechAvailable() &&
|
|
||||||
requireContext().settings().shouldShowVoiceSearch
|
|
||||||
|
|
||||||
if (voiceSearchButtonAlreadyAdded || !shouldShowVoiceSearch) return
|
|
||||||
|
|
||||||
toolbarView.view.addEditActionEnd(
|
|
||||||
BrowserToolbar.Button(
|
|
||||||
imageDrawable = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_microphone)!!,
|
|
||||||
contentDescription = requireContext().getString(R.string.voice_search_content_description),
|
|
||||||
visible = { true },
|
|
||||||
listener = ::launchVoiceSearch,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
voiceSearchButtonAlreadyAdded = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun launchVoiceSearch() {
|
|
||||||
// Note if a user disables speech while the app is on the search fragment
|
|
||||||
// the voice button will still be available and *will* cause a crash if tapped,
|
|
||||||
// since the `visible` call is only checked on create. In order to avoid extra complexity
|
|
||||||
// around such a small edge case, we make the button have no functionality in this case.
|
|
||||||
if (!isSpeechAvailable()) { return }
|
|
||||||
|
|
||||||
speechIntent.apply {
|
|
||||||
putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
|
|
||||||
putExtra(RecognizerIntent.EXTRA_PROMPT, requireContext().getString(R.string.voice_search_explainer))
|
|
||||||
}
|
|
||||||
|
|
||||||
startVoiceSearchForResult.launch(speechIntent)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isSpeechAvailable(): Boolean = speechIntent.resolveActivity(requireContext().packageManager) != null
|
|
||||||
}
|
|
@ -1,30 +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.library.history
|
|
||||||
|
|
||||||
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
|
|
||||||
import org.mozilla.fenix.library.history.awesomebar.AwesomeBarInteractor
|
|
||||||
import org.mozilla.fenix.library.history.toolbar.ToolbarInteractor
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interactor for the history search
|
|
||||||
* Provides implementations for the AwesomeBarView and ToolbarView
|
|
||||||
*/
|
|
||||||
class HistorySearchDialogInteractor(
|
|
||||||
private val historySearchController: HistorySearchDialogController,
|
|
||||||
) : AwesomeBarInteractor, ToolbarInteractor {
|
|
||||||
|
|
||||||
override fun onEditingCanceled() {
|
|
||||||
historySearchController.handleEditingCancelled()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(text: String) {
|
|
||||||
historySearchController.handleTextChanged(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUrlTapped(url: String, flags: LoadUrlFlags) {
|
|
||||||
historySearchController.handleUrlTapped(url, flags)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +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.library.history
|
|
||||||
|
|
||||||
import mozilla.components.lib.state.Action
|
|
||||||
import mozilla.components.lib.state.State
|
|
||||||
import mozilla.components.lib.state.Store
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The [Store] for holding the [HistorySearchFragmentState] and applying [HistorySearchFragmentAction]s.
|
|
||||||
*/
|
|
||||||
class HistorySearchFragmentStore(
|
|
||||||
initialState: HistorySearchFragmentState,
|
|
||||||
) : Store<HistorySearchFragmentState, HistorySearchFragmentAction>(
|
|
||||||
initialState,
|
|
||||||
::historySearchStateReducer,
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The state for the History Search Screen
|
|
||||||
*
|
|
||||||
* @property query The current search query string
|
|
||||||
*/
|
|
||||||
data class HistorySearchFragmentState(
|
|
||||||
val query: String,
|
|
||||||
) : State
|
|
||||||
|
|
||||||
fun createInitialHistorySearchFragmentState(): HistorySearchFragmentState {
|
|
||||||
return HistorySearchFragmentState(query = "")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Actions to dispatch through the [HistorySearchFragmentStore] to modify [HistorySearchFragmentState]
|
|
||||||
* through the reducer.
|
|
||||||
*/
|
|
||||||
sealed class HistorySearchFragmentAction : Action {
|
|
||||||
data class UpdateQuery(val query: String) : HistorySearchFragmentAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The [HistorySearchFragmentState] Reducer.
|
|
||||||
*/
|
|
||||||
private fun historySearchStateReducer(
|
|
||||||
state: HistorySearchFragmentState,
|
|
||||||
action: HistorySearchFragmentAction,
|
|
||||||
): HistorySearchFragmentState {
|
|
||||||
return when (action) {
|
|
||||||
is HistorySearchFragmentAction.UpdateQuery ->
|
|
||||||
state.copy(query = action.query)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,20 +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.library.history.awesomebar
|
|
||||||
|
|
||||||
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, flags: LoadUrlFlags = LoadUrlFlags.none())
|
|
||||||
}
|
|
@ -1,61 +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.library.history.awesomebar
|
|
||||||
|
|
||||||
import mozilla.components.concept.engine.EngineSession
|
|
||||||
import mozilla.components.feature.awesomebar.provider.CombinedHistorySuggestionProvider
|
|
||||||
import mozilla.components.feature.session.SessionUseCases
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
|
||||||
import org.mozilla.fenix.components.Core.Companion.METADATA_SHORTCUT_SUGGESTION_LIMIT
|
|
||||||
import org.mozilla.fenix.ext.components
|
|
||||||
import org.mozilla.fenix.library.history.HistorySearchFragmentState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View that contains and configures the BrowserAwesomeBar
|
|
||||||
*/
|
|
||||||
class AwesomeBarView(
|
|
||||||
activity: HomeActivity,
|
|
||||||
val interactor: AwesomeBarInteractor,
|
|
||||||
val view: AwesomeBarWrapper,
|
|
||||||
) {
|
|
||||||
private val combinedHistoryProvider: CombinedHistorySuggestionProvider
|
|
||||||
|
|
||||||
private val loadUrlUseCase = object : SessionUseCases.LoadUrlUseCase {
|
|
||||||
override fun invoke(
|
|
||||||
url: String,
|
|
||||||
flags: EngineSession.LoadUrlFlags,
|
|
||||||
additionalHeaders: Map<String, String>?,
|
|
||||||
) {
|
|
||||||
interactor.onUrlTapped(url, flags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
val components = activity.components
|
|
||||||
|
|
||||||
val engineForSpeculativeConnects = when (activity.browsingModeManager.mode) {
|
|
||||||
BrowsingMode.Normal -> components.core.engine
|
|
||||||
BrowsingMode.Private -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
combinedHistoryProvider =
|
|
||||||
CombinedHistorySuggestionProvider(
|
|
||||||
historyStorage = components.core.historyStorage,
|
|
||||||
historyMetadataStorage = components.core.historyStorage,
|
|
||||||
loadUrlUseCase = loadUrlUseCase,
|
|
||||||
icons = components.core.icons,
|
|
||||||
engine = engineForSpeculativeConnects,
|
|
||||||
maxNumberOfSuggestions = METADATA_SHORTCUT_SUGGESTION_LIMIT,
|
|
||||||
showEditSuggestion = false,
|
|
||||||
)
|
|
||||||
|
|
||||||
view.addProviders(combinedHistoryProvider)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun update(state: HistorySearchFragmentState) {
|
|
||||||
view.onInputChanged(state.query)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +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.library.history.awesomebar
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.AbstractComposeView
|
|
||||||
import mozilla.components.compose.browser.awesomebar.AwesomeBar
|
|
||||||
import mozilla.components.compose.browser.awesomebar.AwesomeBarDefaults
|
|
||||||
import mozilla.components.compose.browser.awesomebar.AwesomeBarOrientation
|
|
||||||
import mozilla.components.concept.awesomebar.AwesomeBar
|
|
||||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.ext.components
|
|
||||||
import org.mozilla.fenix.ext.settings
|
|
||||||
import org.mozilla.fenix.theme.FirefoxTheme
|
|
||||||
import org.mozilla.fenix.theme.ThemeManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This wrapper wraps the `AwesomeBar()` composable and exposes it as a `View` and `concept-awesomebar`
|
|
||||||
* implementation to be integrated in the view hierarchy of [HistorySearchDialogFragment] until more parts
|
|
||||||
* of that screen have been refactored to use Jetpack Compose.
|
|
||||||
*/
|
|
||||||
class AwesomeBarWrapper @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0,
|
|
||||||
) : AbstractComposeView(context, attrs, defStyleAttr), AwesomeBar {
|
|
||||||
private val providers = mutableStateOf(emptyList<AwesomeBar.SuggestionProvider>())
|
|
||||||
private val text = mutableStateOf("")
|
|
||||||
private var onEditSuggestionListener: ((String) -> Unit)? = null
|
|
||||||
private var onStopListener: (() -> Unit)? = null
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun Content() {
|
|
||||||
if (providers.value.isEmpty()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val orientation = if (context.settings().shouldUseBottomToolbar) {
|
|
||||||
AwesomeBarOrientation.BOTTOM
|
|
||||||
} else {
|
|
||||||
AwesomeBarOrientation.TOP
|
|
||||||
}
|
|
||||||
|
|
||||||
FirefoxTheme {
|
|
||||||
AwesomeBar(
|
|
||||||
text = text.value,
|
|
||||||
providers = providers.value,
|
|
||||||
orientation = orientation,
|
|
||||||
colors = AwesomeBarDefaults.colors(
|
|
||||||
background = Color.Transparent,
|
|
||||||
title = ThemeManager.resolveAttributeColor(R.attr.textPrimary),
|
|
||||||
description = ThemeManager.resolveAttributeColor(R.attr.textSecondary),
|
|
||||||
autocompleteIcon = ThemeManager.resolveAttributeColor(R.attr.textSecondary),
|
|
||||||
),
|
|
||||||
onSuggestionClicked = { suggestion ->
|
|
||||||
suggestion.onSuggestionClicked?.invoke()
|
|
||||||
onStopListener?.invoke()
|
|
||||||
},
|
|
||||||
onAutoComplete = { suggestion ->
|
|
||||||
onEditSuggestionListener?.invoke(suggestion.editSuggestion!!)
|
|
||||||
},
|
|
||||||
onScroll = { hideKeyboard() },
|
|
||||||
profiler = context.components.core.engine.profiler,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addProviders(vararg providers: AwesomeBar.SuggestionProvider) {
|
|
||||||
val newProviders = this.providers.value.toMutableList()
|
|
||||||
newProviders.addAll(providers)
|
|
||||||
this.providers.value = newProviders
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun containsProvider(provider: AwesomeBar.SuggestionProvider): Boolean {
|
|
||||||
return providers.value.any { current -> current.id == provider.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInputChanged(text: String) {
|
|
||||||
this.text.value = text
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeAllProviders() {
|
|
||||||
providers.value = emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun removeProviders(vararg providers: AwesomeBar.SuggestionProvider) {
|
|
||||||
val newProviders = this.providers.value.toMutableList()
|
|
||||||
newProviders.removeAll(providers)
|
|
||||||
this.providers.value = newProviders
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setOnEditSuggestionListener(listener: (String) -> Unit) {
|
|
||||||
onEditSuggestionListener = listener
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setOnStopListener(listener: () -> Unit) {
|
|
||||||
onStopListener = listener
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,124 +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.library.history.toolbar
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import mozilla.components.browser.toolbar.BrowserToolbar
|
|
||||||
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
|
||||||
import mozilla.components.support.ktx.android.content.res.resolveAttribute
|
|
||||||
import mozilla.components.support.ktx.android.view.hideKeyboard
|
|
||||||
import org.mozilla.fenix.R
|
|
||||||
import org.mozilla.fenix.library.history.HistorySearchFragmentState
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 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.
|
|
||||||
*/
|
|
||||||
@Suppress("LongParameterList")
|
|
||||||
class ToolbarView(
|
|
||||||
private val context: Context,
|
|
||||||
private val interactor: ToolbarInteractor,
|
|
||||||
private val isPrivate: Boolean,
|
|
||||||
val view: BrowserToolbar,
|
|
||||||
) {
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal var isInitialized = false
|
|
||||||
|
|
||||||
init {
|
|
||||||
view.apply {
|
|
||||||
editMode()
|
|
||||||
|
|
||||||
background = AppCompatResources.getDrawable(
|
|
||||||
context,
|
|
||||||
context.theme.resolveAttribute(R.attr.layer1),
|
|
||||||
)
|
|
||||||
|
|
||||||
edit.hint = context.getString(R.string.history_search_1)
|
|
||||||
|
|
||||||
edit.colors = edit.colors.copy(
|
|
||||||
text = context.getColorFromAttr(R.attr.textPrimary),
|
|
||||||
hint = context.getColorFromAttr(R.attr.textSecondary),
|
|
||||||
suggestionBackground = ContextCompat.getColor(
|
|
||||||
context,
|
|
||||||
R.color.suggestion_highlight_color,
|
|
||||||
),
|
|
||||||
clear = context.getColorFromAttr(R.attr.textPrimary),
|
|
||||||
)
|
|
||||||
|
|
||||||
edit.setUrlBackground(
|
|
||||||
AppCompatResources.getDrawable(context, R.drawable.search_url_background),
|
|
||||||
)
|
|
||||||
|
|
||||||
private = isPrivate
|
|
||||||
|
|
||||||
setOnUrlCommitListener {
|
|
||||||
hideKeyboard()
|
|
||||||
|
|
||||||
// We need to return false to not show display mode
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
setDefaultIcon()
|
|
||||||
|
|
||||||
setOnEditListener(
|
|
||||||
object : mozilla.components.concept.toolbar.Toolbar.OnEditListener {
|
|
||||||
override fun onCancelEditing(): Boolean {
|
|
||||||
interactor.onEditingCanceled()
|
|
||||||
// We need to return false to not show display mode
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(text: String) {
|
|
||||||
url = text
|
|
||||||
interactor.onTextChanged(text)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun update(state: HistorySearchFragmentState) {
|
|
||||||
if (!isInitialized) {
|
|
||||||
view.url = state.query
|
|
||||||
view.setSearchTerms(state.query)
|
|
||||||
|
|
||||||
// We must trigger an onTextChanged so when search terms are set when transitioning to `editMode`
|
|
||||||
// we have the most up to date text
|
|
||||||
interactor.onTextChanged(view.url.toString())
|
|
||||||
|
|
||||||
view.editMode()
|
|
||||||
isInitialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setDefaultIcon() {
|
|
||||||
val historySearchIcon = AppCompatResources.getDrawable(context, R.drawable.ic_history)
|
|
||||||
|
|
||||||
historySearchIcon?.let {
|
|
||||||
view.edit.setIcon(historySearchIcon, context.getString(R.string.history_search_1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,111 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!-- 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/. -->
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:id="@+id/search_wrapper"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
android:background="?attr/scrimBackground">
|
|
||||||
|
|
||||||
<mozilla.components.browser.toolbar.BrowserToolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="@dimen/browser_toolbar_height"
|
|
||||||
android:background="@drawable/toolbar_background_top"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
app:layout_scrollFlags="scroll|enterAlways|snap|exitUntilCollapsed"
|
|
||||||
app:browserToolbarClearColor="?attr/textPrimary"
|
|
||||||
app:browserToolbarInsecureColor="?attr/textPrimary"
|
|
||||||
app:browserToolbarMenuColor="?attr/textPrimary"
|
|
||||||
app:browserToolbarProgressBarGravity="bottom"
|
|
||||||
app:browserToolbarSecureColor="?attr/textPrimary"
|
|
||||||
app:browserToolbarTrackingProtectionAndSecurityIndicatorSeparatorColor="?borderPrimary"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"/>
|
|
||||||
|
|
||||||
<ViewStub
|
|
||||||
android:id="@+id/search_suggestions_hint"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:inflatedId="@id/search_suggestions_hint"
|
|
||||||
android:layout="@layout/search_suggestions_hint"
|
|
||||||
app:layout_constrainedHeight="true"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/search_hint_bottom_barrier"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/search_suggestions_hint_divider"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="?borderPrimary"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/search_suggestions_hint"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
|
||||||
android:id="@+id/search_hint_bottom_barrier"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:barrierDirection="top"
|
|
||||||
app:constraint_referenced_ids="awesome_bar,pill_wrapper"/>
|
|
||||||
|
|
||||||
<org.mozilla.fenix.library.history.awesomebar.AwesomeBarWrapper
|
|
||||||
android:id="@+id/awesome_bar"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:fadingEdge="horizontal"
|
|
||||||
android:fadingEdgeLength="40dp"
|
|
||||||
android:nestedScrollingEnabled="false"
|
|
||||||
android:requiresFadingEdge="vertical"
|
|
||||||
android:background="?attr/layer1"
|
|
||||||
android:visibility="invisible"
|
|
||||||
app:layout_constraintBottom_toTopOf="@+id/pill_wrapper"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/search_suggestions_hint" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/link_icon"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/search_fragment_clipboard_item_horizontal_margin"
|
|
||||||
android:clickable="false"
|
|
||||||
android:focusable="false"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:srcCompat="@drawable/ic_link"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/pill_wrapper_divider"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="1dp"
|
|
||||||
android:background="?borderPrimary"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/pill_wrapper" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/pill_wrapper"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:background="?attr/layer1"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:paddingStart="@dimen/search_fragment_pill_padding_start"
|
|
||||||
android:paddingTop="@dimen/search_fragment_pill_padding_vertical"
|
|
||||||
android:paddingEnd="@dimen/search_fragment_pill_padding_end"
|
|
||||||
android:paddingBottom="@dimen/search_fragment_pill_padding_vertical"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,103 +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.library.history
|
|
||||||
|
|
||||||
import io.mockk.MockKAnnotations
|
|
||||||
import io.mockk.impl.annotations.MockK
|
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import mozilla.components.concept.engine.EngineSession
|
|
||||||
import mozilla.components.support.test.robolectric.testContext
|
|
||||||
import mozilla.telemetry.glean.testing.GleanTestRule
|
|
||||||
import org.junit.Assert.assertNotNull
|
|
||||||
import org.junit.Assert.assertNull
|
|
||||||
import org.junit.Assert.assertTrue
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
import org.mozilla.fenix.BrowserDirection
|
|
||||||
import org.mozilla.fenix.GleanMetrics.History
|
|
||||||
import org.mozilla.fenix.HomeActivity
|
|
||||||
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
|
||||||
|
|
||||||
@RunWith(FenixRobolectricTestRunner::class)
|
|
||||||
class HistorySearchControllerTest {
|
|
||||||
|
|
||||||
@get:Rule
|
|
||||||
val gleanTestRule = GleanTestRule(testContext)
|
|
||||||
|
|
||||||
@MockK(relaxed = true)
|
|
||||||
private lateinit var activity: HomeActivity
|
|
||||||
|
|
||||||
@MockK(relaxed = true)
|
|
||||||
private lateinit var store: HistorySearchFragmentStore
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setUp() {
|
|
||||||
MockKAnnotations.init(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN editing is cancelled THEN clearToolbarFocus is called`() = runTest {
|
|
||||||
var clearToolbarFocusInvoked = false
|
|
||||||
createController(
|
|
||||||
clearToolbarFocus = {
|
|
||||||
clearToolbarFocusInvoked = true
|
|
||||||
},
|
|
||||||
).handleEditingCancelled()
|
|
||||||
|
|
||||||
assertTrue(clearToolbarFocusInvoked)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN text changed THEN update query action is dispatched`() {
|
|
||||||
val text = "fenix"
|
|
||||||
|
|
||||||
createController().handleTextChanged(text)
|
|
||||||
|
|
||||||
verify { store.dispatch(HistorySearchFragmentAction.UpdateQuery(text)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN text is changed to empty THEN update query action is dispatched`() {
|
|
||||||
val text = ""
|
|
||||||
|
|
||||||
createController().handleTextChanged(text)
|
|
||||||
|
|
||||||
verify { store.dispatch(HistorySearchFragmentAction.UpdateQuery(text)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `WHEN url is tapped THEN openToBrowserAndLoad is called`() {
|
|
||||||
val url = "https://www.google.com/"
|
|
||||||
val flags = EngineSession.LoadUrlFlags.none()
|
|
||||||
assertNull(History.searchResultTapped.testGetValue())
|
|
||||||
|
|
||||||
createController().handleUrlTapped(url, flags)
|
|
||||||
createController().handleUrlTapped(url)
|
|
||||||
|
|
||||||
assertNotNull(History.searchResultTapped.testGetValue())
|
|
||||||
assertNull(History.searchResultTapped.testGetValue()!!.last().extra)
|
|
||||||
verify {
|
|
||||||
activity.openToBrowserAndLoad(
|
|
||||||
searchTermOrURL = url,
|
|
||||||
newTab = true,
|
|
||||||
from = BrowserDirection.FromHistorySearchDialog,
|
|
||||||
flags = flags,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createController(
|
|
||||||
clearToolbarFocus: () -> Unit = { },
|
|
||||||
): HistorySearchDialogController {
|
|
||||||
return HistorySearchDialogController(
|
|
||||||
activity = activity,
|
|
||||||
fragmentStore = store,
|
|
||||||
clearToolbarFocus = clearToolbarFocus,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,50 +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.library.history
|
|
||||||
|
|
||||||
import io.mockk.mockk
|
|
||||||
import io.mockk.verify
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Before
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class HistorySearchDialogInteractorTest {
|
|
||||||
|
|
||||||
lateinit var searchController: HistorySearchDialogController
|
|
||||||
lateinit var interactor: HistorySearchDialogInteractor
|
|
||||||
|
|
||||||
@Before
|
|
||||||
fun setup() {
|
|
||||||
searchController = mockk(relaxed = true)
|
|
||||||
interactor = HistorySearchDialogInteractor(
|
|
||||||
searchController,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun onEditingCanceled() = runTest {
|
|
||||||
interactor.onEditingCanceled()
|
|
||||||
|
|
||||||
verify {
|
|
||||||
searchController.handleEditingCancelled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun onTextChanged() {
|
|
||||||
interactor.onTextChanged("test")
|
|
||||||
|
|
||||||
verify { searchController.handleTextChanged("test") }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun onUrlTapped() {
|
|
||||||
interactor.onUrlTapped("test")
|
|
||||||
|
|
||||||
verify {
|
|
||||||
searchController.handleUrlTapped("test")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +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.library.history
|
|
||||||
|
|
||||||
import io.mockk.impl.annotations.MockK
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Assert.assertEquals
|
|
||||||
import org.junit.Assert.assertNotSame
|
|
||||||
import org.junit.Test
|
|
||||||
import org.mozilla.fenix.components.Components
|
|
||||||
|
|
||||||
class HistorySearchFragmentStoreTest {
|
|
||||||
|
|
||||||
@MockK(relaxed = true)
|
|
||||||
private lateinit var components: Components
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `GIVEN createInitialHistorySearchFragmentState THEN query is empty`() {
|
|
||||||
val expected = HistorySearchFragmentState(query = "")
|
|
||||||
|
|
||||||
assertEquals(
|
|
||||||
expected,
|
|
||||||
createInitialHistorySearchFragmentState(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun updateQuery() = runTest {
|
|
||||||
val initialState = HistorySearchFragmentState(query = "")
|
|
||||||
val store = HistorySearchFragmentStore(initialState)
|
|
||||||
val query = "test query"
|
|
||||||
|
|
||||||
store.dispatch(HistorySearchFragmentAction.UpdateQuery(query)).join()
|
|
||||||
assertNotSame(initialState, store.state)
|
|
||||||
assertEquals(query, store.state.query)
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue