Merge remote-tracking branch 'upstream/master' into fork
commit
94ce9bb335
@ -0,0 +1,31 @@
|
|||||||
|
/* 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.settings
|
||||||
|
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import mozilla.components.support.ktx.android.content.PreferencesHolder
|
||||||
|
|
||||||
|
class CounterPreference(
|
||||||
|
private val holder: PreferencesHolder,
|
||||||
|
private val key: String,
|
||||||
|
private val maxCount: Int
|
||||||
|
) {
|
||||||
|
|
||||||
|
val value get() = holder.preferences.getInt(key, 0)
|
||||||
|
|
||||||
|
fun underMaxCount() = value < maxCount
|
||||||
|
|
||||||
|
fun increment() {
|
||||||
|
holder.preferences.edit {
|
||||||
|
putInt(key, value + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property delegate for getting and an int shared preference and incrementing it.
|
||||||
|
*/
|
||||||
|
fun PreferencesHolder.counterPreference(key: String, maxCount: Int = -1) =
|
||||||
|
CounterPreference(this, key, maxCount)
|
@ -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.components.settings
|
||||||
|
|
||||||
|
import mozilla.components.support.ktx.android.content.PreferencesHolder
|
||||||
|
import mozilla.components.support.ktx.android.content.booleanPreference
|
||||||
|
import kotlin.properties.ReadWriteProperty
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
private class DummyProperty : ReadWriteProperty<PreferencesHolder, Boolean> {
|
||||||
|
override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>) = false
|
||||||
|
override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Boolean) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property delegate for getting and setting a boolean shared preference gated by a feature flag.
|
||||||
|
*/
|
||||||
|
fun featureFlagPreference(key: String, default: Boolean, featureFlag: Boolean) =
|
||||||
|
if (featureFlag) {
|
||||||
|
booleanPreference(key, default)
|
||||||
|
} else {
|
||||||
|
DummyProperty()
|
||||||
|
}
|
@ -0,0 +1,315 @@
|
|||||||
|
/* 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.toolbar
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
|
||||||
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
|
import mozilla.components.feature.session.SessionFeature
|
||||||
|
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.NavGraphDirections
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.BrowserAnimator
|
||||||
|
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
||||||
|
import org.mozilla.fenix.browser.readermode.ReaderModeController
|
||||||
|
import org.mozilla.fenix.collections.SaveCollectionStep
|
||||||
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.getRootView
|
||||||
|
import org.mozilla.fenix.ext.nav
|
||||||
|
import org.mozilla.fenix.ext.navigateSafe
|
||||||
|
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
||||||
|
import org.mozilla.fenix.utils.Do
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface that handles events from the BrowserToolbar menu, triggered by the Interactor
|
||||||
|
*/
|
||||||
|
interface BrowserToolbarMenuController {
|
||||||
|
fun handleToolbarItemInteraction(item: ToolbarMenu.Item)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LargeClass")
|
||||||
|
class DefaultBrowserToolbarMenuController(
|
||||||
|
private val activity: HomeActivity,
|
||||||
|
private val navController: NavController,
|
||||||
|
private val metrics: MetricController,
|
||||||
|
private val settings: Settings,
|
||||||
|
private val readerModeController: ReaderModeController,
|
||||||
|
private val sessionFeature: ViewBoundFeatureWrapper<SessionFeature>,
|
||||||
|
private val sessionManager: SessionManager,
|
||||||
|
private val findInPageLauncher: () -> Unit,
|
||||||
|
private val browserAnimator: BrowserAnimator,
|
||||||
|
private val swipeRefresh: SwipeRefreshLayout,
|
||||||
|
private val customTabSession: Session?,
|
||||||
|
private val openInFenixIntent: Intent,
|
||||||
|
private val bookmarkTapped: (Session) -> Unit,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val tabCollectionStorage: TabCollectionStorage
|
||||||
|
) : BrowserToolbarMenuController {
|
||||||
|
|
||||||
|
private val currentSession
|
||||||
|
get() = customTabSession ?: sessionManager.selectedSession
|
||||||
|
|
||||||
|
// We hold onto a reference of the inner scope so that we can override this with the
|
||||||
|
// TestCoroutineScope to ensure sequential execution. If we didn't have this, our tests
|
||||||
|
// would fail intermittently due to the async nature of coroutine scheduling.
|
||||||
|
@VisibleForTesting
|
||||||
|
internal var ioScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod", "LongMethod")
|
||||||
|
override fun handleToolbarItemInteraction(item: ToolbarMenu.Item) {
|
||||||
|
val sessionUseCases = activity.components.useCases.sessionUseCases
|
||||||
|
trackToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
Do exhaustive when (item) {
|
||||||
|
is ToolbarMenu.Item.Back -> {
|
||||||
|
if (item.viewHistory) {
|
||||||
|
navController.navigate(
|
||||||
|
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sessionUseCases.goBack.invoke(currentSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ToolbarMenu.Item.Forward -> {
|
||||||
|
if (item.viewHistory) {
|
||||||
|
navController.navigate(
|
||||||
|
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sessionUseCases.goForward.invoke(currentSession)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is ToolbarMenu.Item.Reload -> {
|
||||||
|
val flags = if (item.bypassCache) {
|
||||||
|
LoadUrlFlags.select(LoadUrlFlags.BYPASS_CACHE)
|
||||||
|
} else {
|
||||||
|
LoadUrlFlags.none()
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionUseCases.reload.invoke(currentSession, flags = flags)
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession)
|
||||||
|
ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
|
||||||
|
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
|
||||||
|
navController.nav(R.id.browserFragment, directions)
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.SyncedTabs -> browserAnimator.captureEngineViewAndDrawStatically {
|
||||||
|
navController.nav(
|
||||||
|
R.id.browserFragment,
|
||||||
|
BrowserFragmentDirections.actionBrowserFragmentToSyncedTabsFragment()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke(
|
||||||
|
item.isChecked,
|
||||||
|
currentSession
|
||||||
|
)
|
||||||
|
ToolbarMenu.Item.AddToTopSites -> {
|
||||||
|
scope.launch {
|
||||||
|
ioScope.launch {
|
||||||
|
currentSession?.let {
|
||||||
|
with(activity.components.useCases.topSitesUseCase) {
|
||||||
|
addPinnedSites(it.title, it.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.join()
|
||||||
|
|
||||||
|
FenixSnackbar.make(
|
||||||
|
view = swipeRefresh,
|
||||||
|
duration = Snackbar.LENGTH_SHORT,
|
||||||
|
isDisplayedWithBrowserToolbar = true
|
||||||
|
)
|
||||||
|
.setText(
|
||||||
|
swipeRefresh.context.getString(R.string.snackbar_added_to_top_sites)
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.AddToHomeScreen, ToolbarMenu.Item.InstallToHomeScreen -> {
|
||||||
|
settings.installPwaOpened = true
|
||||||
|
MainScope().launch {
|
||||||
|
with(activity.components.useCases.webAppUseCases) {
|
||||||
|
if (isInstallable()) {
|
||||||
|
addToHomescreen()
|
||||||
|
} else {
|
||||||
|
val directions =
|
||||||
|
BrowserFragmentDirections.actionBrowserFragmentToCreateShortcutFragment()
|
||||||
|
navController.navigateSafe(R.id.browserFragment, directions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.Share -> {
|
||||||
|
val directions = NavGraphDirections.actionGlobalShareFragment(
|
||||||
|
data = arrayOf(
|
||||||
|
ShareData(
|
||||||
|
url = currentSession?.url,
|
||||||
|
title = currentSession?.title
|
||||||
|
)
|
||||||
|
),
|
||||||
|
showPage = true
|
||||||
|
)
|
||||||
|
navController.navigate(directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarMenu.Item.FindInPage -> {
|
||||||
|
findInPageLauncher()
|
||||||
|
metrics.track(Event.FindInPageOpened)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarMenu.Item.AddonsManager -> browserAnimator.captureEngineViewAndDrawStatically {
|
||||||
|
navController.nav(
|
||||||
|
R.id.browserFragment,
|
||||||
|
BrowserFragmentDirections.actionGlobalAddonsManagementFragment()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.SaveToCollection -> {
|
||||||
|
metrics
|
||||||
|
.track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER))
|
||||||
|
|
||||||
|
currentSession?.let { currentSession ->
|
||||||
|
val directions =
|
||||||
|
BrowserFragmentDirections.actionGlobalCollectionCreationFragment(
|
||||||
|
tabIds = arrayOf(currentSession.id),
|
||||||
|
selectedTabIds = arrayOf(currentSession.id),
|
||||||
|
saveCollectionStep = if (tabCollectionStorage.cachedTabCollections.isEmpty()) {
|
||||||
|
SaveCollectionStep.NameCollection
|
||||||
|
} else {
|
||||||
|
SaveCollectionStep.SelectCollection
|
||||||
|
}
|
||||||
|
)
|
||||||
|
navController.nav(R.id.browserFragment, directions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.OpenInFenix -> {
|
||||||
|
// Stop the SessionFeature from updating the EngineView and let it release the session
|
||||||
|
// from the EngineView so that it can immediately be rendered by a different view once
|
||||||
|
// we switch to the actual browser.
|
||||||
|
sessionFeature.get()?.release()
|
||||||
|
|
||||||
|
// Strip the CustomTabConfig to turn this Session into a regular tab and then select it
|
||||||
|
customTabSession!!.customTabConfig = null
|
||||||
|
sessionManager.select(customTabSession)
|
||||||
|
|
||||||
|
// Switch to the actual browser which should now display our new selected session
|
||||||
|
activity.startActivity(openInFenixIntent)
|
||||||
|
|
||||||
|
// Close this activity since it is no longer displaying any session
|
||||||
|
activity.finish()
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.Quit -> {
|
||||||
|
// We need to show the snackbar while the browsing data is deleting (if "Delete
|
||||||
|
// browsing data on quit" is activated). After the deletion is over, the snackbar
|
||||||
|
// is dismissed.
|
||||||
|
val snackbar: FenixSnackbar? = activity.getRootView()?.let { v ->
|
||||||
|
FenixSnackbar.make(
|
||||||
|
view = v,
|
||||||
|
duration = Snackbar.LENGTH_LONG,
|
||||||
|
isDisplayedWithBrowserToolbar = true
|
||||||
|
)
|
||||||
|
.setText(v.context.getString(R.string.deleting_browsing_data_in_progress))
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAndQuit(activity, scope, snackbar)
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.ReaderModeAppearance -> {
|
||||||
|
readerModeController.showControls()
|
||||||
|
metrics.track(Event.ReaderModeAppearanceOpened)
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.OpenInApp -> {
|
||||||
|
settings.openInAppOpened = true
|
||||||
|
|
||||||
|
val appLinksUseCases = activity.components.useCases.appLinksUseCases
|
||||||
|
val getRedirect = appLinksUseCases.appLinkRedirect
|
||||||
|
currentSession?.let {
|
||||||
|
val redirect = getRedirect.invoke(it.url)
|
||||||
|
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
appLinksUseCases.openAppLink.invoke(redirect.appIntent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.Bookmark -> {
|
||||||
|
sessionManager.selectedSession?.let {
|
||||||
|
bookmarkTapped(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically {
|
||||||
|
navController.nav(
|
||||||
|
R.id.browserFragment,
|
||||||
|
BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ToolbarMenu.Item.History -> browserAnimator.captureEngineViewAndDrawStatically {
|
||||||
|
navController.nav(
|
||||||
|
R.id.browserFragment,
|
||||||
|
BrowserFragmentDirections.actionGlobalHistoryFragment()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically {
|
||||||
|
navController.nav(
|
||||||
|
R.id.browserFragment,
|
||||||
|
BrowserFragmentDirections.actionGlobalDownloadsFragment()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ComplexMethod")
|
||||||
|
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
|
||||||
|
val eventItem = when (item) {
|
||||||
|
is ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK
|
||||||
|
is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD
|
||||||
|
is ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD
|
||||||
|
ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP
|
||||||
|
ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS
|
||||||
|
is ToolbarMenu.Item.RequestDesktop ->
|
||||||
|
if (item.isChecked) {
|
||||||
|
Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON
|
||||||
|
} else {
|
||||||
|
Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarMenu.Item.FindInPage -> Event.BrowserMenuItemTapped.Item.FIND_IN_PAGE
|
||||||
|
ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX
|
||||||
|
ToolbarMenu.Item.Share -> Event.BrowserMenuItemTapped.Item.SHARE
|
||||||
|
ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION
|
||||||
|
ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES
|
||||||
|
ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
|
||||||
|
ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS
|
||||||
|
ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
|
||||||
|
ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT
|
||||||
|
ToolbarMenu.Item.ReaderModeAppearance ->
|
||||||
|
Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE
|
||||||
|
ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
|
||||||
|
ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK
|
||||||
|
ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
|
||||||
|
ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
|
||||||
|
ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
|
||||||
|
ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
|
||||||
|
}
|
||||||
|
|
||||||
|
metrics.track(Event.BrowserMenuItemTapped(eventItem))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
internal const val TELEMETRY_BROWSER_IDENTIFIER = "browserMenu"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,100 @@
|
|||||||
|
/* 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.toolbar
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.VisibleForTesting
|
||||||
|
import mozilla.components.browser.menu2.BrowserMenuController
|
||||||
|
import mozilla.components.concept.menu.MenuController
|
||||||
|
import mozilla.components.concept.menu.candidate.DividerMenuCandidate
|
||||||
|
import mozilla.components.concept.menu.candidate.DrawableMenuIcon
|
||||||
|
import mozilla.components.concept.menu.candidate.MenuCandidate
|
||||||
|
import mozilla.components.concept.menu.candidate.TextMenuCandidate
|
||||||
|
import mozilla.components.concept.menu.candidate.TextStyle
|
||||||
|
import mozilla.components.support.ktx.android.content.getColorFromAttr
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
|
||||||
|
class TabCounterMenu(
|
||||||
|
context: Context,
|
||||||
|
private val metrics: MetricController,
|
||||||
|
private val onItemTapped: (Item) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
sealed class Item {
|
||||||
|
object CloseTab : Item()
|
||||||
|
data class NewTab(val mode: BrowsingMode) : Item()
|
||||||
|
}
|
||||||
|
|
||||||
|
val menuController: MenuController by lazy { BrowserMenuController() }
|
||||||
|
|
||||||
|
private val newTabItem: TextMenuCandidate
|
||||||
|
private val newPrivateTabItem: TextMenuCandidate
|
||||||
|
private val closeTabItem: TextMenuCandidate
|
||||||
|
|
||||||
|
init {
|
||||||
|
val primaryTextColor = context.getColorFromAttr(R.attr.primaryText)
|
||||||
|
val textStyle = TextStyle(color = primaryTextColor)
|
||||||
|
|
||||||
|
newTabItem = TextMenuCandidate(
|
||||||
|
text = context.getString(R.string.browser_menu_new_tab),
|
||||||
|
start = DrawableMenuIcon(
|
||||||
|
context,
|
||||||
|
R.drawable.ic_new,
|
||||||
|
tint = primaryTextColor
|
||||||
|
),
|
||||||
|
textStyle = textStyle
|
||||||
|
) {
|
||||||
|
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB))
|
||||||
|
onItemTapped(Item.NewTab(BrowsingMode.Normal))
|
||||||
|
}
|
||||||
|
|
||||||
|
newPrivateTabItem = TextMenuCandidate(
|
||||||
|
text = context.getString(R.string.home_screen_shortcut_open_new_private_tab_2),
|
||||||
|
start = DrawableMenuIcon(
|
||||||
|
context,
|
||||||
|
R.drawable.ic_private_browsing,
|
||||||
|
tint = primaryTextColor
|
||||||
|
),
|
||||||
|
textStyle = textStyle
|
||||||
|
) {
|
||||||
|
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_PRIVATE_TAB))
|
||||||
|
onItemTapped(Item.NewTab(BrowsingMode.Private))
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTabItem = TextMenuCandidate(
|
||||||
|
text = context.getString(R.string.close_tab),
|
||||||
|
start = DrawableMenuIcon(
|
||||||
|
context,
|
||||||
|
R.drawable.ic_close,
|
||||||
|
tint = primaryTextColor
|
||||||
|
),
|
||||||
|
textStyle = textStyle
|
||||||
|
) {
|
||||||
|
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.CLOSE_TAB))
|
||||||
|
onItemTapped(Item.CloseTab)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
internal fun menuItems(showOnly: BrowsingMode?): List<MenuCandidate> {
|
||||||
|
return when (showOnly) {
|
||||||
|
BrowsingMode.Normal -> listOf(newTabItem)
|
||||||
|
BrowsingMode.Private -> listOf(newPrivateTabItem)
|
||||||
|
null -> listOf(
|
||||||
|
newTabItem,
|
||||||
|
newPrivateTabItem,
|
||||||
|
DividerMenuCandidate(),
|
||||||
|
closeTabItem
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateMenu(showOnly: BrowsingMode? = null) {
|
||||||
|
menuController.submitList(menuItems(showOnly))
|
||||||
|
}
|
||||||
|
}
|
@ -1,29 +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.utils
|
|
||||||
|
|
||||||
import mozilla.components.support.ktx.android.content.PreferencesHolder
|
|
||||||
import kotlin.properties.ReadWriteProperty
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
|
|
||||||
fun featureFlagPreference(
|
|
||||||
key: String,
|
|
||||||
default: Boolean,
|
|
||||||
featureFlag: Boolean
|
|
||||||
): ReadWriteProperty<PreferencesHolder, Boolean> =
|
|
||||||
FeatureFlagPreferencePreference(key, default, featureFlag)
|
|
||||||
|
|
||||||
private class FeatureFlagPreferencePreference(
|
|
||||||
private val key: String,
|
|
||||||
private val default: Boolean,
|
|
||||||
private val featureFlag: Boolean
|
|
||||||
) : ReadWriteProperty<PreferencesHolder, Boolean> {
|
|
||||||
|
|
||||||
override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>): Boolean =
|
|
||||||
featureFlag && thisRef.preferences.getBoolean(key, default)
|
|
||||||
|
|
||||||
override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Boolean) =
|
|
||||||
thisRef.preferences.edit().putBoolean(key, value).apply()
|
|
||||||
}
|
|
@ -0,0 +1,63 @@
|
|||||||
|
/* 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.settings
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.support.ktx.android.content.PreferencesHolder
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class CounterPreferenceTest {
|
||||||
|
|
||||||
|
@MockK private lateinit var prefs: SharedPreferences
|
||||||
|
@MockK private lateinit var editor: SharedPreferences.Editor
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
every { prefs.getInt("key", 0) } returns 0
|
||||||
|
every { prefs.edit() } returns editor
|
||||||
|
every { editor.putInt("key", any()) } returns editor
|
||||||
|
every { editor.apply() } just Runs
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `update value after increment`() {
|
||||||
|
val holder = CounterHolder()
|
||||||
|
|
||||||
|
assertEquals(0, holder.property.value)
|
||||||
|
holder.property.increment()
|
||||||
|
|
||||||
|
verify { editor.putInt("key", 1) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check if value is under max count`() {
|
||||||
|
val holder = CounterHolder(maxCount = 2)
|
||||||
|
|
||||||
|
every { prefs.getInt("key", 0) } returns 0
|
||||||
|
assertEquals(0, holder.property.value)
|
||||||
|
assertTrue(holder.property.underMaxCount())
|
||||||
|
|
||||||
|
every { prefs.getInt("key", 0) } returns 2
|
||||||
|
assertEquals(2, holder.property.value)
|
||||||
|
assertFalse(holder.property.underMaxCount())
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class CounterHolder(maxCount: Int = -1) : PreferencesHolder {
|
||||||
|
override val preferences = prefs
|
||||||
|
|
||||||
|
val property = counterPreference("key", maxCount)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
/* 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.settings
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import io.mockk.Called
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.verify
|
||||||
|
import mozilla.components.support.ktx.android.content.PreferencesHolder
|
||||||
|
import org.junit.Assert.assertFalse
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class FeatureFlagPreferenceTest {
|
||||||
|
|
||||||
|
@MockK private lateinit var prefs: SharedPreferences
|
||||||
|
@MockK private lateinit var editor: SharedPreferences.Editor
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
every { prefs.getBoolean("key", false) } returns true
|
||||||
|
every { prefs.edit() } returns editor
|
||||||
|
every { editor.putBoolean("key", any()) } returns editor
|
||||||
|
every { editor.apply() } just Runs
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `acts like boolean preference if feature flag is true`() {
|
||||||
|
val holder = FeatureFlagHolder(featureFlag = true)
|
||||||
|
|
||||||
|
assertTrue(holder.property)
|
||||||
|
verify { prefs.getBoolean("key", false) }
|
||||||
|
|
||||||
|
holder.property = false
|
||||||
|
verify { editor.putBoolean("key", false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `no-op if feature flag is false`() {
|
||||||
|
val holder = FeatureFlagHolder(featureFlag = false)
|
||||||
|
|
||||||
|
assertFalse(holder.property)
|
||||||
|
holder.property = true
|
||||||
|
holder.property = false
|
||||||
|
|
||||||
|
verify { prefs wasNot Called }
|
||||||
|
verify { editor wasNot Called }
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class FeatureFlagHolder(featureFlag: Boolean) : PreferencesHolder {
|
||||||
|
override val preferences = prefs
|
||||||
|
|
||||||
|
var property by featureFlagPreference(
|
||||||
|
"key",
|
||||||
|
default = false,
|
||||||
|
featureFlag = featureFlag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,494 @@
|
|||||||
|
/* 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.toolbar
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
import io.mockk.MockKAnnotations
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.impl.annotations.MockK
|
||||||
|
import io.mockk.impl.annotations.RelaxedMockK
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkObject
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.mockk.unmockkObject
|
||||||
|
import io.mockk.unmockkStatic
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
|
import mozilla.components.browser.session.Session
|
||||||
|
import mozilla.components.browser.session.SessionManager
|
||||||
|
import mozilla.components.concept.engine.EngineSession
|
||||||
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
|
import mozilla.components.feature.search.SearchUseCases
|
||||||
|
import mozilla.components.feature.session.SessionFeature
|
||||||
|
import mozilla.components.feature.session.SessionUseCases
|
||||||
|
import mozilla.components.feature.tab.collections.TabCollection
|
||||||
|
import mozilla.components.feature.top.sites.TopSitesUseCases
|
||||||
|
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||||
|
import mozilla.components.support.test.rule.MainCoroutineRule
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.HomeActivity
|
||||||
|
import org.mozilla.fenix.NavGraphDirections
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.BrowserAnimator
|
||||||
|
import org.mozilla.fenix.browser.BrowserFragmentDirections
|
||||||
|
import org.mozilla.fenix.browser.readermode.ReaderModeController
|
||||||
|
import org.mozilla.fenix.collections.SaveCollectionStep
|
||||||
|
import org.mozilla.fenix.components.FenixSnackbar
|
||||||
|
import org.mozilla.fenix.components.TabCollectionStorage
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
|
import org.mozilla.fenix.ext.directionsEq
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
|
||||||
|
import org.mozilla.fenix.utils.Settings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class DefaultBrowserToolbarMenuControllerTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val coroutinesTestRule = MainCoroutineRule()
|
||||||
|
|
||||||
|
@MockK private lateinit var swipeRefreshLayout: SwipeRefreshLayout
|
||||||
|
@RelaxedMockK private lateinit var activity: HomeActivity
|
||||||
|
@RelaxedMockK private lateinit var navController: NavController
|
||||||
|
@RelaxedMockK private lateinit var findInPageLauncher: () -> Unit
|
||||||
|
@RelaxedMockK private lateinit var bookmarkTapped: (Session) -> Unit
|
||||||
|
@RelaxedMockK private lateinit var sessionManager: SessionManager
|
||||||
|
@RelaxedMockK private lateinit var currentSession: Session
|
||||||
|
@RelaxedMockK private lateinit var openInFenixIntent: Intent
|
||||||
|
@RelaxedMockK private lateinit var metrics: MetricController
|
||||||
|
@RelaxedMockK private lateinit var settings: Settings
|
||||||
|
@RelaxedMockK private lateinit var searchUseCases: SearchUseCases
|
||||||
|
@RelaxedMockK private lateinit var sessionUseCases: SessionUseCases
|
||||||
|
@RelaxedMockK private lateinit var browserAnimator: BrowserAnimator
|
||||||
|
@RelaxedMockK private lateinit var snackbar: FenixSnackbar
|
||||||
|
@RelaxedMockK private lateinit var tabCollectionStorage: TabCollectionStorage
|
||||||
|
@RelaxedMockK private lateinit var topSitesUseCase: TopSitesUseCases
|
||||||
|
@RelaxedMockK private lateinit var readerModeController: ReaderModeController
|
||||||
|
@MockK private lateinit var sessionFeatureWrapper: ViewBoundFeatureWrapper<SessionFeature>
|
||||||
|
@RelaxedMockK private lateinit var sessionFeature: SessionFeature
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockKAnnotations.init(this)
|
||||||
|
|
||||||
|
mockkStatic(
|
||||||
|
"org.mozilla.fenix.settings.deletebrowsingdata.DeleteAndQuitKt"
|
||||||
|
)
|
||||||
|
every { deleteAndQuit(any(), any(), any()) } just Runs
|
||||||
|
|
||||||
|
mockkObject(FenixSnackbar.Companion)
|
||||||
|
every { FenixSnackbar.make(any(), any(), any(), any()) } returns snackbar
|
||||||
|
|
||||||
|
every { activity.components.useCases.sessionUseCases } returns sessionUseCases
|
||||||
|
every { activity.components.useCases.searchUseCases } returns searchUseCases
|
||||||
|
every { activity.components.useCases.topSitesUseCase } returns topSitesUseCase
|
||||||
|
every { sessionManager.selectedSession } returns currentSession
|
||||||
|
every { sessionFeatureWrapper.get() } returns sessionFeature
|
||||||
|
every { navController.currentDestination } returns mockk {
|
||||||
|
every { id } returns R.id.browserFragment
|
||||||
|
}
|
||||||
|
every { currentSession.id } returns "1"
|
||||||
|
|
||||||
|
val onComplete = slot<() -> Unit>()
|
||||||
|
every { browserAnimator.captureEngineViewAndDrawStatically(capture(onComplete)) } answers { onComplete.captured.invoke() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkStatic("org.mozilla.fenix.settings.deletebrowsingdata.DeleteAndQuitKt")
|
||||||
|
unmockkObject(FenixSnackbar.Companion)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarBackPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Back(false)
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BACK)) }
|
||||||
|
verify { sessionUseCases.goBack(currentSession) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarBackLongPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Back(true)
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
val directions = BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment()
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BACK)) }
|
||||||
|
verify { navController.navigate(directions) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarForwardPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Forward(false)
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.FORWARD)) }
|
||||||
|
verify { sessionUseCases.goForward(currentSession) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarForwardLongPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Forward(true)
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
val directions = BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment()
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.FORWARD)) }
|
||||||
|
verify { navController.navigate(directions) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarReloadPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Reload(false)
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.RELOAD)) }
|
||||||
|
verify { sessionUseCases.reload(currentSession) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarReloadLongPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Reload(true)
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.RELOAD)) }
|
||||||
|
verify {
|
||||||
|
sessionUseCases.reload(
|
||||||
|
currentSession,
|
||||||
|
EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.BYPASS_CACHE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarStopPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Stop
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.STOP)) }
|
||||||
|
verify { sessionUseCases.stopLoading(currentSession) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarSettingsPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Settings
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SETTINGS)) }
|
||||||
|
verify { navController.navigate(directions, null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarBookmarkPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Bookmark
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BOOKMARK)) }
|
||||||
|
verify { bookmarkTapped(currentSession) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarBookmarksPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Bookmarks
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
val directions = BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BOOKMARKS)) }
|
||||||
|
verify { navController.navigate(directions, null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarHistoryPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.History
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
val directions = BrowserFragmentDirections.actionGlobalHistoryFragment()
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.HISTORY)) }
|
||||||
|
verify { navController.navigate(directions, null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarRequestDesktopOnPress() = runBlockingTest {
|
||||||
|
val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase =
|
||||||
|
mockk(relaxed = true)
|
||||||
|
val item = ToolbarMenu.Item.RequestDesktop(true)
|
||||||
|
|
||||||
|
every { sessionUseCases.requestDesktopSite } returns requestDesktopSiteUseCase
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON)) }
|
||||||
|
verify {
|
||||||
|
requestDesktopSiteUseCase.invoke(
|
||||||
|
true,
|
||||||
|
currentSession
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarRequestDesktopOffPress() = runBlockingTest {
|
||||||
|
val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase =
|
||||||
|
mockk(relaxed = true)
|
||||||
|
val item = ToolbarMenu.Item.RequestDesktop(false)
|
||||||
|
|
||||||
|
every { sessionUseCases.requestDesktopSite } returns requestDesktopSiteUseCase
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF)) }
|
||||||
|
verify {
|
||||||
|
requestDesktopSiteUseCase.invoke(
|
||||||
|
false,
|
||||||
|
currentSession
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarAddToTopSitesPressed() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.AddToTopSites
|
||||||
|
val addPinnedSiteUseCase: TopSitesUseCases.AddPinnedSiteUseCase = mockk(relaxed = true)
|
||||||
|
|
||||||
|
every { topSitesUseCase.addPinnedSites } returns addPinnedSiteUseCase
|
||||||
|
every {
|
||||||
|
swipeRefreshLayout.context.getString(R.string.snackbar_added_to_top_sites)
|
||||||
|
} returns "Added to top sites!"
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { addPinnedSiteUseCase.invoke(currentSession.title, currentSession.url) }
|
||||||
|
verify { snackbar.setText("Added to top sites!") }
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarAddonsManagerPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.AddonsManager
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarAddToHomeScreenPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.AddToHomeScreen
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarSharePress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Share
|
||||||
|
|
||||||
|
every { currentSession.url } returns "https://mozilla.org"
|
||||||
|
every { currentSession.title } returns "Mozilla"
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SHARE)) }
|
||||||
|
verify {
|
||||||
|
navController.navigate(
|
||||||
|
directionsEq(NavGraphDirections.actionGlobalShareFragment(
|
||||||
|
data = arrayOf(ShareData(url = "https://mozilla.org", title = "Mozilla")),
|
||||||
|
showPage = true
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarFindInPagePress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.FindInPage
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { findInPageLauncher() }
|
||||||
|
verify { metrics.track(Event.FindInPageOpened) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarSaveToCollectionPressWhenAtLeastOneCollectionExists() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.SaveToCollection
|
||||||
|
val cachedTabCollections: List<TabCollection> = mockk(relaxed = true)
|
||||||
|
every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollections
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
metrics.track(
|
||||||
|
Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
verify {
|
||||||
|
metrics.track(
|
||||||
|
Event.CollectionSaveButtonPressed(DefaultBrowserToolbarController.TELEMETRY_BROWSER_IDENTIFIER)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val directions = BrowserFragmentDirections.actionGlobalCollectionCreationFragment(
|
||||||
|
saveCollectionStep = SaveCollectionStep.SelectCollection,
|
||||||
|
tabIds = arrayOf(currentSession.id),
|
||||||
|
selectedTabIds = arrayOf(currentSession.id)
|
||||||
|
)
|
||||||
|
verify { navController.navigate(directionsEq(directions), null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarSaveToCollectionPressWhenNoCollectionsExists() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.SaveToCollection
|
||||||
|
val cachedTabCollectionsEmpty: List<TabCollection> = emptyList()
|
||||||
|
every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollectionsEmpty
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION)) }
|
||||||
|
verify {
|
||||||
|
metrics.track(
|
||||||
|
Event.CollectionSaveButtonPressed(
|
||||||
|
DefaultBrowserToolbarController.TELEMETRY_BROWSER_IDENTIFIER
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val directions = BrowserFragmentDirections.actionGlobalCollectionCreationFragment(
|
||||||
|
saveCollectionStep = SaveCollectionStep.NameCollection,
|
||||||
|
tabIds = arrayOf(currentSession.id),
|
||||||
|
selectedTabIds = arrayOf(currentSession.id)
|
||||||
|
)
|
||||||
|
verify { navController.navigate(directionsEq(directions), null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarOpenInFenixPress() = runBlockingTest {
|
||||||
|
val controller = createController(scope = this, customTabSession = currentSession)
|
||||||
|
|
||||||
|
val item = ToolbarMenu.Item.OpenInFenix
|
||||||
|
|
||||||
|
every { currentSession.customTabConfig } returns mockk()
|
||||||
|
every { activity.startActivity(any()) } just Runs
|
||||||
|
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { sessionFeature.release() }
|
||||||
|
verify { currentSession.customTabConfig = null }
|
||||||
|
verify { sessionManager.select(currentSession) }
|
||||||
|
verify { activity.startActivity(openInFenixIntent) }
|
||||||
|
verify { activity.finish() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarQuitPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.Quit
|
||||||
|
val testScope = this
|
||||||
|
|
||||||
|
val controller = createController(scope = testScope)
|
||||||
|
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { deleteAndQuit(activity, testScope, null) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarReaderModeAppearancePress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.ReaderModeAppearance
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { readerModeController.showControls() }
|
||||||
|
verify { metrics.track(Event.ReaderModeAppearanceOpened) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun handleToolbarOpenInAppPress() = runBlockingTest {
|
||||||
|
val item = ToolbarMenu.Item.OpenInApp
|
||||||
|
|
||||||
|
val controller = createController(scope = this)
|
||||||
|
|
||||||
|
controller.handleToolbarItemInteraction(item)
|
||||||
|
|
||||||
|
verify { settings.openInAppOpened = true }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createController(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
activity: HomeActivity = this.activity,
|
||||||
|
customTabSession: Session? = null
|
||||||
|
) = DefaultBrowserToolbarMenuController(
|
||||||
|
activity = activity,
|
||||||
|
navController = navController,
|
||||||
|
metrics = metrics,
|
||||||
|
settings = settings,
|
||||||
|
findInPageLauncher = findInPageLauncher,
|
||||||
|
browserAnimator = browserAnimator,
|
||||||
|
customTabSession = customTabSession,
|
||||||
|
openInFenixIntent = openInFenixIntent,
|
||||||
|
scope = scope,
|
||||||
|
swipeRefresh = swipeRefreshLayout,
|
||||||
|
tabCollectionStorage = tabCollectionStorage,
|
||||||
|
bookmarkTapped = bookmarkTapped,
|
||||||
|
readerModeController = readerModeController,
|
||||||
|
sessionManager = sessionManager,
|
||||||
|
sessionFeature = sessionFeatureWrapper
|
||||||
|
).apply {
|
||||||
|
ioScope = scope
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
/* 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.toolbar
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verifyAll
|
||||||
|
import mozilla.components.concept.menu.candidate.DividerMenuCandidate
|
||||||
|
import mozilla.components.concept.menu.candidate.DrawableMenuIcon
|
||||||
|
import mozilla.components.concept.menu.candidate.TextMenuCandidate
|
||||||
|
import mozilla.components.support.test.robolectric.testContext
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mozilla.fenix.R
|
||||||
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.components.metrics.MetricController
|
||||||
|
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
|
||||||
|
|
||||||
|
@RunWith(FenixRobolectricTestRunner::class)
|
||||||
|
class TabCounterMenuTest {
|
||||||
|
|
||||||
|
private lateinit var context: Context
|
||||||
|
private lateinit var metrics: MetricController
|
||||||
|
private lateinit var onItemTapped: (TabCounterMenu.Item) -> Unit
|
||||||
|
private lateinit var menu: TabCounterMenu
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setup() {
|
||||||
|
context = ContextThemeWrapper(testContext, R.style.NormalTheme)
|
||||||
|
metrics = mockk(relaxed = true)
|
||||||
|
onItemTapped = mockk(relaxed = true)
|
||||||
|
menu = TabCounterMenu(context, metrics, onItemTapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `all items use primary text color styling`() {
|
||||||
|
val items = menu.menuItems(showOnly = null)
|
||||||
|
assertEquals(4, items.size)
|
||||||
|
|
||||||
|
val textItems = items.mapNotNull { it as? TextMenuCandidate }
|
||||||
|
assertEquals(3, textItems.size)
|
||||||
|
|
||||||
|
val primaryTextColor = context.getColor(R.color.primary_text_normal_theme)
|
||||||
|
for (item in textItems) {
|
||||||
|
assertEquals(primaryTextColor, item.textStyle.color)
|
||||||
|
assertEquals(primaryTextColor, (item.start as DrawableMenuIcon).tint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `return only the new tab item`() {
|
||||||
|
val items = menu.menuItems(showOnly = BrowsingMode.Normal)
|
||||||
|
assertEquals(1, items.size)
|
||||||
|
|
||||||
|
val item = items[0] as TextMenuCandidate
|
||||||
|
assertEquals("New tab", item.text)
|
||||||
|
item.onClick()
|
||||||
|
|
||||||
|
verifyAll {
|
||||||
|
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB))
|
||||||
|
onItemTapped(TabCounterMenu.Item.NewTab(BrowsingMode.Normal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `return only the new private tab item`() {
|
||||||
|
val items = menu.menuItems(showOnly = BrowsingMode.Private)
|
||||||
|
assertEquals(1, items.size)
|
||||||
|
|
||||||
|
val item = items[0] as TextMenuCandidate
|
||||||
|
assertEquals("New private tab", item.text)
|
||||||
|
item.onClick()
|
||||||
|
|
||||||
|
verifyAll {
|
||||||
|
metrics.track(Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_PRIVATE_TAB))
|
||||||
|
onItemTapped(TabCounterMenu.Item.NewTab(BrowsingMode.Private))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `return two new tab items and a close button`() {
|
||||||
|
val (newTab, newPrivateTab, divider, closeTab) = menu.menuItems(showOnly = null)
|
||||||
|
|
||||||
|
assertEquals("New tab", (newTab as TextMenuCandidate).text)
|
||||||
|
assertEquals("New private tab", (newPrivateTab as TextMenuCandidate).text)
|
||||||
|
assertEquals("Close tab", (closeTab as TextMenuCandidate).text)
|
||||||
|
assertEquals(DividerMenuCandidate(), divider)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue