diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt b/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt index 2a16302048..15c1a54247 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt @@ -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( diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt index 0a4d00fd61..138e36ff62 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt @@ -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) } 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) { + 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) + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index 90756e972e..195877bd1a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -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 @@ -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) { - - 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 } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt index b898e67659..f8223e4b6e 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt @@ -30,3 +30,28 @@ interface TabsTrayInteractor { */ fun onDeleteTabs(tabs: Collection) } + +/** + * 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) { + controller.handleMultipleTabsDeletion(tabs) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt index 0c65dbf5c7..43d74280ed 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -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() { private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) } diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/DefaultTabsTrayControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/DefaultTabsTrayControllerTest.kt new file mode 100644 index 0000000000..7f61b5b0fe --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/DefaultTabsTrayControllerTest.kt @@ -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()) } + verify(exactly = 0) { navController.navigateBlockingForAsyncNavGraph(any()) } + 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()) } + verify(exactly = 0) { navController.navigateBlockingForAsyncNavGraph(any()) } + 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") } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/DefaultTabsTrayInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/DefaultTabsTrayInteractorTest.kt new file mode 100644 index 0000000000..55b84c759f --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/DefaultTabsTrayInteractorTest.kt @@ -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(mockk(), mockk()) + + trayInteractor.onDeleteTabs(tabsToDelete) + + verifySequence { controller.handleMultipleTabsDeletion(tabsToDelete) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt new file mode 100644 index 0000000000..1a25024695 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabstray/TabsTrayFragmentTest.kt @@ -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().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().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().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().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().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().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().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().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() + + 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() + 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) } + } +}