[fenix] Closes https://github.com/mozilla-mobile/fenix/issues/4396 - Add a Bookmarks Controller (https://github.com/mozilla-mobile/fenix/pull/4593)
* For https://github.com/mozilla-mobile/fenix/issues/4396 - Rename BookmarkInteractor methods Following the naming model used in other Interactors this too will use reactive method names in the form of "on..." instead of the previous imperative model. Kept the imperative naming model for the methods from `SelectionInteractor` as they are a new addition and I'm not sure about the future direction. * For https://github.com/mozilla-mobile/fenix/issues/4396 - Add a BookmarkController It abstracts the Fragment behavior in a contract through which various Interactors can inform about the specific View changes and can ask for modifications in their container Fragment. This contract and it's implementation - `DefaultBookmarkController` are the result of extracting the container Fragment's business logic from `BookmarkFragmentInteractor` in it's own standalone component. * For https://github.com/mozilla-mobile/fenix/issues/4396 - Refactored Bookmark related tests Added a new `BookmarkControllerTest` tests class which complements the new `BookmarkController` to ensure that it properly operates on `BookmarkFragment` Also refactored the existing `BookmarkFragmentInteractorTest` to accommodate `BookmarkFragmentInteractor`'s now more specialized behavior.pull/600/head
parent
84349bda63
commit
43e4bcc0b1
@ -0,0 +1,118 @@
|
||||
/* 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.library.bookmarks
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDirections
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.BrowsingMode
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbarPresenter
|
||||
import org.mozilla.fenix.components.Services
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.copyUrl
|
||||
import org.mozilla.fenix.ext.nav
|
||||
|
||||
/**
|
||||
* [BookmarkFragment] controller.
|
||||
* Delegated by View Interactors, handles container business logic and operates changes on it.
|
||||
*/
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
interface BookmarkController {
|
||||
fun handleBookmarkTapped(item: BookmarkNode)
|
||||
fun handleBookmarkExpand(folder: BookmarkNode)
|
||||
fun handleSelectionModeSwitch()
|
||||
fun handleBookmarkEdit(node: BookmarkNode)
|
||||
fun handleBookmarkSelected(node: BookmarkNode)
|
||||
fun handleCopyUrl(item: BookmarkNode)
|
||||
fun handleBookmarkSharing(item: BookmarkNode)
|
||||
fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode)
|
||||
fun handleBookmarkDeletion(nodes: Set<BookmarkNode>, eventType: Event)
|
||||
fun handleBackPressed()
|
||||
fun handleSigningIn()
|
||||
}
|
||||
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
class DefaultBookmarkController(
|
||||
private val context: Context,
|
||||
private val navController: NavController,
|
||||
private val snackbarPresenter: FenixSnackbarPresenter,
|
||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit
|
||||
) : BookmarkController {
|
||||
|
||||
private val activity: HomeActivity = context as HomeActivity
|
||||
private val resources: Resources = context.resources
|
||||
private val services: Services = activity.components.services
|
||||
|
||||
override fun handleBookmarkTapped(item: BookmarkNode) {
|
||||
openInNewTab(item.url!!, true, BrowserDirection.FromBookmarks, BrowsingMode.Normal)
|
||||
}
|
||||
|
||||
override fun handleBookmarkExpand(folder: BookmarkNode) {
|
||||
navigate(BookmarkFragmentDirections.actionBookmarkFragmentSelf(folder.guid))
|
||||
}
|
||||
|
||||
override fun handleSelectionModeSwitch() {
|
||||
activity.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun handleBookmarkEdit(node: BookmarkNode) {
|
||||
navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid))
|
||||
}
|
||||
|
||||
override fun handleBookmarkSelected(node: BookmarkNode) {
|
||||
snackbarPresenter.present(resources.getString(R.string.bookmark_cannot_edit_root))
|
||||
}
|
||||
|
||||
override fun handleCopyUrl(item: BookmarkNode) {
|
||||
item.copyUrl(context)
|
||||
snackbarPresenter.present(resources.getString(R.string.url_copied))
|
||||
}
|
||||
|
||||
override fun handleBookmarkSharing(item: BookmarkNode) {
|
||||
navigate(BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment(
|
||||
url = item.url!!,
|
||||
title = item.title
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode) {
|
||||
openInNewTab(item.url!!, true, BrowserDirection.FromBookmarks, mode)
|
||||
}
|
||||
|
||||
override fun handleBookmarkDeletion(nodes: Set<BookmarkNode>, eventType: Event) {
|
||||
deleteBookmarkNodes(nodes, eventType)
|
||||
}
|
||||
|
||||
override fun handleBackPressed() {
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
override fun handleSigningIn() {
|
||||
services.launchPairingSignIn(context, navController)
|
||||
}
|
||||
|
||||
private fun openInNewTab(
|
||||
searchTermOrURL: String,
|
||||
newTab: Boolean,
|
||||
from: BrowserDirection,
|
||||
mode: BrowsingMode
|
||||
) {
|
||||
with(activity) {
|
||||
browsingModeManager.mode = mode
|
||||
openToBrowserAndLoad(searchTermOrURL, newTab, from)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigate(directions: NavDirections) {
|
||||
navController.nav(R.id.bookmarkFragment, directions)
|
||||
}
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
/* 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.library.bookmarks
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyOrder
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.storage.BookmarkNodeType
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.BrowsingMode
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbarPresenter
|
||||
import org.mozilla.fenix.components.Services
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||
class BookmarkControllerTest {
|
||||
|
||||
private lateinit var controller: BookmarkController
|
||||
|
||||
private val context: Context = mockk(relaxed = true)
|
||||
private val navController: NavController = mockk(relaxed = true)
|
||||
private val snackbarPresenter: FenixSnackbarPresenter = mockk(relaxed = true)
|
||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit = mockk(relaxed = true)
|
||||
|
||||
private val homeActivity: HomeActivity = mockk(relaxed = true)
|
||||
private val services: Services = mockk(relaxed = true)
|
||||
|
||||
private val item = BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null)
|
||||
private val subfolder = BookmarkNode(BookmarkNodeType.FOLDER, "987", "123", 0, "Subfolder", null, listOf())
|
||||
private val childItem = BookmarkNode(
|
||||
BookmarkNodeType.ITEM, "987", "123", 2, "Firefox", "https://www.mozilla.org/en-US/firefox/", null
|
||||
)
|
||||
private val tree = BookmarkNode(
|
||||
BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf(item, item, childItem, subfolder)
|
||||
)
|
||||
private val root = BookmarkNode(
|
||||
BookmarkNodeType.FOLDER, BookmarkRoot.Root.id, null, 0, BookmarkRoot.Root.name, null, null
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
// needed for mocking 'getSystemService<ClipboardManager>()'
|
||||
mockkStatic(
|
||||
"androidx.core.content.ContextCompat",
|
||||
"android.content.ClipData"
|
||||
)
|
||||
|
||||
every { homeActivity.components.services } returns services
|
||||
every { navController.currentDestination } returns NavDestination("").apply { id = R.id.bookmarkFragment }
|
||||
|
||||
controller = DefaultBookmarkController(
|
||||
context = homeActivity,
|
||||
navController = navController,
|
||||
snackbarPresenter = snackbarPresenter,
|
||||
deleteBookmarkNodes = deleteBookmarkNodes
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleBookmarkTapped should load the bookmark in a new tab`() {
|
||||
controller.handleBookmarkTapped(item)
|
||||
|
||||
verifyOrder {
|
||||
homeActivity.browsingModeManager.mode = BrowsingMode.Normal
|
||||
homeActivity.openToBrowserAndLoad(item.url!!, true, BrowserDirection.FromBookmarks)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleBookmarkExpand should navigate to the 'Bookmark' fragment`() {
|
||||
controller.handleBookmarkExpand(tree)
|
||||
|
||||
verify {
|
||||
navController.navigate(BookmarkFragmentDirections.actionBookmarkFragmentSelf(tree.guid))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleSelectionModeSwitch should invalidateOptionsMenu`() {
|
||||
controller.handleSelectionModeSwitch()
|
||||
|
||||
verify {
|
||||
homeActivity.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleBookmarkEdit should navigate to the 'Edit' fragment`() {
|
||||
controller.handleBookmarkEdit(item)
|
||||
|
||||
verify {
|
||||
navController.navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(item.guid))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleBookmarkSelected should show a toast when selecting a folder`() {
|
||||
val errorMessage = context.getString(R.string.bookmark_cannot_edit_root)
|
||||
|
||||
controller.handleBookmarkSelected(root)
|
||||
|
||||
verify {
|
||||
snackbarPresenter.present(errorMessage, any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleCopyUrl should copy bookmark url to clipboard and show a toast`() {
|
||||
val clipboardManager: ClipboardManager = mockk(relaxed = true)
|
||||
val urlCopiedMessage = context.getString(R.string.url_copied)
|
||||
every { any<Context>().getSystemService<ClipboardManager>() } returns clipboardManager
|
||||
every { ClipData.newPlainText(any(), any()) } returns mockk(relaxed = true)
|
||||
|
||||
controller.handleCopyUrl(item)
|
||||
|
||||
verifyOrder {
|
||||
ClipData.newPlainText(item.url, item.url)
|
||||
snackbarPresenter.present(urlCopiedMessage, any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleBookmarkSharing should navigate to the 'Share' fragment`() {
|
||||
controller.handleBookmarkSharing(item)
|
||||
|
||||
verify {
|
||||
navController.navigate(
|
||||
BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment(
|
||||
item.url,
|
||||
item.title
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleOpeningBookmark should open the bookmark a new 'Normal' tab`() {
|
||||
controller.handleOpeningBookmark(item, BrowsingMode.Normal)
|
||||
|
||||
verifyOrder {
|
||||
homeActivity.browsingModeManager.mode = BrowsingMode.Normal
|
||||
homeActivity.openToBrowserAndLoad(item.url!!, true, BrowserDirection.FromBookmarks)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleOpeningBookmark should open the bookmark a new 'Private' tab`() {
|
||||
controller.handleOpeningBookmark(item, BrowsingMode.Private)
|
||||
|
||||
verifyOrder {
|
||||
homeActivity.browsingModeManager.mode = BrowsingMode.Private
|
||||
homeActivity.openToBrowserAndLoad(item.url!!, true, BrowserDirection.FromBookmarks)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleBookmarkDeletion for an item should properly call a passed in delegate`() {
|
||||
controller.handleBookmarkDeletion(setOf(item), Event.RemoveBookmark)
|
||||
|
||||
verify {
|
||||
deleteBookmarkNodes(setOf(item), Event.RemoveBookmark)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleBookmarkDeletion for multiple bookmarks should properly call a passed in delegate`() {
|
||||
controller.handleBookmarkDeletion(setOf(item, subfolder), Event.RemoveBookmarks)
|
||||
|
||||
verify {
|
||||
deleteBookmarkNodes(setOf(item, subfolder), Event.RemoveBookmarks)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleBookmarkDeletion for a folder should properly call a passed in delegate`() {
|
||||
controller.handleBookmarkDeletion(setOf(subfolder), Event.RemoveBookmarkFolder)
|
||||
|
||||
verify {
|
||||
deleteBookmarkNodes(setOf(subfolder), Event.RemoveBookmarkFolder)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleBackPressed should trigger handleBackPressed in NavController`() {
|
||||
controller.handleBackPressed()
|
||||
|
||||
verify {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleSigningIn should trigger 'PairingSignIn`() {
|
||||
controller.handleSigningIn()
|
||||
|
||||
verify {
|
||||
services.launchPairingSignIn(homeActivity, navController)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue