1
0
Fork 0

No issue: refactor tabs tray to use interactor/controller, add tests

master
ekager 2020-06-16 23:10:06 -04:00 committed by Emily Kager
parent 0ba1d266b3
commit 635c30510d
6 changed files with 414 additions and 109 deletions

View File

@ -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<Session> {
return activity.components.core.sessionManager.sessionsOfType(private = private).toList()
}
}

View File

@ -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<TabsFeature>()
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<Session> {
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.

View File

@ -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)
}
}

View File

@ -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
*/

View File

@ -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<TabCollection> = 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<NavDirections>()) } 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<NavDirections>()
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<NavDirections>()
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())
}
}
}

View File

@ -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<TabTrayController>(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) }
}
}