From 635c30510d600dc5135293cc4df0ab51f7e194d8 Mon Sep 17 00:00:00 2001 From: ekager Date: Tue, 16 Jun 2020 23:10:06 -0400 Subject: [PATCH] No issue: refactor tabs tray to use interactor/controller, add tests --- .../fenix/tabtray/TabTrayController.kt | 124 +++++++++++++ .../fenix/tabtray/TabTrayDialogFragment.kt | 136 ++++----------- .../tabtray/TabTrayFragmentInteractor.kt | 38 ++++ .../org/mozilla/fenix/tabtray/TabTrayView.kt | 7 - .../tabtray/DefaultTabTrayControllerTest.kt | 165 ++++++++++++++++++ .../tabtray/TabTrayFragmentInteractorTest.kt | 53 ++++++ 6 files changed, 414 insertions(+), 109 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt create mode 100644 app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt create mode 100644 app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt create mode 100644 app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt new file mode 100644 index 000000000..72718208c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -0,0 +1,124 @@ +/* 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.tabtray + +import androidx.annotation.VisibleForTesting +import androidx.navigation.NavController +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager +import mozilla.components.concept.engine.prompt.ShareData +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.collections.SaveCollectionStep +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.sessionsOfType + +/** + * [TabTrayDialogFragment] controller. + * + * Delegated by View Interactors, handles container business logic and operates changes on it. + */ +interface TabTrayController { + fun onNewTabTapped(private: Boolean) + fun onTabTrayDismissed() + fun onShareTabsClicked(private: Boolean) + fun onSaveToCollectionClicked() + fun onCloseAllTabsClicked(private: Boolean) +} + +@Suppress("TooManyFunctions") +class DefaultTabTrayController( + private val activity: HomeActivity, + private val navController: NavController, + private val dismissTabTray: () -> Unit, + private val showUndoSnackbar: (String, SessionManager.Snapshot) -> Unit, + private val registerCollectionStorageObserver: () -> Unit +) : TabTrayController { + override fun onNewTabTapped(private: Boolean) { + activity.browsingModeManager.mode = BrowsingMode.fromBoolean(private) + navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) + dismissTabTray() + } + + override fun onTabTrayDismissed() { + dismissTabTray() + } + + override fun onSaveToCollectionClicked() { + val tabs = getListOfSessions(false) + val tabIds = tabs.map { it.id }.toList().toTypedArray() + val tabCollectionStorage = activity.components.core.tabCollectionStorage + + val step = when { + // Show the SelectTabs fragment if there are multiple opened tabs to select which tabs + // you want to save to a collection. + tabs.size > 1 -> SaveCollectionStep.SelectTabs + // If there is an existing tab collection, show the SelectCollection fragment to save + // the selected tab to a collection of your choice. + tabCollectionStorage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection + // Show the NameCollection fragment to create a new collection for the selected tab. + else -> SaveCollectionStep.NameCollection + } + + if (navController.currentDestination?.id == R.id.collectionCreationFragment) return + + // Only register the observer right before moving to collection creation + registerCollectionStorageObserver() + + val directions = TabTrayDialogFragmentDirections.actionGlobalCollectionCreationFragment( + tabIds = tabIds, + saveCollectionStep = step, + selectedTabIds = tabIds + ) + navController.navigate(directions) + } + + override fun onShareTabsClicked(private: Boolean) { + val tabs = getListOfSessions(private) + val data = tabs.map { + ShareData(url = it.url, title = it.title) + } + val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment( + data = data.toTypedArray() + ) + navController.navigate(directions) + } + + override fun onCloseAllTabsClicked(private: Boolean) { + val sessionManager = activity.components.core.sessionManager + val tabs = getListOfSessions(private) + + val selectedIndex = sessionManager + .selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0 + + val snapshot = tabs + .map(sessionManager::createSessionSnapshot) + .map { + it.copy( + engineSession = null, + engineSessionState = it.engineSession?.saveState() + ) + } + .let { SessionManager.Snapshot(it, selectedIndex) } + + tabs.forEach { + sessionManager.remove(it) + } + + val snackbarMessage = if (private) { + activity.getString(R.string.snackbar_private_tabs_closed) + } else { + activity.getString(R.string.snackbar_tabs_closed) + } + + showUndoSnackbar(snackbarMessage, snapshot) + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + private fun getListOfSessions(private: Boolean): List { + return activity.components.core.sessionManager.sessionsOfType(private = private).toList() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt index fdc6a419d..151756e32 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -22,25 +22,21 @@ import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.state.BrowserState -import mozilla.components.concept.engine.prompt.ShareData -import mozilla.components.feature.tabs.tabstray.TabsFeature import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.TabsUseCases +import mozilla.components.feature.tabs.tabstray.TabsFeature import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.utils.allowUndo -import org.mozilla.fenix.components.TabCollectionStorage @SuppressWarnings("TooManyFunctions", "LargeClass") -class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor { +class TabTrayDialogFragment : AppCompatDialogFragment() { private val tabsFeature = ViewBoundFeatureWrapper() private var _tabTrayView: TabTrayView? = null private val tabTrayView: TabTrayView @@ -108,10 +104,19 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor { _tabTrayView = TabTrayView( view.tabLayout, - this, - isPrivate, - requireContext().resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE, - viewLifecycleOwner.lifecycleScope + interactor = TabTrayFragmentInteractor( + DefaultTabTrayController( + activity = (activity as HomeActivity), + navController = findNavController(), + dismissTabTray = ::dismissAllowingStateLoss, + showUndoSnackbar = ::showUndoSnackbar, + registerCollectionStorageObserver = ::registerCollectionStorageObserver + ) + ), + isPrivate = isPrivate, + startingInLandscape = requireContext().resources.configuration.orientation == + Configuration.ORIENTATION_LANDSCAPE, + lifecycleScope = viewLifecycleOwner.lifecycleScope ) { tabsFeature.get()?.filterTabs(it) } tabsFeature.set( @@ -195,96 +200,6 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor { } } - override fun onNewTabTapped(private: Boolean) { - (activity as HomeActivity).browsingModeManager.mode = BrowsingMode.fromBoolean(private) - findNavController().navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) - dismissAllowingStateLoss() - } - - override fun onTabTrayDismissed() { - dismissAllowingStateLoss() - } - - override fun onSaveToCollectionClicked() { - val tabs = getListOfSessions(false) - val tabIds = tabs.map { it.id }.toList().toTypedArray() - val tabCollectionStorage = (activity as HomeActivity).components.core.tabCollectionStorage - val navController = findNavController() - - val step = when { - // Show the SelectTabs fragment if there are multiple opened tabs to select which tabs - // you want to save to a collection. - tabs.size > 1 -> SaveCollectionStep.SelectTabs - // If there is an existing tab collection, show the SelectCollection fragment to save - // the selected tab to a collection of your choice. - tabCollectionStorage.cachedTabCollections.isNotEmpty() -> SaveCollectionStep.SelectCollection - // Show the NameCollection fragment to create a new collection for the selected tab. - else -> SaveCollectionStep.NameCollection - } - - if (navController.currentDestination?.id == R.id.collectionCreationFragment) return - - // Only register the observer right before moving to collection creation - registerCollectionStorageObserver() - - val directions = TabTrayDialogFragmentDirections.actionGlobalCollectionCreationFragment( - tabIds = tabIds, - saveCollectionStep = step, - selectedTabIds = tabIds - ) - navController.navigate(directions) - } - - override fun onShareTabsClicked(private: Boolean) { - val tabs = getListOfSessions(private) - val data = tabs.map { - ShareData(url = it.url, title = it.title) - } - val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment( - data = data.toTypedArray() - ) - findNavController().navigate(directions) - } - - override fun onCloseAllTabsClicked(private: Boolean) { - val sessionManager = requireContext().components.core.sessionManager - val tabs = getListOfSessions(private) - - val selectedIndex = sessionManager - .selectedSession?.let { sessionManager.sessions.indexOf(it) } ?: 0 - - val snapshot = tabs - .map(sessionManager::createSessionSnapshot) - .map { it.copy(engineSession = null, engineSessionState = it.engineSession?.saveState()) } - .let { SessionManager.Snapshot(it, selectedIndex) } - - tabs.forEach { - sessionManager.remove(it) - } - - val snackbarMessage = if (tabTrayView.isPrivateModeSelected) { - getString(R.string.snackbar_private_tabs_closed) - } else { - getString(R.string.snackbar_tabs_closed) - } - - viewLifecycleOwner.lifecycleScope.allowUndo( - requireView(), - snackbarMessage, - getString(R.string.snackbar_deleted_undo), - { - sessionManager.restore(snapshot) - }, - operation = { }, - elevation = ELEVATION - ) - } - - private fun getListOfSessions(private: Boolean): List { - return requireContext().components.core.sessionManager.sessionsOfType(private = private) - .toList() - } - private fun navigateHomeIfNeeded(state: BrowserState) { val shouldPop = if (tabTrayView.isPrivateModeSelected) { state.privateTabs.isEmpty() @@ -301,6 +216,21 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor { requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) } + private fun showUndoSnackbar(snackbarMessage: String, snapshot: SessionManager.Snapshot) { + view?.let { + viewLifecycleOwner.lifecycleScope.allowUndo( + it, + snackbarMessage, + getString(R.string.snackbar_deleted_undo), + { + context?.components?.core?.sessionManager?.restore(snapshot) + }, + operation = { }, + elevation = ELEVATION + ) + } + } + private fun showCollectionSnackbar() { view.let { val snackbar = FenixSnackbar @@ -328,7 +258,9 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), TabTrayInteractor { fun show(fragmentManager: FragmentManager) { // If we've killed the fragmentManager. Let's not try to show the tabs tray. - if (fragmentManager.isDestroyed) { return } + if (fragmentManager.isDestroyed) { + return + } // We want to make sure we don't accidentally show the dialog twice if // a user somehow manages to trigger `show()` twice before we present the dialog. diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt new file mode 100644 index 000000000..374292c8e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt @@ -0,0 +1,38 @@ +/* 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.tabtray + +interface TabTrayInteractor { + fun onNewTabTapped(private: Boolean) + fun onTabTrayDismissed() + fun onShareTabsClicked(private: Boolean) + fun onSaveToCollectionClicked() + fun onCloseAllTabsClicked(private: Boolean) +} + +/** + * Interactor for the tab tray fragment. + */ +class TabTrayFragmentInteractor(private val controller: TabTrayController) : TabTrayInteractor { + override fun onNewTabTapped(private: Boolean) { + controller.onNewTabTapped(private) + } + + override fun onTabTrayDismissed() { + controller.onTabTrayDismissed() + } + + override fun onShareTabsClicked(private: Boolean) { + controller.onShareTabsClicked(private) + } + + override fun onSaveToCollectionClicked() { + controller.onSaveToCollectionClicked() + } + + override fun onCloseAllTabsClicked(private: Boolean) { + controller.onCloseAllTabsClicked(private) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt index 822236f60..3ced840b3 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -32,13 +32,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings -interface TabTrayInteractor { - fun onNewTabTapped(private: Boolean) - fun onTabTrayDismissed() - fun onShareTabsClicked(private: Boolean) - fun onSaveToCollectionClicked() - fun onCloseAllTabsClicked(private: Boolean) -} /** * View that contains and configures the BrowserAwesomeBar */ diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt new file mode 100644 index 000000000..503a316eb --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt @@ -0,0 +1,165 @@ +/* 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.tabtray + +import androidx.navigation.NavController +import androidx.navigation.NavDestination +import androidx.navigation.NavDirections +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.verify +import io.mockk.verifyOrder +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager +import mozilla.components.feature.tab.collections.TabCollection +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.TabCollectionStorage +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.sessionsOfType + +class DefaultTabTrayControllerTest { + + private val activity: HomeActivity = mockk(relaxed = true) + private val navController: NavController = mockk() + private val sessionManager: SessionManager = mockk(relaxed = true) + private val dismissTabTray: (() -> Unit) = mockk(relaxed = true) + private val showUndoSnackbar: ((String, SessionManager.Snapshot) -> Unit) = + mockk(relaxed = true) + private val registerCollectionStorageObserver: (() -> Unit) = mockk(relaxed = true) + private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true) + private val tabCollection: TabCollection = mockk() + private val cachedTabCollections: List = listOf(tabCollection) + private val currentDestination: NavDestination = mockk(relaxed = true) + + private lateinit var controller: DefaultTabTrayController + + private val session = Session( + "mozilla.org", + true + ) + + private val nonPrivateSession = Session( + "mozilla.org", + false + ) + + @Before + fun setUp() { + mockkStatic("org.mozilla.fenix.ext.SessionManagerKt") + + every { activity.components.core.sessionManager } returns sessionManager + every { activity.components.core.tabCollectionStorage } returns tabCollectionStorage + every { sessionManager.sessionsOfType(private = true) } returns listOf(session).asSequence() + every { sessionManager.sessionsOfType(private = false) } returns listOf(nonPrivateSession).asSequence() + every { sessionManager.createSessionSnapshot(any()) } returns SessionManager.Snapshot.Item( + session + ) + every { sessionManager.remove(any()) } just Runs + every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollections + every { sessionManager.selectedSession } returns nonPrivateSession + every { navController.navigate(any()) } just Runs + every { navController.currentDestination } returns currentDestination + every { currentDestination.id } returns R.id.browserFragment + + controller = DefaultTabTrayController( + activity = activity, + navController = navController, + dismissTabTray = dismissTabTray, + showUndoSnackbar = showUndoSnackbar, + registerCollectionStorageObserver = registerCollectionStorageObserver + ) + } + + @Test + fun onNewTabTapped() { + controller.onNewTabTapped(private = false) + + verifyOrder { + activity.browsingModeManager.mode = BrowsingMode.fromBoolean(false) + navController.navigate( + TabTrayDialogFragmentDirections.actionGlobalHome( + focusOnAddressBar = true + ) + ) + dismissTabTray() + } + + controller.onNewTabTapped(private = true) + + verifyOrder { + activity.browsingModeManager.mode = BrowsingMode.fromBoolean(true) + navController.navigate( + TabTrayDialogFragmentDirections.actionGlobalHome( + focusOnAddressBar = true + ) + ) + dismissTabTray() + } + } + + @Test + fun onTabTrayDismissed() { + controller.onTabTrayDismissed() + + verify { + dismissTabTray() + } + } + + @Test + fun onSaveToCollectionClicked() { + val navDirectionsSlot = slot() + every { navController.navigate(capture(navDirectionsSlot)) } just Runs + + controller.onSaveToCollectionClicked() + verify { + registerCollectionStorageObserver() + navController.navigate(capture(navDirectionsSlot)) + } + + assertTrue(navDirectionsSlot.isCaptured) + assertEquals( + R.id.action_global_collectionCreationFragment, + navDirectionsSlot.captured.actionId + ) + } + + @Test + fun onShareTabsClicked() { + val navDirectionsSlot = slot() + every { navController.navigate(capture(navDirectionsSlot)) } just Runs + + controller.onShareTabsClicked(private = false) + + verify { + navController.navigate(capture(navDirectionsSlot)) + } + + assertTrue(navDirectionsSlot.isCaptured) + assertEquals(R.id.action_global_shareFragment, navDirectionsSlot.captured.actionId) + } + + @Test + fun onCloseAllTabsClicked() { + controller.onCloseAllTabsClicked(private = false) + val snackbarMessage = activity.getString(R.string.snackbar_tabs_closed) + + verify { + sessionManager.createSessionSnapshot(nonPrivateSession) + sessionManager.remove(nonPrivateSession) + showUndoSnackbar(snackbarMessage, any()) + } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt new file mode 100644 index 000000000..4ecbefe15 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt @@ -0,0 +1,53 @@ +/* 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.tabtray + +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test + +class TabTrayFragmentInteractorTest { + private val controller = mockk(relaxed = true) + private val interactor = TabTrayFragmentInteractor(controller) + + @Test + fun onNewTabTapped() { + interactor.onNewTabTapped(private = true) + verify { controller.onNewTabTapped(true) } + + interactor.onNewTabTapped(private = false) + verify { controller.onNewTabTapped(false) } + } + + @Test + fun onTabTrayDismissed() { + interactor.onTabTrayDismissed() + verify { controller.onTabTrayDismissed() } + } + + @Test + fun onShareTabsClicked() { + interactor.onShareTabsClicked(private = true) + verify { controller.onShareTabsClicked(true) } + + interactor.onShareTabsClicked(private = false) + verify { controller.onShareTabsClicked(false) } + } + + @Test + fun onSaveToCollectionClicked() { + interactor.onSaveToCollectionClicked() + verify { controller.onSaveToCollectionClicked() } + } + + @Test + fun onCloseAllTabsClicked() { + interactor.onCloseAllTabsClicked(private = false) + verify { controller.onCloseAllTabsClicked(false) } + + interactor.onCloseAllTabsClicked(private = true) + verify { controller.onCloseAllTabsClicked(true) } + } +}