mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-03 23:15:31 +00:00
[fenix] Close https://github.com/mozilla-mobile/fenix/issues/18862: Add multi-select banner to tabs tray (https://github.com/mozilla-mobile/fenix/pull/18932)
* Issue https://github.com/mozilla-mobile/fenix/issues/18862: Add new addBookmark BookmarksUseCase * Issue https://github.com/mozilla-mobile/fenix/issues/18862: Add class for state binding features * Issue https://github.com/mozilla-mobile/fenix/issues/18862: Add delete multiple tabs to tray interactor * Issue https://github.com/mozilla-mobile/fenix/issues/18862: Add new actions to navigation interactor * Issue https://github.com/mozilla-mobile/fenix/issues/18862: Enable select mode from main tray menu * Issue https://github.com/mozilla-mobile/fenix/issues/18862: Add menu when in select mode * Close https://github.com/mozilla-mobile/fenix/issues/18862: Add multi-select banner to tabs tray * Close https://github.com/mozilla-mobile/fenix/issues/18862: Add select support for handle UI We apply various layout changes to the "handle" UI in the tabs tray when switching modes. It isn't quite clear to my, why we do this, if it's really needed to meet the end result, and if there is a better way. For now, we're simplying moving over that logic that we can re-evaluate at a later time.
This commit is contained in:
parent
298c4c2814
commit
5819a7c0a4
@ -0,0 +1,43 @@
|
||||
/* 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.annotation.CallSuper
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
import mozilla.components.lib.state.ext.flowScoped
|
||||
import mozilla.components.support.base.feature.LifecycleAwareFeature
|
||||
|
||||
/**
|
||||
* Helper class for creating small binding classes that are responsible for reacting to state
|
||||
* changes.
|
||||
*
|
||||
* Taken with ♥️ from Focus.
|
||||
*/
|
||||
abstract class AbstractBinding<in S : State>(
|
||||
private val store: Store<S, out Action>
|
||||
) : LifecycleAwareFeature {
|
||||
private var scope: CoroutineScope? = null
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@CallSuper
|
||||
override fun start() {
|
||||
scope = store.flowScoped { flow ->
|
||||
onState(flow)
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun stop() {
|
||||
scope?.cancel()
|
||||
}
|
||||
|
||||
abstract suspend fun onState(flow: Flow<S>)
|
||||
}
|
@ -70,7 +70,8 @@ class Components(private val context: Context) {
|
||||
core.sessionManager,
|
||||
core.store,
|
||||
core.webAppShortcutManager,
|
||||
core.topSitesStorage
|
||||
core.topSitesStorage,
|
||||
core.bookmarksStorage
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import android.content.Context
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.Engine
|
||||
import mozilla.components.concept.storage.BookmarksStorage
|
||||
import mozilla.components.feature.app.links.AppLinksUseCases
|
||||
import mozilla.components.feature.contextmenu.ContextMenuUseCases
|
||||
import mozilla.components.feature.downloads.DownloadsUseCases
|
||||
@ -23,6 +24,7 @@ import mozilla.components.feature.tabs.TabsUseCases
|
||||
import mozilla.components.feature.top.sites.TopSitesStorage
|
||||
import mozilla.components.feature.top.sites.TopSitesUseCases
|
||||
import mozilla.components.support.locale.LocaleUseCases
|
||||
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
|
||||
import org.mozilla.fenix.perf.lazyMonitored
|
||||
import org.mozilla.fenix.utils.Mockable
|
||||
|
||||
@ -38,7 +40,8 @@ class UseCases(
|
||||
private val sessionManager: SessionManager,
|
||||
private val store: BrowserStore,
|
||||
private val shortcutManager: WebAppShortcutManager,
|
||||
private val topSitesStorage: TopSitesStorage
|
||||
private val topSitesStorage: TopSitesStorage,
|
||||
private val bookmarksStorage: BookmarksStorage
|
||||
) {
|
||||
/**
|
||||
* Use cases that provide engine interactions for a given browser session.
|
||||
@ -94,4 +97,9 @@ class UseCases(
|
||||
* Use cases that handle locale management.
|
||||
*/
|
||||
val localeUseCases by lazyMonitored { LocaleUseCases(store) }
|
||||
|
||||
/**
|
||||
* Use cases that provide bookmark management.
|
||||
*/
|
||||
val bookmarksUseCases by lazyMonitored { BookmarksUseCase(bookmarksStorage) }
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
/* 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.bookmarks
|
||||
|
||||
import androidx.annotation.WorkerThread
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.concept.storage.BookmarksStorage
|
||||
|
||||
/**
|
||||
* Use cases that allow for modifying bookmarks.
|
||||
*/
|
||||
class BookmarksUseCase(storage: BookmarksStorage) {
|
||||
|
||||
class AddBookmarksUseCase internal constructor(private val storage: BookmarksStorage) {
|
||||
|
||||
/**
|
||||
* Adds a new bookmark with the provided [url] and [title].
|
||||
*
|
||||
* @return The result if the operation was executed or not. A bookmark may not be added if
|
||||
* one with the identical [url] already exists.
|
||||
*/
|
||||
@WorkerThread
|
||||
suspend operator fun invoke(url: String, title: String, position: Int? = null): Boolean {
|
||||
val canAdd = storage.getBookmarksWithUrl(url).firstOrNull { it.url == it.url } == null
|
||||
|
||||
if (canAdd) {
|
||||
storage.addItem(
|
||||
BookmarkRoot.Mobile.id,
|
||||
url = url,
|
||||
title = title,
|
||||
position = position
|
||||
)
|
||||
}
|
||||
|
||||
return canAdd
|
||||
}
|
||||
}
|
||||
|
||||
val addBookmark by lazy { AddBookmarksUseCase(storage) }
|
||||
}
|
@ -5,15 +5,11 @@
|
||||
package org.mozilla.fenix.tabstray
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import mozilla.components.browser.menu.BrowserMenu
|
||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.utils.Do
|
||||
|
||||
/**
|
||||
* A wrapper class that building the tabs tray menu that handles item clicks.
|
||||
@ -43,31 +39,19 @@ class MenuIntegration(
|
||||
fun build() = tabsTrayItemMenu.menuBuilder.build(context)
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun handleMenuClicked(item: TabsTrayMenu.Item) = when (item) {
|
||||
is TabsTrayMenu.Item.ShareAllTabs ->
|
||||
navigationInteractor.onShareTabsOfTypeClicked(isPrivateMode)
|
||||
is TabsTrayMenu.Item.OpenTabSettings ->
|
||||
navigationInteractor.onTabSettingsClicked()
|
||||
is TabsTrayMenu.Item.CloseAllTabs ->
|
||||
navigationInteractor.onCloseAllTabsClicked(isPrivateMode)
|
||||
is TabsTrayMenu.Item.OpenRecentlyClosed ->
|
||||
navigationInteractor.onOpenRecentlyClosedClicked()
|
||||
is TabsTrayMenu.Item.SelectTabs -> {
|
||||
/* TODO implement when mulitiselect call is available */
|
||||
internal fun handleMenuClicked(item: TabsTrayMenu.Item) {
|
||||
Do exhaustive when (item) {
|
||||
is TabsTrayMenu.Item.ShareAllTabs ->
|
||||
navigationInteractor.onShareTabsOfTypeClicked(isPrivateMode)
|
||||
is TabsTrayMenu.Item.OpenTabSettings ->
|
||||
navigationInteractor.onTabSettingsClicked()
|
||||
is TabsTrayMenu.Item.CloseAllTabs ->
|
||||
navigationInteractor.onCloseAllTabsClicked(isPrivateMode)
|
||||
is TabsTrayMenu.Item.OpenRecentlyClosed ->
|
||||
navigationInteractor.onOpenRecentlyClosedClicked()
|
||||
is TabsTrayMenu.Item.SelectTabs -> {
|
||||
tabsTrayStore.dispatch(TabsTrayAction.EnterSelectMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes [BrowserMenu.show] and applies the default theme color background.
|
||||
*/
|
||||
fun BrowserMenu.showWithTheme(view: View) {
|
||||
show(view).also { popupMenu ->
|
||||
(popupMenu.contentView as? CardView)?.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
view.context,
|
||||
R.color.foundation_normal_theme
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -5,10 +5,15 @@
|
||||
package org.mozilla.fenix.tabstray
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.engine.prompt.ShareData
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.home.HomeFragment
|
||||
@ -24,35 +29,53 @@ interface NavigationInteractor {
|
||||
fun onTabTrayDismissed()
|
||||
|
||||
/**
|
||||
* Called when user clicks the share tabs button.
|
||||
* Called when sharing a list of [Tab]s.
|
||||
*/
|
||||
fun onShareTabs(tabs: Collection<Tab>)
|
||||
|
||||
/**
|
||||
* Called when clicking the share tabs button.
|
||||
*/
|
||||
fun onShareTabsOfTypeClicked(private: Boolean)
|
||||
|
||||
/**
|
||||
* Called when user clicks the tab settings button.
|
||||
* Called when clicking the tab settings button.
|
||||
*/
|
||||
fun onTabSettingsClicked()
|
||||
|
||||
/**
|
||||
* Called when user clicks the close all tabs button.
|
||||
* Called when clicking the close all tabs button.
|
||||
*/
|
||||
fun onCloseAllTabsClicked(private: Boolean)
|
||||
|
||||
/**
|
||||
* Called when user clicks the recently closed tabs menu button.
|
||||
* Called when opening the recently closed tabs menu button.
|
||||
*/
|
||||
fun onOpenRecentlyClosedClicked()
|
||||
|
||||
/**
|
||||
* Used when opening the add-to-collections user flow.
|
||||
*/
|
||||
fun onSaveToCollections(tabs: Collection<Tab>)
|
||||
|
||||
/**
|
||||
* Used when adding [Tab]s as bookmarks.
|
||||
*/
|
||||
fun onSaveToBookmarks(tabs: Collection<Tab>)
|
||||
}
|
||||
|
||||
/**
|
||||
* A default implementation of [NavigationInteractor].
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class DefaultNavigationInteractor(
|
||||
private val tabsTrayStore: TabsTrayStore,
|
||||
private val browserStore: BrowserStore,
|
||||
private val navController: NavController,
|
||||
private val metrics: MetricController,
|
||||
private val dismissTabTray: () -> Unit,
|
||||
private val dismissTabTrayAndNavigateHome: (String) -> Unit
|
||||
private val dismissTabTrayAndNavigateHome: (String) -> Unit,
|
||||
private val bookmarksUseCase: BookmarksUseCase
|
||||
) : NavigationInteractor {
|
||||
|
||||
override fun onTabTrayDismissed() {
|
||||
@ -68,6 +91,16 @@ class DefaultNavigationInteractor(
|
||||
metrics.track(Event.RecentlyClosedTabsOpened)
|
||||
}
|
||||
|
||||
override fun onShareTabs(tabs: Collection<Tab>) {
|
||||
val data = tabs.map {
|
||||
ShareData(url = it.url, title = it.title)
|
||||
}
|
||||
val directions = TabsTrayFragmentDirections.actionGlobalShareFragment(
|
||||
data = data.toTypedArray()
|
||||
)
|
||||
navController.navigate(directions)
|
||||
}
|
||||
|
||||
override fun onShareTabsOfTypeClicked(private: Boolean) {
|
||||
val tabs = browserStore.state.getNormalOrPrivateTabs(private)
|
||||
val data = tabs.map {
|
||||
@ -89,4 +122,24 @@ class DefaultNavigationInteractor(
|
||||
|
||||
dismissTabTrayAndNavigateHome(sessionsToClose)
|
||||
}
|
||||
|
||||
override fun onSaveToCollections(tabs: Collection<Tab>) {
|
||||
metrics.track(Event.TabsTraySaveToCollectionPressed)
|
||||
|
||||
// TODO add this is a separate PR; it's quite a large change.
|
||||
}
|
||||
|
||||
override fun onSaveToBookmarks(tabs: Collection<Tab>) {
|
||||
tabs.forEach { tab ->
|
||||
// We don't combine the context with lifecycleScope so that our jobs are not cancelled
|
||||
// if we leave the fragment, i.e. we still want the bookmarks to be added.
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
bookmarksUseCase.addBookmark(tab.url, tab.title)
|
||||
}
|
||||
}
|
||||
|
||||
tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode)
|
||||
|
||||
// TODO show successful snackbar here (regardless of operation succes).
|
||||
}
|
||||
}
|
||||
|
@ -15,13 +15,18 @@ import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import kotlinx.android.synthetic.main.component_tabstray.view.*
|
||||
import kotlinx.android.synthetic.main.component_tabstray2.*
|
||||
import kotlinx.android.synthetic.main.component_tabstray2.view.*
|
||||
import kotlinx.android.synthetic.main.component_tabstray2.view.tab_tray_overflow
|
||||
import kotlinx.android.synthetic.main.component_tabstray2.view.tab_wrapper
|
||||
import kotlinx.android.synthetic.main.component_tabstray_fab.*
|
||||
import kotlinx.android.synthetic.main.tabs_tray_tab_counter2.*
|
||||
import kotlinx.android.synthetic.main.tabstray_multiselect_items.*
|
||||
import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.plus
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.NavGraphDirections
|
||||
@ -32,8 +37,13 @@ import org.mozilla.fenix.ext.requireComponents
|
||||
import org.mozilla.fenix.home.HomeScreenViewModel
|
||||
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
|
||||
import org.mozilla.fenix.tabstray.browser.DefaultBrowserTrayInteractor
|
||||
import org.mozilla.fenix.tabstray.browser.SelectionHandleBinding
|
||||
import org.mozilla.fenix.tabstray.browser.SelectionBannerBinding
|
||||
import org.mozilla.fenix.tabstray.browser.SelectionBannerBinding.VisibilityModifier
|
||||
import org.mozilla.fenix.tabstray.ext.showWithTheme
|
||||
import org.mozilla.fenix.tabstray.syncedtabs.SyncedTabsInteractor
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
||||
|
||||
private var fabView: View? = null
|
||||
@ -45,6 +55,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
||||
private val tabLayoutMediator = ViewBoundFeatureWrapper<TabLayoutMediator>()
|
||||
private val tabCounterBinding = ViewBoundFeatureWrapper<TabCounterBinding>()
|
||||
private val floatingActionButtonBinding = ViewBoundFeatureWrapper<FloatingActionButtonBinding>()
|
||||
private val selectionBannerBinding = ViewBoundFeatureWrapper<SelectionBannerBinding>()
|
||||
private val selectionHandleBinding = ViewBoundFeatureWrapper<SelectionHandleBinding>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@ -73,7 +85,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
||||
return containerView
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@Suppress("LongMethod")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
val activity = activity as HomeActivity
|
||||
@ -100,11 +112,13 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
||||
|
||||
val navigationInteractor =
|
||||
DefaultNavigationInteractor(
|
||||
tabsTrayStore = tabsTrayStore,
|
||||
browserStore = requireComponents.core.store,
|
||||
navController = findNavController(),
|
||||
metrics = requireComponents.analytics.metrics,
|
||||
dismissTabTray = ::dismissAllowingStateLoss,
|
||||
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome
|
||||
dismissTabTrayAndNavigateHome = ::dismissTabTrayAndNavigateHome,
|
||||
bookmarksUseCase = requireComponents.useCases.bookmarksUseCases
|
||||
)
|
||||
|
||||
val syncedTabsTrayInteractor = SyncedTabsInteractor(
|
||||
@ -152,6 +166,41 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
||||
owner = this,
|
||||
view = view
|
||||
)
|
||||
|
||||
selectionBannerBinding.set(
|
||||
feature = SelectionBannerBinding(
|
||||
context = requireContext(),
|
||||
store = tabsTrayStore,
|
||||
navInteractor = navigationInteractor,
|
||||
tabsTrayInteractor = this,
|
||||
containerView = view,
|
||||
backgroundView = topBar,
|
||||
showOnSelectViews = VisibilityModifier(
|
||||
collect_multi_select,
|
||||
share_multi_select,
|
||||
menu_multi_select,
|
||||
multiselect_title,
|
||||
exit_multi_select
|
||||
),
|
||||
showOnNormalViews = VisibilityModifier(
|
||||
tab_layout,
|
||||
tab_tray_overflow,
|
||||
new_tab_button
|
||||
)
|
||||
),
|
||||
owner = this,
|
||||
view = view
|
||||
)
|
||||
|
||||
selectionHandleBinding.set(
|
||||
feature = SelectionHandleBinding(
|
||||
store = tabsTrayStore,
|
||||
handle = handle,
|
||||
containerLayout = tab_wrapper
|
||||
),
|
||||
owner = this,
|
||||
view = view
|
||||
)
|
||||
}
|
||||
|
||||
override fun setCurrentTrayPosition(position: Int, smoothScroll: Boolean) {
|
||||
@ -172,7 +221,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
||||
}
|
||||
}
|
||||
|
||||
override fun tabRemoved(tabId: String) {
|
||||
override fun onDeleteTab(tabId: String) {
|
||||
// TODO re-implement these methods
|
||||
// showUndoSnackbarForTab(sessionId)
|
||||
// removeIfNotLastTab(sessionId)
|
||||
@ -181,6 +230,12 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
|
||||
requireComponents.useCases.tabsUseCases.removeTab(tabId)
|
||||
}
|
||||
|
||||
override fun onDeleteTabs(tabs: Collection<Tab>) {
|
||||
tabs.forEach {
|
||||
onDeleteTab(it.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupPager(
|
||||
context: Context,
|
||||
store: TabsTrayStore,
|
||||
|
@ -4,6 +4,8 @@
|
||||
|
||||
package org.mozilla.fenix.tabstray
|
||||
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
|
||||
interface TabsTrayInteractor {
|
||||
/**
|
||||
* Set the current tray item to the clamped [position].
|
||||
@ -21,5 +23,10 @@ interface TabsTrayInteractor {
|
||||
/**
|
||||
* Invoked when a tab is removed from the tabs tray with the given [tabId].
|
||||
*/
|
||||
fun tabRemoved(tabId: String)
|
||||
fun onDeleteTab(tabId: String)
|
||||
|
||||
/**
|
||||
* Invoked when [Tab]s need to be deleted.
|
||||
*/
|
||||
fun onDeleteTabs(tabs: Collection<Tab>)
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ abstract class BaseBrowserTrayList @JvmOverloads constructor(
|
||||
val removeTabUseCase = RemoveTabUseCaseWrapper(
|
||||
context.components.analytics.metrics
|
||||
) { sessionId ->
|
||||
interactor.tabRemoved(sessionId)
|
||||
interactor.onDeleteTab(sessionId)
|
||||
}
|
||||
|
||||
TabsFeature(
|
||||
|
@ -64,7 +64,7 @@ class DefaultBrowserTrayInteractor(
|
||||
private val removeTabWrapper by lazy {
|
||||
RemoveTabUseCaseWrapper(metrics) {
|
||||
// Handle removal from the interactor where we can also handle "undo" visuals.
|
||||
trayInteractor.tabRemoved(it)
|
||||
trayInteractor.onDeleteTab(it)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,141 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.tabstray.browser
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import kotlinx.android.synthetic.main.component_tabstray2.view.exit_multi_select
|
||||
import kotlinx.android.synthetic.main.component_tabstray2.view.multiselect_title
|
||||
import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.map
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.AbstractBinding
|
||||
import org.mozilla.fenix.tabstray.NavigationInteractor
|
||||
import org.mozilla.fenix.tabstray.TabsTrayInteractor
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.tabstray.TabsTrayAction.ExitSelectMode
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState.Mode
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState.Mode.Select
|
||||
import org.mozilla.fenix.tabstray.ext.showWithTheme
|
||||
|
||||
/**
|
||||
* A binding that shows/hides the multi-select banner of the selected count of tabs.
|
||||
*
|
||||
* @property context An Android context.
|
||||
* @property store The TabsTrayStore instance.
|
||||
* @property navInteractor An instance of [NavigationInteractor] for navigating on menu clicks.
|
||||
* @property tabsTrayInteractor An instance of [TabsTrayInteractor] for handling deletion.
|
||||
* @property containerView The view in the layout that contains all the implicit multi-select
|
||||
* views. NB: This parameter is a bit opaque and requires a larger layout refactor to correct.
|
||||
* @property backgroundView The background view that we want to alter when changing [Mode].
|
||||
* @property showOnSelectViews A variable list of views that will be made visible when in select mode.
|
||||
* @property showOnNormalViews A variable list of views that will be made visible when in normal mode.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class SelectionBannerBinding(
|
||||
private val context: Context,
|
||||
private val store: TabsTrayStore,
|
||||
private val navInteractor: NavigationInteractor,
|
||||
private val tabsTrayInteractor: TabsTrayInteractor,
|
||||
private val containerView: View,
|
||||
private val backgroundView: View,
|
||||
private val showOnSelectViews: VisibilityModifier,
|
||||
private val showOnNormalViews: VisibilityModifier
|
||||
) : AbstractBinding<TabsTrayState>(store) {
|
||||
|
||||
/**
|
||||
* A holder of views that will be used by having their [View.setVisibility] modified.
|
||||
*/
|
||||
class VisibilityModifier(vararg val views: View)
|
||||
|
||||
private var isPreviousModeSelect = false
|
||||
|
||||
override fun start() {
|
||||
super.start()
|
||||
|
||||
initListeners(containerView)
|
||||
}
|
||||
|
||||
override suspend fun onState(flow: Flow<TabsTrayState>) {
|
||||
flow.map { it.mode }
|
||||
// ignore initial mode update; we never start in select mode.
|
||||
.drop(1)
|
||||
.ifChanged()
|
||||
.collect { mode ->
|
||||
val isSelectMode = mode is Select
|
||||
|
||||
showOnSelectViews.views.forEach {
|
||||
it.isVisible = isSelectMode
|
||||
}
|
||||
|
||||
showOnNormalViews.views.forEach {
|
||||
it.isVisible = isSelectMode.not()
|
||||
}
|
||||
|
||||
updateBackgroundColor(isSelectMode)
|
||||
|
||||
updateSelectTitle(isSelectMode, mode.selectedTabs.size)
|
||||
|
||||
isPreviousModeSelect = isSelectMode
|
||||
}
|
||||
}
|
||||
|
||||
private fun initListeners(containerView: View) {
|
||||
containerView.share_multi_select.setOnClickListener {
|
||||
navInteractor.onShareTabs(store.state.mode.selectedTabs)
|
||||
}
|
||||
|
||||
containerView.collect_multi_select.setOnClickListener {
|
||||
navInteractor.onSaveToCollections(store.state.mode.selectedTabs)
|
||||
}
|
||||
|
||||
containerView.exit_multi_select.setOnClickListener {
|
||||
store.dispatch(ExitSelectMode)
|
||||
}
|
||||
|
||||
containerView.menu_multi_select.setOnClickListener { anchor ->
|
||||
val menu = SelectionMenuIntegration(
|
||||
context,
|
||||
store,
|
||||
navInteractor,
|
||||
tabsTrayInteractor
|
||||
).build()
|
||||
|
||||
menu.showWithTheme(anchor)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
private fun updateBackgroundColor(isSelectMode: Boolean) {
|
||||
// memoize to avoid setting the background unnecessarily.
|
||||
if (isPreviousModeSelect != isSelectMode) {
|
||||
val colorResource = if (isSelectMode) {
|
||||
R.color.accent_normal_theme
|
||||
} else {
|
||||
R.color.foundation_normal_theme
|
||||
}
|
||||
|
||||
val color = ContextCompat.getColor(backgroundView.context, colorResource)
|
||||
|
||||
backgroundView.setBackgroundColor(color)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
private fun updateSelectTitle(selectedMode: Boolean, tabCount: Int) {
|
||||
if (selectedMode) {
|
||||
containerView.multiselect_title.text =
|
||||
context.getString(R.string.tab_tray_multi_select_title, tabCount)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
/* 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.tabstray.browser
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.map
|
||||
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.AbstractBinding
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState
|
||||
import org.mozilla.fenix.tabstray.TabsTrayState.Mode
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
|
||||
private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F
|
||||
|
||||
/**
|
||||
* Various layout updates that need to be applied to the "handle" view when switching
|
||||
* between [Mode].
|
||||
*
|
||||
* @param store The TabsTrayStore instance.
|
||||
* @property handle The "handle" of the Tabs Tray that is used to drag the tray open/close.
|
||||
* @property containerLayout The [ConstraintLayout] that contains the "handle".
|
||||
*/
|
||||
class SelectionHandleBinding(
|
||||
store: TabsTrayStore,
|
||||
private val handle: View,
|
||||
private val containerLayout: ConstraintLayout
|
||||
) : AbstractBinding<TabsTrayState>(store) {
|
||||
|
||||
private var isPreviousModeSelect = false
|
||||
|
||||
override suspend fun onState(flow: Flow<TabsTrayState>) {
|
||||
flow.map { it.mode }
|
||||
// ignore initial mode update; we never start in select mode.
|
||||
.drop(1)
|
||||
.ifChanged()
|
||||
.collect { mode ->
|
||||
val isSelectMode = mode is Mode.Select
|
||||
|
||||
// memoize to avoid unnecessary layout updates.
|
||||
if (isPreviousModeSelect != isSelectMode) {
|
||||
updateLayoutParams(handle, isSelectMode)
|
||||
|
||||
updateBackgroundColor(handle, isSelectMode)
|
||||
|
||||
updateWidthPercent(containerLayout, handle, isSelectMode)
|
||||
}
|
||||
|
||||
isPreviousModeSelect = isSelectMode
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateLayoutParams(handle: View, multiselect: Boolean) {
|
||||
handle.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
||||
height = handle.resources.getDimensionPixelSize(
|
||||
if (multiselect) {
|
||||
R.dimen.tab_tray_multiselect_handle_height
|
||||
} else {
|
||||
R.dimen.bottom_sheet_handle_height
|
||||
}
|
||||
)
|
||||
topMargin = handle.resources.getDimensionPixelSize(
|
||||
if (multiselect) {
|
||||
R.dimen.tab_tray_multiselect_handle_top_margin
|
||||
} else {
|
||||
R.dimen.bottom_sheet_handle_top_margin
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateBackgroundColor(handle: View, multiselect: Boolean) {
|
||||
val colorResource = if (multiselect) {
|
||||
R.color.accent_normal_theme
|
||||
} else {
|
||||
R.color.secondary_text_normal_theme
|
||||
}
|
||||
|
||||
val color = ContextCompat.getColor(handle.context, colorResource)
|
||||
|
||||
handle.setBackgroundColor(color)
|
||||
}
|
||||
|
||||
private fun updateWidthPercent(
|
||||
container: ConstraintLayout,
|
||||
handle: View,
|
||||
multiselect: Boolean
|
||||
) {
|
||||
val widthPercent = if (multiselect) 1F else NORMAL_HANDLE_PERCENT_WIDTH
|
||||
container.run {
|
||||
ConstraintSet().apply {
|
||||
clone(this@run)
|
||||
constrainPercentWidth(handle.id, widthPercent)
|
||||
applyTo(this@run)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/* 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.tabstray.browser
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
class SelectionMenu(
|
||||
private val context: Context,
|
||||
private val onItemTapped: (Item) -> Unit = {}
|
||||
) {
|
||||
sealed class Item {
|
||||
object BookmarkTabs : Item()
|
||||
object DeleteTabs : Item()
|
||||
}
|
||||
|
||||
val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
|
||||
|
||||
private val menuItems by lazy {
|
||||
listOf(
|
||||
SimpleBrowserMenuItem(
|
||||
context.getString(R.string.tab_tray_multiselect_menu_item_bookmark),
|
||||
textColorResource = R.color.primary_text_normal_theme
|
||||
) {
|
||||
onItemTapped.invoke(Item.BookmarkTabs)
|
||||
},
|
||||
|
||||
SimpleBrowserMenuItem(
|
||||
context.getString(R.string.tab_tray_multiselect_menu_item_close),
|
||||
textColorResource = R.color.primary_text_normal_theme
|
||||
) {
|
||||
onItemTapped.invoke(Item.DeleteTabs)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/* 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.tabstray.browser
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import mozilla.components.browser.menu.BrowserMenuBuilder
|
||||
import org.mozilla.fenix.tabstray.NavigationInteractor
|
||||
import org.mozilla.fenix.tabstray.TabsTrayInteractor
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
import org.mozilla.fenix.utils.Do
|
||||
|
||||
class SelectionMenuIntegration(
|
||||
private val context: Context,
|
||||
private val store: TabsTrayStore,
|
||||
private val navInteractor: NavigationInteractor,
|
||||
private val trayInteractor: TabsTrayInteractor
|
||||
) {
|
||||
private val menu by lazy {
|
||||
SelectionMenu(context, ::handleMenuClicked)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the internal menu items list. See [BrowserMenuBuilder.build].
|
||||
*/
|
||||
fun build() = menu.menuBuilder.build(context)
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun handleMenuClicked(item: SelectionMenu.Item) {
|
||||
Do exhaustive when (item) {
|
||||
is SelectionMenu.Item.BookmarkTabs -> navInteractor.onSaveToBookmarks(
|
||||
store.state.mode.selectedTabs
|
||||
)
|
||||
is SelectionMenu.Item.DeleteTabs -> trayInteractor.onDeleteTabs(
|
||||
store.state.mode.selectedTabs
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
/* 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.tabstray.ext
|
||||
|
||||
import android.view.View
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.core.content.ContextCompat
|
||||
import mozilla.components.browser.menu.BrowserMenu
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
/**
|
||||
* Invokes [BrowserMenu.show] and applies the default theme color background.
|
||||
*/
|
||||
fun BrowserMenu.showWithTheme(view: View) {
|
||||
show(view).also { popupMenu ->
|
||||
(popupMenu.contentView as? CardView)?.setCardBackgroundColor(
|
||||
ContextCompat.getColor(
|
||||
view.context,
|
||||
R.color.foundation_normal_theme
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/* 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 kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import mozilla.components.browser.state.action.TabListAction
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class AbstractBindingTest {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher())
|
||||
|
||||
@Test
|
||||
fun `WHEN started THEN onState flow is invoked`() {
|
||||
val store = BrowserStore()
|
||||
var invoked = false
|
||||
val binding = TestBinding(store) {
|
||||
invoked = true
|
||||
}
|
||||
|
||||
binding.start()
|
||||
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertTrue(invoked)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN actions are dispatched THEN onState flow is invoked`() {
|
||||
val store = BrowserStore()
|
||||
var invoked = false
|
||||
val binding = TestBinding(store) {
|
||||
if (store.state.tabs.isNotEmpty()) {
|
||||
invoked = true
|
||||
}
|
||||
}
|
||||
|
||||
binding.start()
|
||||
|
||||
store.dispatch(TabListAction.AddTabAction(createTab("https://mozilla.org")))
|
||||
|
||||
store.waitUntilIdle()
|
||||
|
||||
assertTrue(invoked)
|
||||
}
|
||||
|
||||
class TestBinding(
|
||||
store: BrowserStore,
|
||||
private val invoked: (BrowserState) -> Unit
|
||||
) : AbstractBinding<BrowserState>(store) {
|
||||
override suspend fun onState(flow: Flow<BrowserState>) {
|
||||
flow.collect {
|
||||
invoked(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -24,7 +24,8 @@ class TestComponents(private val context: Context) : Components(context) {
|
||||
core.sessionManager,
|
||||
core.store,
|
||||
core.webAppShortcutManager,
|
||||
core.topSitesStorage
|
||||
core.topSitesStorage,
|
||||
core.bookmarksStorage
|
||||
)
|
||||
}
|
||||
override val intentProcessors by lazy { mockk<IntentProcessors>(relaxed = true) }
|
||||
|
@ -9,6 +9,7 @@ import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
|
||||
import mozilla.components.browser.thumbnails.storage.ThumbnailStorage
|
||||
import mozilla.components.concept.base.crash.CrashReporting
|
||||
import mozilla.components.concept.engine.Engine
|
||||
@ -32,4 +33,5 @@ class TestCore(context: Context, crashReporter: CrashReporting) : Core(
|
||||
override val webAppShortcutManager = mockk<WebAppShortcutManager>()
|
||||
override val thumbnailStorage = mockk<ThumbnailStorage>()
|
||||
override val topSitesStorage = mockk<DefaultTopSitesStorage>()
|
||||
override val bookmarksStorage = mockk<PlacesBookmarksStorage>()
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
/* 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.bookmarks
|
||||
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.runBlockingTest
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.storage.BookmarksStorage
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
class BookmarksUseCaseTest {
|
||||
|
||||
@Test
|
||||
fun `WHEN adding existing bookmark THEN no new item is stored`() = runBlockingTest {
|
||||
val storage = mockk<BookmarksStorage>()
|
||||
val bookmarkNode = mockk<BookmarkNode>()
|
||||
val useCase = BookmarksUseCase(storage)
|
||||
|
||||
every { bookmarkNode.url }.answers { "https://mozilla.org" }
|
||||
coEvery { storage.getBookmarksWithUrl(any()) }.coAnswers { listOf(bookmarkNode) }
|
||||
|
||||
val result = useCase.addBookmark("https://mozilla.org", "Mozilla")
|
||||
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN adding bookmark THEN new item is stored`() = runBlockingTest {
|
||||
val storage = mockk<BookmarksStorage>(relaxed = true)
|
||||
val useCase = BookmarksUseCase(storage)
|
||||
|
||||
coEvery { storage.getBookmarksWithUrl(any()) }.coAnswers { emptyList() }
|
||||
|
||||
val result = useCase.addBookmark("https://mozilla.org", "Mozilla")
|
||||
|
||||
assertTrue(result)
|
||||
|
||||
coVerify { storage.addItem(BookmarkRoot.Mobile.id, "https://mozilla.org", "Mozilla", null) }
|
||||
}
|
||||
}
|
@ -6,13 +6,21 @@ package org.mozilla.fenix.tabstray
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
||||
import mozilla.components.support.test.libstate.ext.waitUntilIdle
|
||||
import mozilla.components.support.test.middleware.CaptureActionsMiddleware
|
||||
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class MenuIntegrationTest {
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@get:Rule
|
||||
val coroutinesTestRule = MainCoroutineRule(TestCoroutineDispatcher())
|
||||
|
||||
private val captureMiddleware = CaptureActionsMiddleware<TabsTrayState, TabsTrayAction>()
|
||||
private val tabsTrayStore = TabsTrayStore(middlewares = listOf(captureMiddleware))
|
||||
private val interactor = mockk<NavigationInteractor>(relaxed = true)
|
||||
@ -53,12 +61,13 @@ class MenuIntegrationTest {
|
||||
verify { interactor.onOpenRecentlyClosedClicked() }
|
||||
}
|
||||
|
||||
@Ignore("Enable after we connect this menu item to the store")
|
||||
@Test
|
||||
fun `WHEN the select menu item is clicked THEN invoke the action`() {
|
||||
val menu = MenuIntegration(mockk(), mockk(), tabsTrayStore, mockk(), interactor)
|
||||
|
||||
menu.handleMenuClicked(TabsTrayMenu.Item.ShareAllTabs)
|
||||
menu.handleMenuClicked(TabsTrayMenu.Item.SelectTabs)
|
||||
|
||||
tabsTrayStore.waitUntilIdle()
|
||||
|
||||
assertNotNull(captureMiddleware.findLastAction(TabsTrayAction.EnterSelectMode::class))
|
||||
}
|
||||
|
@ -6,34 +6,45 @@ package org.mozilla.fenix.tabstray
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import mozilla.components.browser.state.state.BrowserState
|
||||
import mozilla.components.browser.state.state.TabSessionState
|
||||
import mozilla.components.browser.state.state.createTab
|
||||
import mozilla.components.browser.state.state.createTab as createStateTab
|
||||
import mozilla.components.browser.state.store.BrowserStore
|
||||
import mozilla.components.concept.tabstray.Tab
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.tabstray.browser.createTab as createTrayTab
|
||||
|
||||
class NavigationInteractorTest {
|
||||
private lateinit var store: BrowserStore
|
||||
private lateinit var tabsTrayStore: TabsTrayStore
|
||||
private lateinit var navigationInteractor: NavigationInteractor
|
||||
private val testTab: TabSessionState = createTab(url = "https://mozilla.org")
|
||||
private val testTab: TabSessionState = createStateTab(url = "https://mozilla.org")
|
||||
private val navController: NavController = mockk(relaxed = true)
|
||||
private val metrics: MetricController = mockk(relaxed = true)
|
||||
private val dismissTabTray: () -> Unit = mockk(relaxed = true)
|
||||
private val dismissTabTrayAndNavigateHome: (String) -> Unit = mockk(relaxed = true)
|
||||
private val bookmarksUseCase: BookmarksUseCase = mockk(relaxed = true)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
store = BrowserStore(initialState = BrowserState(tabs = listOf(testTab)))
|
||||
tabsTrayStore = TabsTrayStore()
|
||||
navigationInteractor = DefaultNavigationInteractor(
|
||||
tabsTrayStore,
|
||||
store,
|
||||
navController,
|
||||
metrics,
|
||||
dismissTabTray,
|
||||
dismissTabTrayAndNavigateHome
|
||||
dismissTabTrayAndNavigateHome,
|
||||
bookmarksUseCase
|
||||
)
|
||||
}
|
||||
|
||||
@ -44,6 +55,9 @@ class NavigationInteractorTest {
|
||||
var openRecentlyClosedClicked = false
|
||||
var shareTabsOfTypeClicked = false
|
||||
var closeAllTabsClicked = false
|
||||
var onShareTabs = false
|
||||
var onSaveToCollections = false
|
||||
var onBookmarkTabs = false
|
||||
|
||||
class TestNavigationInteractor : NavigationInteractor {
|
||||
|
||||
@ -51,6 +65,10 @@ class NavigationInteractorTest {
|
||||
tabTrayDismissed = true
|
||||
}
|
||||
|
||||
override fun onShareTabs(tabs: Collection<Tab>) {
|
||||
onShareTabs = true
|
||||
}
|
||||
|
||||
override fun onTabSettingsClicked() {
|
||||
tabSettingsClicked = true
|
||||
}
|
||||
@ -59,6 +77,14 @@ class NavigationInteractorTest {
|
||||
openRecentlyClosedClicked = true
|
||||
}
|
||||
|
||||
override fun onSaveToCollections(tabs: Collection<Tab>) {
|
||||
onSaveToCollections = true
|
||||
}
|
||||
|
||||
override fun onSaveToBookmarks(tabs: Collection<Tab>) {
|
||||
onBookmarkTabs = true
|
||||
}
|
||||
|
||||
override fun onShareTabsOfTypeClicked(private: Boolean) {
|
||||
shareTabsOfTypeClicked = true
|
||||
}
|
||||
@ -70,15 +96,21 @@ class NavigationInteractorTest {
|
||||
|
||||
val navigationInteractor: NavigationInteractor = TestNavigationInteractor()
|
||||
navigationInteractor.onTabTrayDismissed()
|
||||
assert(tabTrayDismissed)
|
||||
assertTrue(tabTrayDismissed)
|
||||
navigationInteractor.onTabSettingsClicked()
|
||||
assert(tabSettingsClicked)
|
||||
assertTrue(tabSettingsClicked)
|
||||
navigationInteractor.onOpenRecentlyClosedClicked()
|
||||
assert(openRecentlyClosedClicked)
|
||||
assertTrue(openRecentlyClosedClicked)
|
||||
navigationInteractor.onShareTabsOfTypeClicked(true)
|
||||
assert(shareTabsOfTypeClicked)
|
||||
assertTrue(shareTabsOfTypeClicked)
|
||||
navigationInteractor.onCloseAllTabsClicked(true)
|
||||
assert(closeAllTabsClicked)
|
||||
assertTrue(closeAllTabsClicked)
|
||||
navigationInteractor.onShareTabs(emptyList())
|
||||
assertTrue(onShareTabs)
|
||||
navigationInteractor.onSaveToCollections(emptyList())
|
||||
assertTrue(onSaveToCollections)
|
||||
navigationInteractor.onSaveToBookmarks(emptyList())
|
||||
assertTrue(onBookmarkTabs)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -110,4 +142,22 @@ class NavigationInteractorTest {
|
||||
navigationInteractor.onShareTabsOfTypeClicked(false)
|
||||
verify(exactly = 1) { navController.navigate(any<NavDirections>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onShareTabs calls navigation on DefaultNavigationInteractor`() {
|
||||
navigationInteractor.onShareTabs(emptyList())
|
||||
verify(exactly = 1) { navController.navigate(any<NavDirections>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onSaveToCollections calls navigation on DefaultNavigationInteractor`() {
|
||||
navigationInteractor.onSaveToCollections(emptyList())
|
||||
verify(exactly = 1) { metrics.track(Event.TabsTraySaveToCollectionPressed) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `onBookmarkTabs calls navigation on DefaultNavigationInteractor`() {
|
||||
navigationInteractor.onSaveToBookmarks(listOf(createTrayTab()))
|
||||
coVerify(exactly = 1) { bookmarksUseCase.addBookmark(any(), any(), any()) }
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,37 @@
|
||||
/* 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.tabstray.browser
|
||||
|
||||
import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.tabstray.NavigationInteractor
|
||||
import org.mozilla.fenix.tabstray.TabsTrayInteractor
|
||||
import org.mozilla.fenix.tabstray.TabsTrayStore
|
||||
|
||||
class SelectionMenuIntegrationTest {
|
||||
|
||||
private val navInteractor = mockk<NavigationInteractor>(relaxed = true)
|
||||
private val trayInteractor = mockk<TabsTrayInteractor>(relaxed = true)
|
||||
private val store = TabsTrayStore()
|
||||
|
||||
@Test
|
||||
fun `WHEN bookmark item is clicked THEN invoke interactor`() {
|
||||
val menu = SelectionMenuIntegration(mockk(), store, navInteractor, trayInteractor)
|
||||
|
||||
menu.handleMenuClicked(SelectionMenu.Item.BookmarkTabs)
|
||||
|
||||
verify { navInteractor.onSaveToBookmarks(store.state.mode.selectedTabs) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `WHEN delete tabs item is clicked THEN invoke interactor`() {
|
||||
val menu = SelectionMenuIntegration(mockk(), store, navInteractor, trayInteractor)
|
||||
|
||||
menu.handleMenuClicked(SelectionMenu.Item.DeleteTabs)
|
||||
|
||||
verify { trayInteractor.onDeleteTabs(store.state.mode.selectedTabs) }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user