For #19475 - Introduce a DefaultTabsTrayInteractor

Refactored the TabsTrayFragment to not implement the TabsTrayInteractor which
among other advantages allowed for easier testing.
upstream-sync
Mugurell 3 years ago
parent dc26272381
commit 3fb40e1f40

@ -15,11 +15,11 @@ import org.mozilla.fenix.utils.Do
* A wrapper class that building the tabs tray menu that handles item clicks.
*/
class MenuIntegration(
private val context: Context,
private val browserStore: BrowserStore,
private val tabsTrayStore: TabsTrayStore,
private val tabLayout: TabLayout,
private val navigationInteractor: NavigationInteractor
@VisibleForTesting internal val context: Context,
@VisibleForTesting internal val browserStore: BrowserStore,
@VisibleForTesting internal val tabsTrayStore: TabsTrayStore,
@VisibleForTesting internal val tabLayout: TabLayout,
@VisibleForTesting internal val navigationInteractor: NavigationInteractor
) {
private val tabsTrayItemMenu by lazy {
TabsTrayMenu(

@ -4,15 +4,22 @@
package org.mozilla.fenix.tabstray
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
interface TabsTrayController {
@ -21,14 +28,49 @@ interface TabsTrayController {
* Called to open a new tab.
*/
fun handleOpeningNewTab(isPrivate: Boolean)
/**
* Set the current tray item to the clamped [position].
*
* @param position The position on the tray to focus.
* @param smoothScroll If true, animate the scrolling from the current tab to [position].
*/
fun handleTrayScrollingToPosition(position: Int, smoothScroll: Boolean)
/**
* Navigate from TabsTray to Browser.
*/
fun handleNavigateToBrowser()
/**
* Deletes the [Tab] with the specified [tabId].
*
* @param tabId The id of the [Tab] to be removed from TabsTray.
*/
fun handleTabDeletion(tabId: String)
/**
* Deletes a list of [tabs].
*
* @param tabs List of [Tab]s (sessions) to be removed.
*/
fun handleMultipleTabsDeletion(tabs: Collection<Tab>)
}
class DefaultTabsTrayController(
private val trayStore: TabsTrayStore,
private val browserStore: BrowserStore,
private val browsingModeManager: BrowsingModeManager,
private val navController: NavController,
private val navigateToHomeAndDeleteSession: (String) -> Unit,
private val profiler: Profiler?,
private val navigationInteractor: NavigationInteractor,
private val metrics: MetricController,
private val tabsUseCases: TabsUseCases,
private val selectTabPosition: (Int, Boolean) -> Unit,
private val dismissTray: () -> Unit,
private val showUndoSnackbarForTab: (Boolean) -> Unit
) : TabsTrayController {
override fun handleOpeningNewTab(isPrivate: Boolean) {
@ -44,7 +86,67 @@ class DefaultTabsTrayController(
sendNewTabEvent(isPrivate)
}
private fun sendNewTabEvent(isPrivateModeSelected: Boolean) {
override fun handleTrayScrollingToPosition(position: Int, smoothScroll: Boolean) {
selectTabPosition(position, smoothScroll)
trayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(position)))
}
/**
* Dismisses the tabs tray and navigates to the browser.
*/
override fun handleNavigateToBrowser() {
dismissTray()
if (navController.currentDestination?.id == R.id.browserFragment) {
return
} else if (!navController.popBackStack(R.id.browserFragment, false)) {
navController.navigateBlockingForAsyncNavGraph(R.id.browserFragment)
}
}
/**
* Deletes the [Tab] with the specified [tabId].
*
* @param tabId The id of the [Tab] to be removed from TabsTray.
* This method has no effect if the tab does not exist.
*/
override fun handleTabDeletion(tabId: String) {
val tab = browserStore.state.findTab(tabId)
tab?.let {
if (browserStore.state.getNormalOrPrivateTabs(it.content.private).size != 1) {
tabsUseCases.removeTab(tabId)
showUndoSnackbarForTab(it.content.private)
} else {
dismissTabsTrayAndNavigateHome(tabId)
}
}
}
/**
* Deletes a list of [tabs] offering an undo option.
*
* @param tabs List of [Tab]s (sessions) to be removed. This method has no effect for tabs that do not exist.
*/
@ExperimentalCoroutinesApi
override fun handleMultipleTabsDeletion(tabs: Collection<Tab>) {
val isPrivate = tabs.any { it.private }
// If user closes all the tabs from selected tabs page dismiss tray and navigate home.
if (tabs.size == browserStore.state.getNormalOrPrivateTabs(isPrivate).size) {
dismissTabsTrayAndNavigateHome(
if (isPrivate) HomeFragment.ALL_PRIVATE_TABS else HomeFragment.ALL_NORMAL_TABS
)
} else {
tabs.map { it.id }.let {
tabsUseCases.removeTabs(it)
}
}
showUndoSnackbarForTab(isPrivate)
}
@VisibleForTesting
internal fun sendNewTabEvent(isPrivateModeSelected: Boolean) {
val eventToSend = if (isPrivateModeSelected) {
Event.NewPrivateTabTapped
} else {
@ -53,4 +155,10 @@ class DefaultTabsTrayController(
metrics.track(eventToSend)
}
@VisibleForTesting
internal fun dismissTabsTrayAndNavigateHome(sessionId: String) {
dismissTray()
navigateToHomeAndDeleteSession(sessionId)
}
}

@ -10,6 +10,7 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
@ -17,7 +18,7 @@ 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 com.google.android.material.tabs.TabLayout
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
@ -26,14 +27,10 @@ import kotlinx.android.synthetic.main.component_tabstray_fab.*
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.*
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 mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.concept.tabstray.Tab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
@ -44,7 +41,6 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.browser.DefaultBrowserTrayInteractor
@ -56,11 +52,12 @@ import org.mozilla.fenix.utils.allowUndo
import kotlin.math.max
@Suppress("TooManyFunctions", "LargeClass")
class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
class TabsTrayFragment : AppCompatDialogFragment() {
private var fabView: View? = null
private lateinit var tabsTrayStore: TabsTrayStore
@VisibleForTesting internal lateinit var tabsTrayStore: TabsTrayStore
private lateinit var browserTrayInteractor: BrowserTrayInteractor
private lateinit var tabsTrayInteractor: TabsTrayInteractor
private lateinit var tabsTrayController: DefaultTabsTrayController
private lateinit var behavior: BottomSheetBehavior<ConstraintLayout>
@ -123,16 +120,25 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
)
tabsTrayController = DefaultTabsTrayController(
trayStore = tabsTrayStore,
browserStore = requireComponents.core.store,
browsingModeManager = activity.browsingModeManager,
navController = findNavController(),
navigateToHomeAndDeleteSession = ::navigateToHomeAndDeleteSession,
navigationInteractor = navigationInteractor,
profiler = requireComponents.core.engine.profiler,
metrics = requireComponents.analytics.metrics,
tabsUseCases = requireComponents.useCases.tabsUseCases,
selectTabPosition = ::selectTabPosition,
dismissTray = ::dismissTabsTray,
showUndoSnackbarForTab = ::showUndoSnackbarForTab
)
tabsTrayInteractor = DefaultTabsTrayInteractor(tabsTrayController)
browserTrayInteractor = DefaultBrowserTrayInteractor(
tabsTrayStore,
this@TabsTrayFragment,
tabsTrayInteractor,
tabsTrayController,
requireComponents.useCases.tabsUseCases.selectTab,
requireComponents.settings,
@ -143,7 +149,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
setupPager(
view.context,
tabsTrayStore,
this,
tabsTrayInteractor,
browserTrayInteractor,
navigationInteractor
)
@ -183,7 +189,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
tabLayoutMediator.set(
feature = TabLayoutMediator(
tabLayout = tab_layout,
interactor = this,
interactor = tabsTrayInteractor,
browsingModeManager = activity.browsingModeManager,
tabsTrayStore = tabsTrayStore,
metrics = requireComponents.analytics.metrics
@ -227,7 +233,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
context = requireContext(),
store = tabsTrayStore,
navInteractor = navigationInteractor,
tabsTrayInteractor = this,
tabsTrayInteractor = tabsTrayInteractor,
containerView = view,
backgroundView = topBar,
showOnSelectViews = VisibilityModifier(
@ -258,60 +264,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
)
}
override fun onTrayPositionSelected(position: Int, smoothScroll: Boolean) {
tabsTray.setCurrentItem(position, smoothScroll)
tab_layout.getTabAt(position)?.select()
tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(position)))
}
override fun onBrowserTabSelected() {
dismissTabsTray()
val navController = findNavController()
if (navController.currentDestination?.id == R.id.browserFragment) {
return
}
if (!navController.popBackStack(R.id.browserFragment, false)) {
navController.navigateBlockingForAsyncNavGraph(R.id.browserFragment)
}
}
override fun onDeleteTab(tabId: String) {
val browserStore = requireComponents.core.store
val tab = browserStore.state.findTab(tabId)
tab?.let {
if (browserStore.state.getNormalOrPrivateTabs(it.content.private).size != 1) {
requireComponents.useCases.tabsUseCases.removeTab(tabId)
showUndoSnackbarForTab(it.content.private)
} else {
dismissTabsTrayAndNavigateHome(tabId)
}
}
}
@OptIn(ExperimentalCoroutinesApi::class)
override fun onDeleteTabs(tabs: Collection<Tab>) {
val browserStore = requireComponents.core.store
val isPrivate = tabs.any { it.private }
// If user closes all the tabs from selected tabs page dismiss tray and navigate home.
if (tabs.size == browserStore.state.getNormalOrPrivateTabs(isPrivate).size) {
dismissTabsTrayAndNavigateHome(
if (isPrivate) HomeFragment.ALL_PRIVATE_TABS else HomeFragment.ALL_NORMAL_TABS
)
} else {
tabs.map { it.id }.let {
requireComponents.useCases.tabsUseCases.removeTabs(it)
}
}
showUndoSnackbarForTab(isPrivate)
}
private fun showUndoSnackbarForTab(isPrivate: Boolean) {
@VisibleForTesting
internal fun showUndoSnackbarForTab(isPrivate: Boolean) {
val snackbarMessage =
when (isPrivate) {
true -> getString(R.string.snackbar_private_tab_closed)
@ -334,7 +288,8 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
)
}
private fun setupPager(
@VisibleForTesting
internal fun setupPager(
context: Context,
store: TabsTrayStore,
trayInteractor: TabsTrayInteractor,
@ -354,12 +309,13 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
}
}
private fun setupMenu(view: View, navigationInteractor: NavigationInteractor) {
@VisibleForTesting
internal fun setupMenu(view: View, navigationInteractor: NavigationInteractor) {
view.tab_tray_overflow.setOnClickListener { anchor ->
requireComponents.analytics.metrics.track(Event.TabsTrayMenuOpened)
val menu = MenuIntegration(
val menu = getTrayMenu(
context = requireContext(),
browserStore = requireComponents.core.store,
tabsTrayStore = tabsTrayStore,
@ -371,21 +327,44 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
}
}
private fun setupBackgroundDismissalListener(block: (View) -> Unit) {
@VisibleForTesting
internal fun getTrayMenu(
context: Context,
browserStore: BrowserStore,
tabsTrayStore: TabsTrayStore,
tabLayout: TabLayout,
navigationInteractor: NavigationInteractor
) = MenuIntegration(context, browserStore, tabsTrayStore, tabLayout, navigationInteractor)
@VisibleForTesting
internal fun setupBackgroundDismissalListener(block: (View) -> Unit) {
tabLayout.setOnClickListener(block)
handle.setOnClickListener(block)
}
private val homeViewModel: HomeScreenViewModel by activityViewModels()
@VisibleForTesting
internal fun dismissTabsTrayAndNavigateHome(sessionId: String) {
navigateToHomeAndDeleteSession(sessionId)
dismissTabsTray()
}
private fun dismissTabsTrayAndNavigateHome(sessionId: String) {
internal val homeViewModel: HomeScreenViewModel by activityViewModels()
@VisibleForTesting
internal fun navigateToHomeAndDeleteSession(sessionId: String) {
homeViewModel.sessionToDelete = sessionId
val directions = NavGraphDirections.actionGlobalHome()
findNavController().navigateBlockingForAsyncNavGraph(directions)
dismissTabsTray()
}
private fun dismissTabsTray() {
@VisibleForTesting
internal fun selectTabPosition(position: Int, smoothScroll: Boolean) {
tabsTray.setCurrentItem(position, smoothScroll)
tab_layout.getTabAt(position)?.select()
}
@VisibleForTesting
internal fun dismissTabsTray() {
dismissAllowingStateLoss()
requireComponents.analytics.metrics.track(Event.TabsTrayClosed)
}
@ -398,6 +377,7 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor {
private const val EXPAND_AT_GRID_SIZE = 3
// Elevation for undo toasts
private const val ELEVATION = 80f
@VisibleForTesting
internal const val ELEVATION = 80f
}
}

@ -30,3 +30,28 @@ interface TabsTrayInteractor {
*/
fun onDeleteTabs(tabs: Collection<Tab>)
}
/**
* Interactor to be called for any tabs tray user actions.
*
* @property controller [TabsTrayController] to which user actions can be delegated for actual app update.
*/
class DefaultTabsTrayInteractor(
private val controller: TabsTrayController
) : TabsTrayInteractor {
override fun onTrayPositionSelected(position: Int, smoothScroll: Boolean) {
controller.handleTrayScrollingToPosition(position, smoothScroll)
}
override fun onBrowserTabSelected() {
controller.handleNavigateToBrowser()
}
override fun onDeleteTab(tabId: String) {
controller.handleTabDeletion(tabId)
}
override fun onDeleteTabs(tabs: Collection<Tab>) {
controller.handleMultipleTabsDeletion(tabs)
}
}

@ -7,6 +7,7 @@ package org.mozilla.fenix.tabstray
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.recyclerview.widget.RecyclerView
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
@ -22,12 +23,12 @@ import org.mozilla.fenix.tabstray.viewholders.PrivateBrowserPageViewHolder
import org.mozilla.fenix.tabstray.viewholders.SyncedTabsPageViewHolder
class TrayPagerAdapter(
private val context: Context,
private val store: TabsTrayStore,
private val browserInteractor: BrowserTrayInteractor,
private val navInteractor: NavigationInteractor,
private val interactor: TabsTrayInteractor,
private val browserStore: BrowserStore
@VisibleForTesting internal val context: Context,
@VisibleForTesting internal val store: TabsTrayStore,
@VisibleForTesting internal val browserInteractor: BrowserTrayInteractor,
@VisibleForTesting internal val navInteractor: NavigationInteractor,
@VisibleForTesting internal val interactor: TabsTrayInteractor,
@VisibleForTesting internal val browserStore: BrowserStore
) : RecyclerView.Adapter<AbstractPageViewHolder>() {
private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) }

@ -0,0 +1,420 @@
/* 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
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import io.mockk.unmockkStatic
import io.mockk.verify
import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.concept.tabstray.Tab
import mozilla.components.feature.tabs.TabsUseCases
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.helpers.DisableNavGraphProviderAssertionRule
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
@RunWith(FenixRobolectricTestRunner::class)
class DefaultTabsTrayControllerTest {
@MockK(relaxed = true)
private lateinit var trayStore: TabsTrayStore
@MockK(relaxed = true)
private lateinit var browserStore: BrowserStore
@MockK(relaxed = true)
private lateinit var browsingModeManager: BrowsingModeManager
@MockK(relaxed = true)
private lateinit var navController: NavController
@MockK(relaxed = true)
private lateinit var navigateToHomeAndDeleteSession: (String) -> Unit
@MockK(relaxed = true)
private lateinit var profiler: Profiler
@MockK(relaxed = true)
private lateinit var navigationInteractor: NavigationInteractor
@MockK(relaxed = true)
private lateinit var metrics: MetricController
@MockK(relaxed = true)
private lateinit var tabsUseCases: TabsUseCases
@MockK(relaxed = true)
private lateinit var selectTabPosition: (Int, Boolean) -> Unit
@MockK(relaxed = true)
private lateinit var dismissTray: () -> Unit
@MockK(relaxed = true)
private lateinit var showUndoSnackbarForTab: (Boolean) -> Unit
private lateinit var controller: DefaultTabsTrayController
@get:Rule
val disableNavGraphProviderAssertionRule = DisableNavGraphProviderAssertionRule()
@Before
fun setup() {
MockKAnnotations.init(this)
controller = DefaultTabsTrayController(
trayStore,
browserStore,
browsingModeManager,
navController,
navigateToHomeAndDeleteSession,
profiler,
navigationInteractor,
metrics,
tabsUseCases,
selectTabPosition,
dismissTray,
showUndoSnackbarForTab
)
}
@Test
fun `GIVEN private mode WHEN handleOpeningNewTab is called THEN a profile marker is added for the operations executed`() {
profiler = spyk(profiler) {
every { getProfilerTime() } returns Double.MAX_VALUE
}
controller = DefaultTabsTrayController(
trayStore,
browserStore,
browsingModeManager,
navController,
navigateToHomeAndDeleteSession,
profiler,
navigationInteractor,
metrics,
tabsUseCases,
selectTabPosition,
dismissTray,
showUndoSnackbarForTab
)
controller.handleOpeningNewTab(true)
verifyOrder {
profiler.getProfilerTime()
navController.navigateBlockingForAsyncNavGraph(
TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
navigationInteractor.onTabTrayDismissed()
profiler.addMarker(
"DefaultTabTrayController.onNewTabTapped",
Double.MAX_VALUE
)
}
}
@Test
fun `GIVEN normal mode WHEN handleOpeningNewTab is called THEN a profile marker is added for the operations executed`() {
profiler = spyk(profiler) {
every { getProfilerTime() } returns Double.MAX_VALUE
}
controller = DefaultTabsTrayController(
trayStore,
browserStore,
browsingModeManager,
navController,
navigateToHomeAndDeleteSession,
profiler,
navigationInteractor,
metrics,
tabsUseCases,
selectTabPosition,
dismissTray,
showUndoSnackbarForTab
)
controller.handleOpeningNewTab(false)
verifyOrder {
profiler.getProfilerTime()
navController.navigateBlockingForAsyncNavGraph(
TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
navigationInteractor.onTabTrayDismissed()
profiler.addMarker(
"DefaultTabTrayController.onNewTabTapped",
Double.MAX_VALUE
)
}
}
@Test
fun `GIVEN private mode WHEN handleOpeningNewTab is called THEN Event#NewPrivateTabTapped is added to telemetry`() {
controller.handleOpeningNewTab(true)
verify { metrics.track(Event.NewPrivateTabTapped) }
}
@Test
fun `GIVEN private mode WHEN handleOpeningNewTab is called THEN Event#NewTabTapped is added to telemetry`() {
controller.handleOpeningNewTab(false)
verify { metrics.track(Event.NewTabTapped) }
}
@Test
fun `WHEN handleTrayScrollingToPosition is called with smoothScroll=true THEN it scrolls to that position with smoothScroll`() {
controller.handleTrayScrollingToPosition(3, true)
verify { selectTabPosition(3, true) }
}
@Test
fun `WHEN handleTrayScrollingToPosition is called with smoothScroll=true THEN it emits an action for the tray page of that tab position`() {
controller.handleTrayScrollingToPosition(33, true)
verify { trayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(33))) }
}
@Test
fun `WHEN handleTrayScrollingToPosition is called with smoothScroll=false THEN it scrolls to that position without smoothScroll`() {
controller.handleTrayScrollingToPosition(4, false)
verify { selectTabPosition(4, false) }
}
@Test
fun `WHEN handleTrayScrollingToPosition is called with smoothScroll=false THEN it emits an action for the tray page of that tab position`() {
controller.handleTrayScrollingToPosition(44, true)
verify { trayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(44))) }
}
@Test
fun `GIVEN already on browserFragment WHEN handleNavigateToBrowser is called THEN the tray is dismissed`() {
every { navController.currentDestination?.id } returns R.id.browserFragment
controller.handleNavigateToBrowser()
verify { dismissTray() }
verify(exactly = 0) { navController.popBackStack() }
verify(exactly = 0) { navController.popBackStack(any(), any()) }
verify(exactly = 0) { navController.navigateBlockingForAsyncNavGraph(any<Int>()) }
verify(exactly = 0) { navController.navigateBlockingForAsyncNavGraph(any<NavDirections>()) }
verify(exactly = 0) { navController.navigateBlockingForAsyncNavGraph(any(), any()) }
}
@Test
fun `GIVEN not already on browserFragment WHEN handleNavigateToBrowser is called THEN the tray is dismissed and popBackStack is executed`() {
every { navController.currentDestination?.id } returns R.id.browserFragment + 1
every { navController.popBackStack(R.id.browserFragment, false) } returns true
controller.handleNavigateToBrowser()
verify { dismissTray() }
verify { navController.popBackStack(R.id.browserFragment, false) }
verify(exactly = 0) { navController.navigateBlockingForAsyncNavGraph(any<Int>()) }
verify(exactly = 0) { navController.navigateBlockingForAsyncNavGraph(any<NavDirections>()) }
verify(exactly = 0) { navController.navigateBlockingForAsyncNavGraph(any(), any()) }
}
@Test
fun `GIVEN not already on browserFragment WHEN handleNavigateToBrowser is called and popBackStack fails THEN it navigates to browserFragment`() {
every { navController.currentDestination?.id } returns R.id.browserFragment + 1
every { navController.popBackStack(R.id.browserFragment, false) } returns false
controller.handleNavigateToBrowser()
verify { dismissTray() }
verify { navController.popBackStack(R.id.browserFragment, false) }
verify { navController.navigateBlockingForAsyncNavGraph(R.id.browserFragment) }
}
@Test
fun `GIVEN not already on browserFragment WHEN handleNavigateToBrowser is called and popBackStack succeeds THEN the method finishes`() {
every { navController.popBackStack(R.id.browserFragment, false) } returns true
controller.handleNavigateToBrowser()
verify { dismissTray() }
verify(exactly = 1) { navController.popBackStack(R.id.browserFragment, false) }
verify(exactly = 0) { navController.navigateBlockingForAsyncNavGraph(R.id.browserFragment) }
}
@Test
fun `GIVEN more tabs opened WHEN handleTabDeletion is called THEN that tab is removed and an undo snackbar is shown`() {
val tab: TabSessionState = mockk {
every { content } returns mockk()
every { content.private } returns true
}
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
every { browserStore.state.findTab(any()) } returns tab
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(tab, mockk())
controller.handleTabDeletion("22")
verify { tabsUseCases.removeTab("22") }
verify { showUndoSnackbarForTab(true) }
} finally {
unmockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
}
}
@Test
fun `GIVEN only one tab opened WHEN handleTabDeletion is called THEN that it navigates to home where the tab will be removed`() {
controller = spyk(controller)
val tab: TabSessionState = mockk {
every { content } returns mockk()
every { content.private } returns true
}
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
every { browserStore.state.findTab(any()) } returns tab
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(tab)
controller.handleTabDeletion("33")
verify { controller.dismissTabsTrayAndNavigateHome("33") }
verify(exactly = 0) { tabsUseCases.removeTab(any()) }
verify(exactly = 0) { showUndoSnackbarForTab(any()) }
} finally {
unmockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
}
}
@ExperimentalCoroutinesApi
@Test
fun `WHEN handleMultipleTabsDeletion is called to close all private tabs THEN that it navigates to home where that tabs will be removed and shows undo snackbar`() {
controller = spyk(controller)
val privateTab: Tab = mockk {
every { private } returns true
}
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(mockk(), mockk())
controller.handleMultipleTabsDeletion(listOf(privateTab, mockk()))
verify { controller.dismissTabsTrayAndNavigateHome(HomeFragment.ALL_PRIVATE_TABS) }
verify { showUndoSnackbarForTab(true) }
verify(exactly = 0) { tabsUseCases.removeTabs(any()) }
} finally {
unmockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
}
}
@ExperimentalCoroutinesApi
@Test
fun `WHEN handleMultipleTabsDeletion is called to close all normal tabs THEN that it navigates to home where that tabs will be removed and shows undo snackbar`() {
controller = spyk(controller)
val normalTab: Tab = mockk {
every { private } returns false
}
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(mockk(), mockk())
controller.handleMultipleTabsDeletion(listOf(normalTab, normalTab))
verify { controller.dismissTabsTrayAndNavigateHome(HomeFragment.ALL_NORMAL_TABS) }
verify { showUndoSnackbarForTab(false) }
verify(exactly = 0) { tabsUseCases.removeTabs(any()) }
} finally {
unmockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
}
}
@ExperimentalCoroutinesApi
@Test
fun `WHEN handleMultipleTabsDeletion is called to close some private tabs THEN that it uses tabsUseCases#removeTabs and shows an undo snackbar`() {
controller = spyk(controller)
val privateTab: Tab = mockk {
every { private } returns true
every { id } returns "42"
}
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(mockk(), mockk())
controller.handleMultipleTabsDeletion(listOf(privateTab))
verify { tabsUseCases.removeTabs(listOf("42")) }
verify { showUndoSnackbarForTab(true) }
verify(exactly = 0) { controller.dismissTabsTrayAndNavigateHome(any()) }
} finally {
unmockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
}
}
@ExperimentalCoroutinesApi
@Test
fun `WHEN handleMultipleTabsDeletion is called to close some normal tabs THEN that it uses tabsUseCases#removeTabs and shows an undo snackbar`() {
controller = spyk(controller)
val privateTab: Tab = mockk {
every { private } returns false
every { id } returns "24"
}
every { browserStore.state } returns mockk()
try {
mockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
every { browserStore.state.getNormalOrPrivateTabs(any()) } returns listOf(mockk(), mockk())
controller.handleMultipleTabsDeletion(listOf(privateTab))
verify { tabsUseCases.removeTabs(listOf("24")) }
verify { showUndoSnackbarForTab(false) }
verify(exactly = 0) { controller.dismissTabsTrayAndNavigateHome(any()) }
} finally {
unmockkStatic("mozilla.components.browser.state.selector.SelectorsKt")
}
}
@Test
fun `GIVEN private mode selected WHEN sendNewTabEvent is called THEN NewPrivateTabTapped is tracked in telemetry`() {
controller.sendNewTabEvent(true)
verify { metrics.track(Event.NewPrivateTabTapped) }
}
@Test
fun `GIVEN normal mode selected WHEN sendNewTabEvent is called THEN NewTabTapped is tracked in telemetry`() {
controller.sendNewTabEvent(false)
verify { metrics.track(Event.NewTabTapped) }
}
@Test
fun `WHEN dismissTabsTrayAndNavigateHome is called with a spefic tab id THEN tray is dismissed and navigates home is opened to delete that tab`() {
controller.dismissTabsTrayAndNavigateHome("randomId")
verify { dismissTray() }
verify { navigateToHomeAndDeleteSession("randomId") }
}
}

@ -0,0 +1,45 @@
/* 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
import io.mockk.mockk
import io.mockk.verifySequence
import org.junit.Test
import mozilla.components.concept.tabstray.Tab
class DefaultTabsTrayInteractorTest {
val controller: TabsTrayController = mockk(relaxed = true)
val trayInteractor = DefaultTabsTrayInteractor(controller)
@Test
fun `GIVEN user selecting a new tray page WHEN onTrayPositionSelected is called THEN the Interactor delegates the controller`() {
trayInteractor.onTrayPositionSelected(14, true)
verifySequence { controller.handleTrayScrollingToPosition(14, true) }
}
@Test
fun `GIVEN user selecting a new browser tab WHEN onBrowserTabSelected is called THEN the Interactor delegates the controller`() {
trayInteractor.onBrowserTabSelected()
verifySequence { controller.handleNavigateToBrowser() }
}
@Test
fun `GIVEN user deleted one browser tab page WHEN onDeleteTab is called THEN the Interactor delegates the controller`() {
trayInteractor.onDeleteTab("testTabId")
verifySequence { controller.handleTabDeletion("testTabId") }
}
@Test
fun `GIVEN user deleted multiple browser tabs WHEN onDeleteTabs is called THEN the Interactor delegates the controller`() {
val tabsToDelete = listOf<Tab>(mockk(), mockk())
trayInteractor.onDeleteTabs(tabsToDelete)
verifySequence { controller.handleMultipleTabsDeletion(tabsToDelete) }
}
}

@ -0,0 +1,348 @@
/* 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
import android.content.Context
import android.view.View
import android.widget.Button
import android.widget.ImageButton
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.viewpager2.widget.ViewPager2
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.tabs.TabLayout
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.spyk
import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.android.synthetic.main.component_tabstray2.*
import kotlinx.android.synthetic.main.component_tabstray2.view.*
import kotlinx.android.synthetic.main.component_tabstray_fab.*
import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.*
import kotlinx.coroutines.CoroutineScope
import mozilla.components.browser.menu.BrowserMenu
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor
import org.mozilla.fenix.tabstray.ext.showWithTheme
import org.mozilla.fenix.utils.allowUndo
@RunWith(FenixRobolectricTestRunner::class)
class TabsTrayFragmentTest {
private lateinit var context: Context
private lateinit var view: View
private lateinit var fragment: TabsTrayFragment
@Before
fun setup() {
context = mockk(relaxed = true)
view = mockk(relaxed = true)
fragment = spyk(TabsTrayFragment())
every { fragment.context } returns context
every { fragment.view } returns view
}
@Test
fun `WHEN showUndoSnackbarForTab is called for a private tab with new tab button visible THEN an appropriate snackbar is shown`() {
try {
mockkStatic("org.mozilla.fenix.utils.UndoKt")
mockkStatic("androidx.lifecycle.LifecycleOwnerKt")
val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
every { any<LifecycleOwner>().lifecycleScope } returns lifecycleScope
val newTabButton: ExtendedFloatingActionButton = mockk {
every { visibility } returns View.VISIBLE
}
every { fragment.new_tab_button } returns newTabButton
every { fragment.context } returns testContext // needed for getString()
every { any<CoroutineScope>().allowUndo(any(), any(), any(), any(), any(), any(), any(), any()) } just Runs
fragment.showUndoSnackbarForTab(true)
verify { lifecycleScope.allowUndo(
fragment.view!!,
testContext.getString(R.string.snackbar_private_tab_closed),
testContext.getString(R.string.snackbar_deleted_undo),
any(),
any(),
newTabButton,
TabsTrayFragment.ELEVATION,
false
) }
} finally {
unmockkStatic("org.mozilla.fenix.utils.UndoKt")
unmockkStatic("androidx.lifecycle.LifecycleOwnerKt")
}
}
@Test
fun `WHEN showUndoSnackbarForTab is called for a private tab with new tab button not visible THEN an appropriate snackbar is shown`() {
try {
mockkStatic("org.mozilla.fenix.utils.UndoKt")
mockkStatic("androidx.lifecycle.LifecycleOwnerKt")
val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
every { any<LifecycleOwner>().lifecycleScope } returns lifecycleScope
val newTabButton: ExtendedFloatingActionButton = mockk {
every { visibility } returns View.GONE
}
every { fragment.new_tab_button } returns newTabButton
every { fragment.context } returns testContext // needed for getString()
every { any<CoroutineScope>().allowUndo(any(), any(), any(), any(), any(), any(), any(), any()) } just Runs
fragment.showUndoSnackbarForTab(true)
verify { lifecycleScope.allowUndo(
fragment.view!!,
testContext.getString(R.string.snackbar_private_tab_closed),
testContext.getString(R.string.snackbar_deleted_undo),
any(),
any(),
null,
TabsTrayFragment.ELEVATION,
false
) }
} finally {
unmockkStatic("org.mozilla.fenix.utils.UndoKt")
unmockkStatic("androidx.lifecycle.LifecycleOwnerKt")
}
}
@Test
fun `WHEN showUndoSnackbarForTab is called for a normal tab with new tab button visible THEN an appropriate snackbar is shown`() {
try {
mockkStatic("org.mozilla.fenix.utils.UndoKt")
mockkStatic("androidx.lifecycle.LifecycleOwnerKt")
val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
every { any<LifecycleOwner>().lifecycleScope } returns lifecycleScope
val newTabButton: ExtendedFloatingActionButton = mockk {
every { visibility } returns View.VISIBLE
}
every { fragment.new_tab_button } returns newTabButton
every { fragment.context } returns testContext // needed for getString()
every { any<CoroutineScope>().allowUndo(any(), any(), any(), any(), any(), any(), any(), any()) } just Runs
fragment.showUndoSnackbarForTab(false)
verify { lifecycleScope.allowUndo(
fragment.view!!,
testContext.getString(R.string.snackbar_tab_closed),
testContext.getString(R.string.snackbar_deleted_undo),
any(),
any(),
newTabButton,
TabsTrayFragment.ELEVATION,
false
) }
} finally {
unmockkStatic("org.mozilla.fenix.utils.UndoKt")
unmockkStatic("androidx.lifecycle.LifecycleOwnerKt")
}
}
@Test
fun `WHEN showUndoSnackbarForTab is called for a normal tab with new tab button not visible THEN an appropriate snackbar is shown`() {
try {
mockkStatic("org.mozilla.fenix.utils.UndoKt")
mockkStatic("androidx.lifecycle.LifecycleOwnerKt")
val lifecycleScope: LifecycleCoroutineScope = mockk(relaxed = true)
every { any<LifecycleOwner>().lifecycleScope } returns lifecycleScope
val newTabButton: ExtendedFloatingActionButton = mockk {
every { visibility } returns View.GONE
}
every { fragment.new_tab_button } returns newTabButton
every { fragment.context } returns testContext // needed for getString()
every { any<CoroutineScope>().allowUndo(any(), any(), any(), any(), any(), any(), any(), any()) } just Runs
fragment.showUndoSnackbarForTab(false)
verify { lifecycleScope.allowUndo(
fragment.view!!,
testContext.getString(R.string.snackbar_tab_closed),
testContext.getString(R.string.snackbar_deleted_undo),
any(),
any(),
null,
TabsTrayFragment.ELEVATION,
false
) }
} finally {
unmockkStatic("org.mozilla.fenix.utils.UndoKt")
unmockkStatic("androidx.lifecycle.LifecycleOwnerKt")
}
}
@Test
fun `WHEN setupPager is called THEN it sets the tray adapter and disables user initiated scrolling`() {
val tray: ViewPager2 = mockk(relaxed = true)
val store: TabsTrayStore = mockk()
val trayInteractor: TabsTrayInteractor = mockk()
val browserInteractor: BrowserTrayInteractor = mockk()
val navigationInteractor: NavigationInteractor = mockk()
val browserStore: BrowserStore = mockk()
every { fragment.tabsTray } returns tray
every { context.components.core.store } returns browserStore
val adapterSlot = slot<TrayPagerAdapter>()
fragment.setupPager(
context, store, trayInteractor, browserInteractor, navigationInteractor
)
verify { tray.adapter = capture(adapterSlot) }
assertSame(context, adapterSlot.captured.context)
assertSame(store, adapterSlot.captured.store)
assertSame(trayInteractor, adapterSlot.captured.interactor)
assertSame(browserInteractor, adapterSlot.captured.browserInteractor)
assertSame(navigationInteractor, adapterSlot.captured.navInteractor)
assertSame(browserStore, adapterSlot.captured.browserStore)
verify { tray.isUserInputEnabled = false }
}
@Test
fun `WHEN setupMenu is called THEN it sets a 3 dot menu click listener to open the tray menu`() {
try {
mockkStatic("org.mozilla.fenix.tabstray.ext.BrowserMenuKt")
val navigationInteractor: NavigationInteractor = mockk()
val threeDotMenu = ImageButton(testContext)
every { view.tab_tray_overflow } returns threeDotMenu
val metrics: MetricController = mockk(relaxed = true)
every { context.components.analytics.metrics } returns metrics
every { context.components.core.store } returns mockk()
every { fragment.tabsTrayStore } returns mockk()
every { fragment.tab_layout } returns mockk<TabLayout>()
val menu: BrowserMenu = mockk {
every { showWithTheme(any()) } just Runs
}
val menuBuilder: MenuIntegration = mockk(relaxed = true) {
every { build() } returns menu
}
every { fragment.getTrayMenu(any(), any(), any(), any(), any()) } returns menuBuilder
fragment.setupMenu(view, navigationInteractor)
threeDotMenu.performClick()
verify { metrics.track(Event.TabsTrayMenuOpened) }
verify { menuBuilder.build() }
verify { menu.showWithTheme(threeDotMenu) }
} finally {
unmockkStatic("org.mozilla.fenix.tabstray.ext.BrowserMenuKt")
}
}
@Test
fun `WHEN getTrayMenu is called THEN it returns a MenuIntegration initialized with the passed in parameters`() {
val browserStore: BrowserStore = mockk()
val tabsTrayStore: TabsTrayStore = mockk()
val tabLayout: TabLayout = mockk()
val navigationInteractor: NavigationInteractor = mockk()
val result = fragment.getTrayMenu(context, browserStore, tabsTrayStore, tabLayout, navigationInteractor)
assertSame(context, result.context)
assertSame(browserStore, result.browserStore)
assertSame(tabsTrayStore, result.tabsTrayStore)
assertSame(tabLayout, result.tabLayout)
assertSame(navigationInteractor, result.navigationInteractor)
}
@Test
fun `WHEN setupBackgroundDismissalListener is called THEN it sets a click listener for tray's tabLayout and handle`() {
var clickCount = 0
val callback: (View) -> Unit = { clickCount++ }
val tabLayout = CoordinatorLayout(testContext)
val handle = Button(testContext)
every { fragment.tabLayout } returns tabLayout
every { fragment.handle } returns handle
fragment.setupBackgroundDismissalListener(callback)
tabLayout.performClick()
assertEquals(1, clickCount)
handle.performClick()
assertEquals(2, clickCount)
}
@Test
fun `WHEN dismissTabsTrayAndNavigateHome is called with a sessionId THEN it navigates to home to delete that sessions and dismisses the tray`() {
every { fragment.navigateToHomeAndDeleteSession(any()) } just Runs
every { fragment.dismissTabsTray() } just Runs
fragment.dismissTabsTrayAndNavigateHome("test")
verify { fragment.navigateToHomeAndDeleteSession("test") }
verify { fragment.dismissTabsTray() }
}
@Test
fun `WHEN navigateToHomeAndDeleteSession is called with a sessionId THEN it navigates to home and transmits there the sessionId`() {
try {
mockkStatic("androidx.fragment.app.FragmentViewModelLazyKt")
mockkStatic("androidx.navigation.fragment.FragmentKt")
mockkStatic("org.mozilla.fenix.ext.NavControllerKt")
val viewModel: HomeScreenViewModel = mockk(relaxed = true)
every { fragment.homeViewModel } returns viewModel
val navController: NavController = mockk(relaxed = true)
every { fragment.findNavController() } returns navController
fragment.navigateToHomeAndDeleteSession("test")
verify { viewModel.sessionToDelete = "test" }
verify { navController.navigateBlockingForAsyncNavGraph(NavGraphDirections.actionGlobalHome()) }
} finally {
unmockkStatic("org.mozilla.fenix.ext.NavControllerKt")
unmockkStatic("androidx.navigation.fragment.FragmentKt")
unmockkStatic("androidx.fragment.app.FragmentViewModelLazyKt")
}
}
@Test
fun `WHEN selectTabPosition is called with a position and smooth scroll indication THEN it scrolls to that tab and selects it`() {
val tabsTray: ViewPager2 = mockk(relaxed = true)
val tab: TabLayout.Tab = mockk(relaxed = true)
val tabLayout: TabLayout = mockk {
every { getTabAt(any()) } returns tab
}
every { fragment.tab_layout } returns tabLayout
every { fragment.tabsTray } returns tabsTray
fragment.selectTabPosition(2, true)
verify { tabsTray.setCurrentItem(2, true) }
verify { tab.select() }
}
@Test
fun `WHEN dismissTabsTray is called THEN it dismisses the tray and record this event`() {
every { fragment.dismissAllowingStateLoss() } just Runs
val metrics: MetricController = mockk(relaxed = true)
every { context.components.analytics.metrics } returns metrics
fragment.dismissTabsTray()
verify { fragment.dismissAllowingStateLoss() }
verify { metrics.track(Event.TabsTrayClosed) }
}
}
Loading…
Cancel
Save