For #2165 - Add swipe to refresh gesture to bookmarks view.
parent
64440409b0
commit
ad90625b48
|
@ -11,13 +11,19 @@ import android.content.res.Resources
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
import mozilla.components.concept.engine.prompt.ShareData
|
import mozilla.components.concept.engine.prompt.ShareData
|
||||||
import mozilla.components.concept.storage.BookmarkNode
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
|
import mozilla.components.service.fxa.sync.SyncReason
|
||||||
import org.mozilla.fenix.BrowserDirection
|
import org.mozilla.fenix.BrowserDirection
|
||||||
import org.mozilla.fenix.HomeActivity
|
import org.mozilla.fenix.HomeActivity
|
||||||
import org.mozilla.fenix.R
|
import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.ext.bookmarkStorage
|
||||||
|
import org.mozilla.fenix.ext.components
|
||||||
import org.mozilla.fenix.ext.nav
|
import org.mozilla.fenix.ext.nav
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,16 +32,20 @@ import org.mozilla.fenix.ext.nav
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("TooManyFunctions")
|
@SuppressWarnings("TooManyFunctions")
|
||||||
interface BookmarkController {
|
interface BookmarkController {
|
||||||
|
fun handleBookmarkChanged(item: BookmarkNode)
|
||||||
fun handleBookmarkTapped(item: BookmarkNode)
|
fun handleBookmarkTapped(item: BookmarkNode)
|
||||||
fun handleBookmarkExpand(folder: BookmarkNode)
|
fun handleBookmarkExpand(folder: BookmarkNode)
|
||||||
fun handleSelectionModeSwitch()
|
fun handleSelectionModeSwitch()
|
||||||
fun handleBookmarkEdit(node: BookmarkNode)
|
fun handleBookmarkEdit(node: BookmarkNode)
|
||||||
fun handleBookmarkSelected(node: BookmarkNode)
|
fun handleBookmarkSelected(node: BookmarkNode)
|
||||||
|
fun handleBookmarkDeselected(node: BookmarkNode)
|
||||||
|
fun handleAllBookmarksDeselected()
|
||||||
fun handleCopyUrl(item: BookmarkNode)
|
fun handleCopyUrl(item: BookmarkNode)
|
||||||
fun handleBookmarkSharing(item: BookmarkNode)
|
fun handleBookmarkSharing(item: BookmarkNode)
|
||||||
fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode)
|
fun handleOpeningBookmark(item: BookmarkNode, mode: BrowsingMode)
|
||||||
fun handleBookmarkDeletion(nodes: Set<BookmarkNode>, eventType: Event)
|
fun handleBookmarkDeletion(nodes: Set<BookmarkNode>, eventType: Event)
|
||||||
fun handleBookmarkFolderDeletion(node: BookmarkNode)
|
fun handleBookmarkFolderDeletion(node: BookmarkNode)
|
||||||
|
fun handleRequestSync()
|
||||||
fun handleBackPressed()
|
fun handleBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +53,10 @@ interface BookmarkController {
|
||||||
class DefaultBookmarkController(
|
class DefaultBookmarkController(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val navController: NavController,
|
private val navController: NavController,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val store: BookmarkFragmentStore,
|
||||||
|
private val sharedViewModel: BookmarksSharedViewModel,
|
||||||
|
private val loadBookmarkNode: suspend (String) -> BookmarkNode?,
|
||||||
private val showSnackbar: (String) -> Unit,
|
private val showSnackbar: (String) -> Unit,
|
||||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit,
|
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit,
|
||||||
private val deleteBookmarkFolder: (BookmarkNode) -> Unit,
|
private val deleteBookmarkFolder: (BookmarkNode) -> Unit,
|
||||||
|
@ -52,12 +66,23 @@ class DefaultBookmarkController(
|
||||||
private val activity: HomeActivity = context as HomeActivity
|
private val activity: HomeActivity = context as HomeActivity
|
||||||
private val resources: Resources = context.resources
|
private val resources: Resources = context.resources
|
||||||
|
|
||||||
|
override fun handleBookmarkChanged(item: BookmarkNode) {
|
||||||
|
sharedViewModel.selectedFolder = item
|
||||||
|
store.dispatch(BookmarkFragmentAction.Change(item))
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleBookmarkTapped(item: BookmarkNode) {
|
override fun handleBookmarkTapped(item: BookmarkNode) {
|
||||||
openInNewTab(item.url!!, true, BrowserDirection.FromBookmarks, activity.browsingModeManager.mode)
|
openInNewTab(item.url!!, true, BrowserDirection.FromBookmarks, activity.browsingModeManager.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleBookmarkExpand(folder: BookmarkNode) {
|
override fun handleBookmarkExpand(folder: BookmarkNode) {
|
||||||
navigate(BookmarkFragmentDirections.actionBookmarkFragmentSelf(folder.guid))
|
handleAllBookmarksDeselected()
|
||||||
|
invokePendingDeletion.invoke()
|
||||||
|
scope.launch {
|
||||||
|
val node = loadBookmarkNode.invoke(folder.guid) ?: return@launch
|
||||||
|
sharedViewModel.selectedFolder = node
|
||||||
|
store.dispatch(BookmarkFragmentAction.Change(node))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleSelectionModeSwitch() {
|
override fun handleSelectionModeSwitch() {
|
||||||
|
@ -69,7 +94,23 @@ class DefaultBookmarkController(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleBookmarkSelected(node: BookmarkNode) {
|
override fun handleBookmarkSelected(node: BookmarkNode) {
|
||||||
showSnackbar(resources.getString(R.string.bookmark_cannot_edit_root))
|
if (store.state.mode is BookmarkFragmentState.Mode.Syncing) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.inRoots()) {
|
||||||
|
showSnackbar(resources.getString(R.string.bookmark_cannot_edit_root))
|
||||||
|
} else {
|
||||||
|
store.dispatch(BookmarkFragmentAction.Select(node))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleBookmarkDeselected(node: BookmarkNode) {
|
||||||
|
store.dispatch(BookmarkFragmentAction.Deselect(node))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleAllBookmarksDeselected() {
|
||||||
|
store.dispatch(BookmarkFragmentAction.DeselectAll)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handleCopyUrl(item: BookmarkNode) {
|
override fun handleCopyUrl(item: BookmarkNode) {
|
||||||
|
@ -98,9 +139,36 @@ class DefaultBookmarkController(
|
||||||
deleteBookmarkFolder(node)
|
deleteBookmarkFolder(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun handleRequestSync() {
|
||||||
|
scope.launch {
|
||||||
|
store.dispatch(BookmarkFragmentAction.StartSync)
|
||||||
|
invokePendingDeletion()
|
||||||
|
context.components.backgroundServices.accountManager.syncNowAsync(SyncReason.User).await()
|
||||||
|
// The current bookmark node we are viewing may be made invalid after syncing so we
|
||||||
|
// check if the current node is valid and if it isn't we find the nearest valid ancestor
|
||||||
|
// and open it
|
||||||
|
val validAncestorGuid = store.state.guidBackstack.findLast { guid ->
|
||||||
|
context.bookmarkStorage.getBookmark(guid) != null
|
||||||
|
} ?: BookmarkRoot.Mobile.id
|
||||||
|
val node = context.bookmarkStorage.getBookmark(validAncestorGuid)!!
|
||||||
|
handleBookmarkExpand(node)
|
||||||
|
store.dispatch(BookmarkFragmentAction.FinishSync)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun handleBackPressed() {
|
override fun handleBackPressed() {
|
||||||
invokePendingDeletion.invoke()
|
invokePendingDeletion.invoke()
|
||||||
navController.popBackStack()
|
scope.launch {
|
||||||
|
val parentGuid = store.state.guidBackstack.findLast { guid ->
|
||||||
|
store.state.tree?.guid != guid && context.bookmarkStorage.getBookmark(guid) != null
|
||||||
|
}
|
||||||
|
if (parentGuid == null) {
|
||||||
|
navController.popBackStack()
|
||||||
|
} else {
|
||||||
|
val parent = context.bookmarkStorage.getBookmark(parentGuid)!!
|
||||||
|
handleBookmarkExpand(parent)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openInNewTab(
|
private fun openInNewTab(
|
||||||
|
|
|
@ -13,18 +13,19 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
import kotlinx.android.synthetic.main.component_bookmark.view.*
|
import kotlinx.android.synthetic.main.component_bookmark.view.*
|
||||||
import kotlinx.android.synthetic.main.fragment_bookmark.view.*
|
import kotlinx.android.synthetic.main.fragment_bookmark.view.*
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.async
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.awaitAll
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
|
@ -59,7 +60,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||||
private lateinit var bookmarkStore: BookmarkFragmentStore
|
private lateinit var bookmarkStore: BookmarkFragmentStore
|
||||||
private lateinit var bookmarkView: BookmarkView
|
private lateinit var bookmarkView: BookmarkView
|
||||||
private var _bookmarkInteractor: BookmarkFragmentInteractor? = null
|
private var _bookmarkInteractor: BookmarkFragmentInteractor? = null
|
||||||
protected val bookmarkInteractor: BookmarkFragmentInteractor
|
private val bookmarkInteractor: BookmarkFragmentInteractor
|
||||||
get() = _bookmarkInteractor!!
|
get() = _bookmarkInteractor!!
|
||||||
|
|
||||||
private val sharedViewModel: BookmarksSharedViewModel by activityViewModels {
|
private val sharedViewModel: BookmarksSharedViewModel by activityViewModels {
|
||||||
|
@ -67,7 +68,6 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||||
}
|
}
|
||||||
private val desktopFolders by lazy { DesktopFolders(requireContext(), showMobileRoot = false) }
|
private val desktopFolders by lazy { DesktopFolders(requireContext(), showMobileRoot = false) }
|
||||||
|
|
||||||
lateinit var initialJob: Job
|
|
||||||
private var pendingBookmarkDeletionJob: (suspend () -> Unit)? = null
|
private var pendingBookmarkDeletionJob: (suspend () -> Unit)? = null
|
||||||
private var pendingBookmarksToDelete: MutableSet<BookmarkNode> = mutableSetOf()
|
private var pendingBookmarksToDelete: MutableSet<BookmarkNode> = mutableSetOf()
|
||||||
|
|
||||||
|
@ -84,11 +84,13 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||||
}
|
}
|
||||||
|
|
||||||
_bookmarkInteractor = BookmarkFragmentInteractor(
|
_bookmarkInteractor = BookmarkFragmentInteractor(
|
||||||
bookmarkStore = bookmarkStore,
|
|
||||||
viewModel = sharedViewModel,
|
|
||||||
bookmarksController = DefaultBookmarkController(
|
bookmarksController = DefaultBookmarkController(
|
||||||
context = requireContext(),
|
context = requireContext(),
|
||||||
navController = findNavController(),
|
navController = findNavController(),
|
||||||
|
scope = viewLifecycleOwner.lifecycleScope,
|
||||||
|
store = bookmarkStore,
|
||||||
|
sharedViewModel = sharedViewModel,
|
||||||
|
loadBookmarkNode = ::loadBookmarkNode,
|
||||||
showSnackbar = ::showSnackBarWithText,
|
showSnackbar = ::showSnackBarWithText,
|
||||||
deleteBookmarkNodes = ::deleteMulti,
|
deleteBookmarkNodes = ::deleteMulti,
|
||||||
deleteBookmarkFolder = ::showRemoveFolderDialog,
|
deleteBookmarkFolder = ::showRemoveFolderDialog,
|
||||||
|
@ -124,8 +126,16 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
val accountManager = requireComponents.backgroundServices.accountManager
|
||||||
consumeFrom(bookmarkStore) {
|
consumeFrom(bookmarkStore) {
|
||||||
bookmarkView.update(it)
|
bookmarkView.update(it)
|
||||||
|
|
||||||
|
// Only display the sign-in prompt if we're inside of the virtual "Desktop Bookmarks" node.
|
||||||
|
// Don't want to pester user too much with it, and if there are lots of bookmarks present,
|
||||||
|
// it'll just get visually lost. Inside of the "Desktop Bookmarks" node, it'll nicely stand-out,
|
||||||
|
// since there are always only three other items in there. It's also the right place contextually.
|
||||||
|
bookmarkView.view.bookmark_folders_sign_in.isVisible =
|
||||||
|
it.tree?.guid == BookmarkRoot.Root.id && accountManager.authenticatedAccount() == null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,36 +148,24 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
|
||||||
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().show()
|
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().show()
|
||||||
val currentGuid = BookmarkFragmentArgs.fromBundle(requireArguments()).currentRoot.ifEmpty {
|
|
||||||
BookmarkRoot.Mobile.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only display the sign-in prompt if we're inside of the virtual "Desktop Bookmarks" node.
|
// Reload bookmarks when returning to this fragment in case they have been edited
|
||||||
// Don't want to pester user too much with it, and if there are lots of bookmarks present,
|
val args by navArgs<BookmarkFragmentArgs>()
|
||||||
// it'll just get visually lost. Inside of the "Desktop Bookmarks" node, it'll nicely stand-out,
|
val currentGuid = bookmarkStore.state.tree?.guid
|
||||||
// since there are always only three other items in there. It's also the right place contextually.
|
?: if (args.currentRoot.isNotEmpty()) {
|
||||||
if (currentGuid == BookmarkRoot.Root.id &&
|
args.currentRoot
|
||||||
requireComponents.backgroundServices.accountManager.authenticatedAccount() == null
|
} else {
|
||||||
) {
|
BookmarkRoot.Mobile.id
|
||||||
bookmarkView.view.bookmark_folders_sign_in.visibility = View.VISIBLE
|
}
|
||||||
} else {
|
loadInitialBookmarkFolder(currentGuid)
|
||||||
bookmarkView.view.bookmark_folders_sign_in.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
initialJob = loadInitialBookmarkFolder(currentGuid)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadInitialBookmarkFolder(currentGuid: String): Job {
|
private fun loadInitialBookmarkFolder(currentGuid: String) {
|
||||||
return viewLifecycleOwner.lifecycleScope.launch(Main) {
|
viewLifecycleOwner.lifecycleScope.launch(Main) {
|
||||||
val currentRoot = withContext(IO) {
|
val currentRoot = loadBookmarkNode(currentGuid)
|
||||||
requireContext().bookmarkStorage
|
|
||||||
.getTree(currentGuid)
|
|
||||||
?.let { desktopFolders.withOptionalDesktopFolders(it) }!!
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive && currentRoot != null) {
|
||||||
bookmarkInteractor.onBookmarksChanged(currentRoot)
|
bookmarkInteractor.onBookmarksChanged(currentRoot)
|
||||||
sharedViewModel.selectedFolder = currentRoot
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -249,14 +247,18 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
|
||||||
return bookmarkView.onBackPressed()
|
return bookmarkView.onBackPressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun loadBookmarkNode(guid: String): BookmarkNode? = withContext(IO) {
|
||||||
|
requireContext().bookmarkStorage
|
||||||
|
.getTree(guid, false)
|
||||||
|
?.let { desktopFolders.withOptionalDesktopFolders(it) }
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun refreshBookmarks() {
|
private suspend fun refreshBookmarks() {
|
||||||
// The bookmark tree in our 'state' can be null - meaning, no bookmark tree has been selected.
|
// The bookmark tree in our 'state' can be null - meaning, no bookmark tree has been selected.
|
||||||
// If that's the case, we don't know what node to refresh, and so we bail out.
|
// If that's the case, we don't know what node to refresh, and so we bail out.
|
||||||
// See https://github.com/mozilla-mobile/fenix/issues/4671
|
// See https://github.com/mozilla-mobile/fenix/issues/4671
|
||||||
val currentGuid = bookmarkStore.state.tree?.guid ?: return
|
val currentGuid = bookmarkStore.state.tree?.guid ?: return
|
||||||
context?.bookmarkStorage
|
loadBookmarkNode(currentGuid)
|
||||||
?.getTree(currentGuid, false)
|
|
||||||
?.let { desktopFolders.withOptionalDesktopFolders(it) }
|
|
||||||
?.let { node ->
|
?.let { node ->
|
||||||
val rootNode = node - pendingBookmarksToDelete
|
val rootNode = node - pendingBookmarksToDelete
|
||||||
bookmarkInteractor.onBookmarksChanged(rootNode)
|
bookmarkInteractor.onBookmarksChanged(rootNode)
|
||||||
|
|
|
@ -22,14 +22,12 @@ import org.mozilla.fenix.utils.Do
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("TooManyFunctions")
|
@SuppressWarnings("TooManyFunctions")
|
||||||
class BookmarkFragmentInteractor(
|
class BookmarkFragmentInteractor(
|
||||||
private val bookmarkStore: BookmarkFragmentStore,
|
|
||||||
private val viewModel: BookmarksSharedViewModel,
|
|
||||||
private val bookmarksController: BookmarkController,
|
private val bookmarksController: BookmarkController,
|
||||||
private val metrics: MetricController
|
private val metrics: MetricController
|
||||||
) : BookmarkViewInteractor {
|
) : BookmarkViewInteractor {
|
||||||
|
|
||||||
override fun onBookmarksChanged(node: BookmarkNode) {
|
override fun onBookmarksChanged(node: BookmarkNode) {
|
||||||
bookmarkStore.dispatch(BookmarkFragmentAction.Change(node))
|
bookmarksController.handleBookmarkChanged(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSelectionModeSwitch(mode: BookmarkFragmentState.Mode) {
|
override fun onSelectionModeSwitch(mode: BookmarkFragmentState.Mode) {
|
||||||
|
@ -41,7 +39,7 @@ class BookmarkFragmentInteractor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAllBookmarksDeselected() {
|
override fun onAllBookmarksDeselected() {
|
||||||
bookmarkStore.dispatch(BookmarkFragmentAction.DeselectAll)
|
bookmarksController.handleAllBookmarksDeselected()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -112,13 +110,14 @@ class BookmarkFragmentInteractor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun select(item: BookmarkNode) {
|
override fun select(item: BookmarkNode) {
|
||||||
when (item.inRoots()) {
|
bookmarksController.handleBookmarkSelected(item)
|
||||||
true -> bookmarksController.handleBookmarkSelected(item)
|
|
||||||
false -> bookmarkStore.dispatch(BookmarkFragmentAction.Select(item))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deselect(item: BookmarkNode) {
|
override fun deselect(item: BookmarkNode) {
|
||||||
bookmarkStore.dispatch(BookmarkFragmentAction.Deselect(item))
|
bookmarksController.handleBookmarkDeselected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestSync() {
|
||||||
|
bookmarksController.handleRequestSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,10 +20,14 @@ class BookmarkFragmentStore(
|
||||||
* The complete state of the bookmarks tree and multi-selection mode
|
* The complete state of the bookmarks tree and multi-selection mode
|
||||||
* @property tree The current tree of bookmarks, if one is loaded
|
* @property tree The current tree of bookmarks, if one is loaded
|
||||||
* @property mode The current bookmark multi-selection mode
|
* @property mode The current bookmark multi-selection mode
|
||||||
|
* @property guidBackstack A set of guids for bookmark nodes we have visited. Used to traverse back
|
||||||
|
* up the tree after a sync.
|
||||||
|
* @property isLoading true if bookmarks are still being loaded from disk
|
||||||
*/
|
*/
|
||||||
data class BookmarkFragmentState(
|
data class BookmarkFragmentState(
|
||||||
val tree: BookmarkNode?,
|
val tree: BookmarkNode?,
|
||||||
val mode: Mode = Mode.Normal(),
|
val mode: Mode = Mode.Normal(),
|
||||||
|
val guidBackstack: List<String> = emptyList(),
|
||||||
val isLoading: Boolean = true
|
val isLoading: Boolean = true
|
||||||
) : State {
|
) : State {
|
||||||
sealed class Mode {
|
sealed class Mode {
|
||||||
|
@ -31,6 +35,7 @@ data class BookmarkFragmentState(
|
||||||
|
|
||||||
data class Normal(val showMenu: Boolean = true) : Mode()
|
data class Normal(val showMenu: Boolean = true) : Mode()
|
||||||
data class Selecting(override val selectedItems: Set<BookmarkNode>) : Mode()
|
data class Selecting(override val selectedItems: Set<BookmarkNode>) : Mode()
|
||||||
|
object Syncing : Mode()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -42,6 +47,8 @@ sealed class BookmarkFragmentAction : Action {
|
||||||
data class Select(val item: BookmarkNode) : BookmarkFragmentAction()
|
data class Select(val item: BookmarkNode) : BookmarkFragmentAction()
|
||||||
data class Deselect(val item: BookmarkNode) : BookmarkFragmentAction()
|
data class Deselect(val item: BookmarkNode) : BookmarkFragmentAction()
|
||||||
object DeselectAll : BookmarkFragmentAction()
|
object DeselectAll : BookmarkFragmentAction()
|
||||||
|
object StartSync : BookmarkFragmentAction()
|
||||||
|
object FinishSync : BookmarkFragmentAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,16 +63,26 @@ private fun bookmarkFragmentStateReducer(
|
||||||
): BookmarkFragmentState {
|
): BookmarkFragmentState {
|
||||||
return when (action) {
|
return when (action) {
|
||||||
is BookmarkFragmentAction.Change -> {
|
is BookmarkFragmentAction.Change -> {
|
||||||
|
// If we change to a node we have already visited, we pop the backstack until the node
|
||||||
|
// is the last item. If we haven't visited the node yet, we just add it to the end of the
|
||||||
|
// backstack
|
||||||
|
val backstack = state.guidBackstack.takeWhile { guid ->
|
||||||
|
guid != action.tree.guid
|
||||||
|
} + action.tree.guid
|
||||||
|
|
||||||
val items = state.mode.selectedItems.filter { it in action.tree }
|
val items = state.mode.selectedItems.filter { it in action.tree }
|
||||||
state.copy(
|
state.copy(
|
||||||
tree = action.tree,
|
tree = action.tree,
|
||||||
mode = if (BookmarkRoot.Root.id == action.tree.guid) {
|
mode = when {
|
||||||
BookmarkFragmentState.Mode.Normal(false)
|
state.mode is BookmarkFragmentState.Mode.Syncing -> {
|
||||||
} else if (items.isEmpty()) {
|
BookmarkFragmentState.Mode.Syncing
|
||||||
BookmarkFragmentState.Mode.Normal()
|
}
|
||||||
} else {
|
items.isEmpty() -> {
|
||||||
BookmarkFragmentState.Mode.Selecting(items.toSet())
|
BookmarkFragmentState.Mode.Normal(shouldShowMenu(action.tree.guid))
|
||||||
|
}
|
||||||
|
else -> BookmarkFragmentState.Mode.Selecting(items.toSet())
|
||||||
},
|
},
|
||||||
|
guidBackstack = backstack,
|
||||||
isLoading = false
|
isLoading = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -81,11 +98,30 @@ private fun bookmarkFragmentStateReducer(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
BookmarkFragmentAction.DeselectAll ->
|
is BookmarkFragmentAction.DeselectAll ->
|
||||||
state.copy(mode = BookmarkFragmentState.Mode.Normal())
|
state.copy(
|
||||||
|
mode = if (state.mode is BookmarkFragmentState.Mode.Syncing) {
|
||||||
|
BookmarkFragmentState.Mode.Syncing
|
||||||
|
} else {
|
||||||
|
BookmarkFragmentState.Mode.Normal()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
is BookmarkFragmentAction.StartSync ->
|
||||||
|
state.copy(
|
||||||
|
mode = BookmarkFragmentState.Mode.Syncing
|
||||||
|
)
|
||||||
|
is BookmarkFragmentAction.FinishSync ->
|
||||||
|
state.copy(
|
||||||
|
mode = BookmarkFragmentState.Mode.Normal(
|
||||||
|
showMenu = shouldShowMenu(state.tree?.guid)
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun shouldShowMenu(currentGuid: String?): Boolean =
|
||||||
|
BookmarkRoot.Root.id != currentGuid
|
||||||
|
|
||||||
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
|
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
|
||||||
return children?.contains(item) ?: false
|
return children?.contains(item) ?: false
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,6 +93,12 @@ interface BookmarkViewInteractor : SelectionInteractor<BookmarkNode> {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
fun onBackPressed()
|
fun onBackPressed()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles user requested sync of bookmarks.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
fun onRequestSync()
|
||||||
}
|
}
|
||||||
|
|
||||||
class BookmarkView(
|
class BookmarkView(
|
||||||
|
@ -106,7 +112,6 @@ class BookmarkView(
|
||||||
|
|
||||||
private var mode: BookmarkFragmentState.Mode = BookmarkFragmentState.Mode.Normal()
|
private var mode: BookmarkFragmentState.Mode = BookmarkFragmentState.Mode.Normal()
|
||||||
private var tree: BookmarkNode? = null
|
private var tree: BookmarkNode? = null
|
||||||
private var canGoBack = false
|
|
||||||
|
|
||||||
private val bookmarkAdapter: BookmarkAdapter
|
private val bookmarkAdapter: BookmarkAdapter
|
||||||
|
|
||||||
|
@ -118,14 +123,18 @@ class BookmarkView(
|
||||||
view.bookmark_folders_sign_in.setOnClickListener {
|
view.bookmark_folders_sign_in.setOnClickListener {
|
||||||
navController.navigate(NavGraphDirections.actionGlobalTurnOnSync())
|
navController.navigate(NavGraphDirections.actionGlobalTurnOnSync())
|
||||||
}
|
}
|
||||||
|
view.swipe_refresh.setOnRefreshListener {
|
||||||
|
interactor.onRequestSync()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(state: BookmarkFragmentState) {
|
fun update(state: BookmarkFragmentState) {
|
||||||
canGoBack = BookmarkRoot.Root.matches(state.tree)
|
|
||||||
tree = state.tree
|
tree = state.tree
|
||||||
if (state.mode != mode) {
|
if (state.mode != mode) {
|
||||||
mode = state.mode
|
mode = state.mode
|
||||||
interactor.onSelectionModeSwitch(mode)
|
if (mode is BookmarkFragmentState.Mode.Normal || mode is BookmarkFragmentState.Mode.Selecting) {
|
||||||
|
interactor.onSelectionModeSwitch(mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarkAdapter.updateData(state.tree, mode)
|
bookmarkAdapter.updateData(state.tree, mode)
|
||||||
|
@ -151,19 +160,21 @@ class BookmarkView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
view.bookmarks_progress_bar.isVisible = state.isLoading
|
view.bookmarks_progress_bar.isVisible = state.isLoading
|
||||||
|
view.swipe_refresh.isEnabled =
|
||||||
|
state.mode is BookmarkFragmentState.Mode.Normal || state.mode is BookmarkFragmentState.Mode.Syncing
|
||||||
|
view.swipe_refresh.isRefreshing = state.mode is BookmarkFragmentState.Mode.Syncing
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed(): Boolean {
|
override fun onBackPressed(): Boolean {
|
||||||
return when {
|
return when (mode) {
|
||||||
mode is BookmarkFragmentState.Mode.Selecting -> {
|
is BookmarkFragmentState.Mode.Selecting -> {
|
||||||
interactor.onAllBookmarksDeselected()
|
interactor.onAllBookmarksDeselected()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
canGoBack -> {
|
else -> {
|
||||||
interactor.onBackPressed()
|
interactor.onBackPressed()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,57 +1,62 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?><!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
<!-- 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
|
- 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/. -->
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/bookmarks_wrapper"
|
android:id="@+id/swipe_refresh"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/bookmark_list"
|
android:id="@+id/bookmarks_wrapper"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent">
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:listitem="@layout/library_site_item" />
|
|
||||||
|
|
||||||
<TextView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/bookmarks_empty_view"
|
android:id="@+id/bookmark_list"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
android:text="@string/bookmarks_empty_message"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:textColor="?secondaryText"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:textSize="16sp"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:visibility="gone"
|
tools:listitem="@layout/library_site_item" />
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<TextView
|
||||||
android:id="@+id/bookmark_folders_sign_in"
|
android:id="@+id/bookmarks_empty_view"
|
||||||
style="@style/NeutralButton"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_gravity="center"
|
||||||
android:text="@string/bookmark_sign_in_button"
|
android:text="@string/bookmarks_empty_message"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:textColor="?secondaryText"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:textSize="16sp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:visibility="gone"
|
||||||
app:layout_constraintTop_toBottomOf="@id/bookmark_list"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintVertical_bias="0.0" />
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<ProgressBar
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/bookmarks_progress_bar"
|
android:id="@+id/bookmark_folders_sign_in"
|
||||||
android:layout_width="wrap_content"
|
style="@style/NeutralButton"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:text="@string/bookmark_sign_in_button"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/bookmark_list"
|
||||||
|
app:layout_constraintVertical_bias="0.0" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
<ProgressBar
|
||||||
|
android:id="@+id/bookmarks_progress_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
|
|
|
@ -12,16 +12,24 @@ import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.NavDestination
|
||||||
import androidx.navigation.NavDirections
|
import androidx.navigation.NavDirections
|
||||||
import io.mockk.Runs
|
import io.mockk.Runs
|
||||||
|
import io.mockk.called
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.runs
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
|
import io.mockk.spyk
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.mockk.verifyOrder
|
import io.mockk.verifyOrder
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.TestCoroutineScope
|
||||||
import mozilla.appservices.places.BookmarkRoot
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
import mozilla.components.concept.storage.BookmarkNode
|
import mozilla.components.concept.storage.BookmarkNode
|
||||||
import mozilla.components.concept.storage.BookmarkNodeType
|
import mozilla.components.concept.storage.BookmarkNodeType
|
||||||
|
import org.junit.After
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -31,15 +39,21 @@ import org.mozilla.fenix.R
|
||||||
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
|
||||||
import org.mozilla.fenix.components.Services
|
import org.mozilla.fenix.components.Services
|
||||||
import org.mozilla.fenix.components.metrics.Event
|
import org.mozilla.fenix.components.metrics.Event
|
||||||
|
import org.mozilla.fenix.ext.bookmarkStorage
|
||||||
import org.mozilla.fenix.ext.components
|
import org.mozilla.fenix.ext.components
|
||||||
|
|
||||||
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
@SuppressWarnings("TooManyFunctions", "LargeClass")
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
class BookmarkControllerTest {
|
class BookmarkControllerTest {
|
||||||
|
|
||||||
private lateinit var controller: BookmarkController
|
private lateinit var controller: BookmarkController
|
||||||
|
|
||||||
|
private val bookmarkStore = spyk(BookmarkFragmentStore(BookmarkFragmentState(null)))
|
||||||
private val context: Context = mockk(relaxed = true)
|
private val context: Context = mockk(relaxed = true)
|
||||||
|
private val scope = TestCoroutineScope()
|
||||||
private val navController: NavController = mockk(relaxed = true)
|
private val navController: NavController = mockk(relaxed = true)
|
||||||
|
private val sharedViewModel: BookmarksSharedViewModel = mockk()
|
||||||
|
private val loadBookmarkNode: suspend (String) -> BookmarkNode? = mockk(relaxed = true)
|
||||||
private val showSnackbar: (String) -> Unit = mockk(relaxed = true)
|
private val showSnackbar: (String) -> Unit = mockk(relaxed = true)
|
||||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit = mockk(relaxed = true)
|
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit = mockk(relaxed = true)
|
||||||
private val deleteBookmarkFolder: (BookmarkNode) -> Unit = mockk(relaxed = true)
|
private val deleteBookmarkFolder: (BookmarkNode) -> Unit = mockk(relaxed = true)
|
||||||
|
@ -87,10 +101,16 @@ class BookmarkControllerTest {
|
||||||
every { navController.currentDestination } returns NavDestination("").apply {
|
every { navController.currentDestination } returns NavDestination("").apply {
|
||||||
id = R.id.bookmarkFragment
|
id = R.id.bookmarkFragment
|
||||||
}
|
}
|
||||||
|
every { bookmarkStore.dispatch(any()) } returns mockk()
|
||||||
|
every { sharedViewModel.selectedFolder = any() } just runs
|
||||||
|
|
||||||
controller = DefaultBookmarkController(
|
controller = DefaultBookmarkController(
|
||||||
context = homeActivity,
|
context = homeActivity,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
scope = scope,
|
||||||
|
store = bookmarkStore,
|
||||||
|
sharedViewModel = sharedViewModel,
|
||||||
|
loadBookmarkNode = loadBookmarkNode,
|
||||||
showSnackbar = showSnackbar,
|
showSnackbar = showSnackbar,
|
||||||
deleteBookmarkNodes = deleteBookmarkNodes,
|
deleteBookmarkNodes = deleteBookmarkNodes,
|
||||||
deleteBookmarkFolder = deleteBookmarkFolder,
|
deleteBookmarkFolder = deleteBookmarkFolder,
|
||||||
|
@ -98,6 +118,21 @@ class BookmarkControllerTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun cleanUp() {
|
||||||
|
scope.cleanupTestCoroutines()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `handleBookmarkChanged updates the selected bookmark node`() {
|
||||||
|
controller.handleBookmarkChanged(tree)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
sharedViewModel.selectedFolder = tree
|
||||||
|
bookmarkStore.dispatch(BookmarkFragmentAction.Change(tree))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `handleBookmarkTapped should load the bookmark in a new tab`() {
|
fun `handleBookmarkTapped should load the bookmark in a new tab`() {
|
||||||
controller.handleBookmarkTapped(item)
|
controller.handleBookmarkTapped(item)
|
||||||
|
@ -124,15 +159,27 @@ class BookmarkControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `handleBookmarkExpand should navigate to the 'Bookmark' fragment`() {
|
fun `handleBookmarkExpand clears selection and invokes pending deletions`() {
|
||||||
|
coEvery { loadBookmarkNode.invoke(any()) } returns tree
|
||||||
|
|
||||||
controller.handleBookmarkExpand(tree)
|
controller.handleBookmarkExpand(tree)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
invokePendingDeletion.invoke()
|
invokePendingDeletion.invoke()
|
||||||
navController.navigate(
|
controller.handleAllBookmarksDeselected()
|
||||||
BookmarkFragmentDirections.actionBookmarkFragmentSelf(tree.guid),
|
}
|
||||||
null
|
}
|
||||||
)
|
|
||||||
|
@Test
|
||||||
|
fun `handleBookmarkExpand should refresh and change the active bookmark node`() {
|
||||||
|
coEvery { loadBookmarkNode.invoke(any()) } returns tree
|
||||||
|
|
||||||
|
controller.handleBookmarkExpand(tree)
|
||||||
|
|
||||||
|
coVerify {
|
||||||
|
loadBookmarkNode.invoke(tree.guid)
|
||||||
|
sharedViewModel.selectedFolder = tree
|
||||||
|
bookmarkStore.dispatch(BookmarkFragmentAction.Change(tree))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +208,16 @@ class BookmarkControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `handleBookmarkSelected should show a toast when selecting a folder`() {
|
fun `handleBookmarkSelected dispatches Select action when selecting a non-root folder`() {
|
||||||
|
controller.handleBookmarkSelected(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
bookmarkStore.dispatch(BookmarkFragmentAction.Select(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `handleBookmarkSelected should show a toast when selecting a root folder`() {
|
||||||
val errorMessage = context.getString(R.string.bookmark_cannot_edit_root)
|
val errorMessage = context.getString(R.string.bookmark_cannot_edit_root)
|
||||||
|
|
||||||
controller.handleBookmarkSelected(root)
|
controller.handleBookmarkSelected(root)
|
||||||
|
@ -171,6 +227,24 @@ class BookmarkControllerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `handleBookmarkSelected does not select in Syncing mode`() {
|
||||||
|
every { bookmarkStore.state.mode } returns BookmarkFragmentState.Mode.Syncing
|
||||||
|
|
||||||
|
controller.handleBookmarkSelected(item)
|
||||||
|
|
||||||
|
verify { bookmarkStore.dispatch(BookmarkFragmentAction.Select(item)) wasNot called }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `handleBookmarkDeselected dispatches Deselect action`() {
|
||||||
|
controller.handleBookmarkDeselected(item)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
bookmarkStore.dispatch(BookmarkFragmentAction.Deselect(item))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `handleCopyUrl should copy bookmark url to clipboard and show a toast`() {
|
fun `handleCopyUrl should copy bookmark url to clipboard and show a toast`() {
|
||||||
val clipboardManager: ClipboardManager = mockk(relaxed = true)
|
val clipboardManager: ClipboardManager = mockk(relaxed = true)
|
||||||
|
@ -248,7 +322,24 @@ class BookmarkControllerTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `handleBackPressed should trigger handleBackPressed in NavController`() {
|
fun `handleRequestSync dispatches actions in the correct order`() {
|
||||||
|
every { homeActivity.components.backgroundServices.accountManager } returns mockk(relaxed = true)
|
||||||
|
coEvery { homeActivity.bookmarkStorage.getBookmark(any()) } returns tree
|
||||||
|
coEvery { loadBookmarkNode.invoke(any()) } returns tree
|
||||||
|
|
||||||
|
controller.handleRequestSync()
|
||||||
|
|
||||||
|
verifyOrder {
|
||||||
|
bookmarkStore.dispatch(BookmarkFragmentAction.StartSync)
|
||||||
|
bookmarkStore.dispatch(BookmarkFragmentAction.FinishSync)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `handleBackPressed with one item in backstack should trigger handleBackPressed in NavController`() {
|
||||||
|
every { bookmarkStore.state.guidBackstack } returns listOf(tree.guid)
|
||||||
|
every { bookmarkStore.state.tree } returns tree
|
||||||
|
|
||||||
controller.handleBackPressed()
|
controller.handleBackPressed()
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
|
|
|
@ -4,10 +4,7 @@
|
||||||
|
|
||||||
package org.mozilla.fenix.library.bookmarks
|
package org.mozilla.fenix.library.bookmarks
|
||||||
|
|
||||||
import io.mockk.called
|
|
||||||
import io.mockk.every
|
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.spyk
|
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.mockk.verifyOrder
|
import io.mockk.verifyOrder
|
||||||
import mozilla.appservices.places.BookmarkRoot
|
import mozilla.appservices.places.BookmarkRoot
|
||||||
|
@ -24,8 +21,6 @@ class BookmarkFragmentInteractorTest {
|
||||||
|
|
||||||
private lateinit var interactor: BookmarkFragmentInteractor
|
private lateinit var interactor: BookmarkFragmentInteractor
|
||||||
|
|
||||||
private val bookmarkStore = spyk(BookmarkFragmentStore(BookmarkFragmentState(null)))
|
|
||||||
private val sharedViewModel: BookmarksSharedViewModel = mockk(relaxed = true)
|
|
||||||
private val bookmarkController: DefaultBookmarkController = mockk(relaxed = true)
|
private val bookmarkController: DefaultBookmarkController = mockk(relaxed = true)
|
||||||
private val metrics: MetricController = mockk(relaxed = true)
|
private val metrics: MetricController = mockk(relaxed = true)
|
||||||
|
|
||||||
|
@ -41,12 +36,8 @@ class BookmarkFragmentInteractorTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
every { bookmarkStore.dispatch(any()) } returns mockk()
|
|
||||||
|
|
||||||
interactor =
|
interactor =
|
||||||
BookmarkFragmentInteractor(
|
BookmarkFragmentInteractor(
|
||||||
bookmarkStore = bookmarkStore,
|
|
||||||
viewModel = sharedViewModel,
|
|
||||||
bookmarksController = bookmarkController,
|
bookmarksController = bookmarkController,
|
||||||
metrics = metrics
|
metrics = metrics
|
||||||
)
|
)
|
||||||
|
@ -57,7 +48,7 @@ class BookmarkFragmentInteractorTest {
|
||||||
interactor.onBookmarksChanged(tree)
|
interactor.onBookmarksChanged(tree)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
bookmarkStore.dispatch(BookmarkFragmentAction.Change(tree))
|
bookmarkController.handleBookmarkChanged(tree)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +99,7 @@ class BookmarkFragmentInteractorTest {
|
||||||
interactor.select(item)
|
interactor.select(item)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
bookmarkStore.dispatch(BookmarkFragmentAction.Select(item))
|
bookmarkController.handleBookmarkSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +108,7 @@ class BookmarkFragmentInteractorTest {
|
||||||
interactor.deselect(item)
|
interactor.deselect(item)
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
bookmarkStore.dispatch(BookmarkFragmentAction.Deselect(item))
|
bookmarkController.handleBookmarkDeselected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,17 +117,10 @@ class BookmarkFragmentInteractorTest {
|
||||||
interactor.onAllBookmarksDeselected()
|
interactor.onAllBookmarksDeselected()
|
||||||
|
|
||||||
verify {
|
verify {
|
||||||
bookmarkStore.dispatch(BookmarkFragmentAction.DeselectAll)
|
bookmarkController.handleAllBookmarksDeselected()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `cannot select bookmark roots`() {
|
|
||||||
interactor.select(root)
|
|
||||||
|
|
||||||
verify { bookmarkStore wasNot called }
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `copy a bookmark item`() {
|
fun `copy a bookmark item`() {
|
||||||
interactor.onCopyPressed(item)
|
interactor.onCopyPressed(item)
|
||||||
|
@ -217,4 +201,13 @@ class BookmarkFragmentInteractorTest {
|
||||||
bookmarkController.handleBackPressed()
|
bookmarkController.handleBackPressed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `request a sync`() {
|
||||||
|
interactor.onRequestSync()
|
||||||
|
|
||||||
|
verify {
|
||||||
|
bookmarkController.handleRequestSync()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,30 @@ class BookmarkFragmentStoreTest {
|
||||||
assertEquals(store.state.mode, initialState.mode)
|
assertEquals(store.state.mode, initialState.mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `changing the tree of bookmarks adds the tree to the visited nodes`() = runBlocking {
|
||||||
|
val initialState = BookmarkFragmentState(null)
|
||||||
|
val store = BookmarkFragmentStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkFragmentAction.Change(tree)).join()
|
||||||
|
store.dispatch(BookmarkFragmentAction.Change(subfolder)).join()
|
||||||
|
|
||||||
|
assertEquals(listOf(tree.guid, subfolder.guid), store.state.guidBackstack)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `changing to a node that is in the backstack removes backstack items after that node`() = runBlocking {
|
||||||
|
val initialState = BookmarkFragmentState(
|
||||||
|
null,
|
||||||
|
guidBackstack = listOf(tree.guid, subfolder.guid, item.guid)
|
||||||
|
)
|
||||||
|
val store = BookmarkFragmentStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkFragmentAction.Change(tree)).join()
|
||||||
|
|
||||||
|
assertEquals(listOf(tree.guid), store.state.guidBackstack)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `change the tree of bookmarks to the same value`() = runBlocking {
|
fun `change the tree of bookmarks to the same value`() = runBlocking {
|
||||||
val initialState = BookmarkFragmentState(tree)
|
val initialState = BookmarkFragmentState(tree)
|
||||||
|
@ -177,6 +201,19 @@ class BookmarkFragmentStoreTest {
|
||||||
assertEquals(store.state.mode, BookmarkFragmentState.Mode.Normal(false))
|
assertEquals(store.state.mode, BookmarkFragmentState.Mode.Normal(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `changing the tree or deselecting in Syncing mode should stay in Syncing mode`() = runBlocking {
|
||||||
|
val initialState = BookmarkFragmentState(tree)
|
||||||
|
val store = BookmarkFragmentStore(initialState)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkFragmentAction.StartSync).join()
|
||||||
|
store.dispatch(BookmarkFragmentAction.Change(childItem))
|
||||||
|
assertEquals(BookmarkFragmentState.Mode.Syncing, store.state.mode)
|
||||||
|
|
||||||
|
store.dispatch(BookmarkFragmentAction.DeselectAll).join()
|
||||||
|
assertEquals(BookmarkFragmentState.Mode.Syncing, store.state.mode)
|
||||||
|
}
|
||||||
|
|
||||||
private val item = BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null)
|
private val item = BookmarkNode(BookmarkNodeType.ITEM, "456", "123", 0, "Mozilla", "http://mozilla.org", null)
|
||||||
private val separator = BookmarkNode(BookmarkNodeType.SEPARATOR, "789", "123", 1, null, null, null)
|
private val separator = BookmarkNode(BookmarkNodeType.SEPARATOR, "789", "123", 1, null, null, null)
|
||||||
private val subfolder = BookmarkNode(BookmarkNodeType.FOLDER, "987", "123", 0, "Subfolder", null, listOf())
|
private val subfolder = BookmarkNode(BookmarkNodeType.FOLDER, "987", "123", 0, "Subfolder", null, listOf())
|
||||||
|
|
Loading…
Reference in New Issue