[fenix] For https://github.com/mozilla-mobile/fenix/issues/4128: Migrate Bookmarks to LibState (https://github.com/mozilla-mobile/fenix/pull/4254)
parent
3eb95adbc7
commit
526c077529
@ -1,110 +0,0 @@
|
||||
/* 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.view.ViewGroup
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import org.mozilla.fenix.mvi.ViewState
|
||||
import org.mozilla.fenix.mvi.Change
|
||||
import org.mozilla.fenix.mvi.Action
|
||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||
import org.mozilla.fenix.mvi.Reducer
|
||||
import org.mozilla.fenix.mvi.UIComponent
|
||||
import org.mozilla.fenix.mvi.UIComponentViewModelBase
|
||||
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
|
||||
import org.mozilla.fenix.mvi.UIView
|
||||
import org.mozilla.fenix.test.Mockable
|
||||
|
||||
@Mockable
|
||||
class BookmarkComponent(
|
||||
private val container: ViewGroup,
|
||||
bus: ActionBusFactory,
|
||||
viewModelProvider: UIComponentViewModelProvider<BookmarkState, BookmarkChange>
|
||||
) :
|
||||
UIComponent<BookmarkState, BookmarkAction, BookmarkChange>(
|
||||
bus.getManagedEmitter(BookmarkAction::class.java),
|
||||
bus.getSafeManagedObservable(BookmarkChange::class.java),
|
||||
viewModelProvider
|
||||
) {
|
||||
override fun initView(): UIView<BookmarkState, BookmarkAction, BookmarkChange> =
|
||||
BookmarkUIView(container, actionEmitter, changesObservable)
|
||||
|
||||
init {
|
||||
bind()
|
||||
}
|
||||
}
|
||||
|
||||
data class BookmarkState(val tree: BookmarkNode?, val mode: Mode) : ViewState {
|
||||
sealed class Mode {
|
||||
object Normal : Mode()
|
||||
data class Selecting(val selectedItems: Set<BookmarkNode>) : Mode()
|
||||
}
|
||||
}
|
||||
|
||||
sealed class BookmarkAction : Action {
|
||||
data class Open(val item: BookmarkNode) : BookmarkAction()
|
||||
data class Expand(val folder: BookmarkNode) : BookmarkAction()
|
||||
data class Edit(val item: BookmarkNode) : BookmarkAction()
|
||||
data class Copy(val item: BookmarkNode) : BookmarkAction()
|
||||
data class Share(val item: BookmarkNode) : BookmarkAction()
|
||||
data class OpenInNewTab(val item: BookmarkNode) : BookmarkAction()
|
||||
data class OpenInPrivateTab(val item: BookmarkNode) : BookmarkAction()
|
||||
data class Select(val item: BookmarkNode) : BookmarkAction()
|
||||
data class Deselect(val item: BookmarkNode) : BookmarkAction()
|
||||
data class Delete(val item: BookmarkNode) : BookmarkAction()
|
||||
object BackPressed : BookmarkAction()
|
||||
object SwitchMode : BookmarkAction()
|
||||
object DeselectAll : BookmarkAction()
|
||||
}
|
||||
|
||||
sealed class BookmarkChange : Change {
|
||||
data class Change(val tree: BookmarkNode) : BookmarkChange()
|
||||
data class IsSelected(val newlySelectedItem: BookmarkNode) : BookmarkChange()
|
||||
data class IsDeselected(val newlyDeselectedItem: BookmarkNode) : BookmarkChange()
|
||||
object ClearSelection : BookmarkChange()
|
||||
}
|
||||
|
||||
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
|
||||
return children?.contains(item) ?: false
|
||||
}
|
||||
|
||||
class BookmarkViewModel(initialState: BookmarkState) :
|
||||
UIComponentViewModelBase<BookmarkState, BookmarkChange>(initialState, reducer) {
|
||||
|
||||
companion object {
|
||||
fun create() = BookmarkViewModel(BookmarkState(null, BookmarkState.Mode.Normal))
|
||||
|
||||
val reducer: Reducer<BookmarkState, BookmarkChange> = { state, change ->
|
||||
when (change) {
|
||||
is BookmarkChange.Change -> {
|
||||
val mode =
|
||||
if (state.mode is BookmarkState.Mode.Selecting) {
|
||||
val items = state.mode.selectedItems.filter {
|
||||
it in change.tree
|
||||
}.toSet()
|
||||
if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items)
|
||||
} else state.mode
|
||||
state.copy(tree = change.tree, mode = mode)
|
||||
}
|
||||
is BookmarkChange.IsSelected -> {
|
||||
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
|
||||
state.mode.selectedItems + change.newlySelectedItem
|
||||
} else setOf(change.newlySelectedItem)
|
||||
state.copy(mode = BookmarkState.Mode.Selecting(selectedItems))
|
||||
}
|
||||
is BookmarkChange.IsDeselected -> {
|
||||
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
|
||||
state.mode.selectedItems - change.newlyDeselectedItem
|
||||
} else setOf()
|
||||
val mode = if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(
|
||||
selectedItems
|
||||
)
|
||||
state.copy(mode = mode)
|
||||
}
|
||||
is BookmarkChange.ClearSelection -> state.copy(mode = BookmarkState.Mode.Normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
/* 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 androidx.navigation.NavController
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.storage.BookmarkNodeType
|
||||
import org.mozilla.fenix.BrowserDirection
|
||||
import org.mozilla.fenix.BrowsingModeManager
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbarPresenter
|
||||
import org.mozilla.fenix.components.metrics.Event
|
||||
import org.mozilla.fenix.components.metrics.MetricController
|
||||
import org.mozilla.fenix.ext.asActivity
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.copyUrl
|
||||
import org.mozilla.fenix.ext.nav
|
||||
|
||||
/**
|
||||
* Interactor for the Bookmarks screen.
|
||||
* Provides implementations for the BookmarkViewInteractor.
|
||||
*
|
||||
* @property context The current Android Context
|
||||
* @property navController The Android Navigation NavController
|
||||
* @property bookmarkStore The BookmarkStore
|
||||
* @property sharedViewModel The shared ViewModel used between the Bookmarks screens
|
||||
* @property snackbarPresenter A presenter for the FenixSnackBar
|
||||
* @property deleteBookmarkNodes A lambda function for deleting bookmark nodes with undo
|
||||
*/
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
class BookmarkFragmentInteractor(
|
||||
private val context: Context,
|
||||
private val navController: NavController,
|
||||
private val bookmarkStore: BookmarkStore,
|
||||
private val sharedViewModel: BookmarksSharedViewModel,
|
||||
private val snackbarPresenter: FenixSnackbarPresenter,
|
||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit
|
||||
) : BookmarkViewInteractor, SignInInteractor {
|
||||
|
||||
val activity: HomeActivity?
|
||||
get() = context.asActivity() as? HomeActivity
|
||||
val metrics: MetricController
|
||||
get() = context.components.analytics.metrics
|
||||
|
||||
override fun change(node: BookmarkNode) {
|
||||
bookmarkStore.dispatch(BookmarkAction.Change(node))
|
||||
}
|
||||
|
||||
override fun open(item: BookmarkNode) {
|
||||
require(item.type == BookmarkNodeType.ITEM)
|
||||
item.url?.let { url ->
|
||||
activity!!
|
||||
.openToBrowserAndLoad(
|
||||
searchTermOrURL = url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromBookmarks
|
||||
)
|
||||
}
|
||||
metrics.track(Event.OpenedBookmark)
|
||||
}
|
||||
|
||||
override fun expand(folder: BookmarkNode) {
|
||||
require(folder.type == BookmarkNodeType.FOLDER)
|
||||
navController.nav(
|
||||
R.id.bookmarkFragment,
|
||||
BookmarkFragmentDirections.actionBookmarkFragmentSelf(folder.guid)
|
||||
)
|
||||
}
|
||||
|
||||
override fun switchMode(mode: BookmarkState.Mode) {
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun edit(node: BookmarkNode) {
|
||||
navController.nav(
|
||||
R.id.bookmarkFragment,
|
||||
BookmarkFragmentDirections
|
||||
.actionBookmarkFragmentToBookmarkEditFragment(node.guid)
|
||||
)
|
||||
}
|
||||
|
||||
override fun select(node: BookmarkNode) {
|
||||
bookmarkStore.dispatch(BookmarkAction.Select(node))
|
||||
}
|
||||
|
||||
override fun deselect(node: BookmarkNode) {
|
||||
bookmarkStore.dispatch(BookmarkAction.Deselect(node))
|
||||
}
|
||||
|
||||
override fun deselectAll() {
|
||||
bookmarkStore.dispatch(BookmarkAction.DeselectAll)
|
||||
}
|
||||
|
||||
override fun copy(item: BookmarkNode) {
|
||||
require(item.type == BookmarkNodeType.ITEM)
|
||||
item.copyUrl(activity!!)
|
||||
snackbarPresenter.present(context.getString(R.string.url_copied))
|
||||
metrics.track(Event.CopyBookmark)
|
||||
}
|
||||
|
||||
override fun share(item: BookmarkNode) {
|
||||
require(item.type == BookmarkNodeType.ITEM)
|
||||
item.url?.apply {
|
||||
navController.nav(
|
||||
R.id.bookmarkFragment,
|
||||
BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment(
|
||||
url = this,
|
||||
title = item.title
|
||||
)
|
||||
)
|
||||
metrics.track(Event.ShareBookmark)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openInNewTab(item: BookmarkNode) {
|
||||
require(item.type == BookmarkNodeType.ITEM)
|
||||
item.url?.let { url ->
|
||||
activity?.browsingModeManager?.mode =
|
||||
BrowsingModeManager.Mode.Normal
|
||||
activity?.openToBrowserAndLoad(
|
||||
searchTermOrURL = url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromBookmarks
|
||||
)
|
||||
metrics.track(Event.OpenedBookmarkInNewTab)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openInPrivateTab(item: BookmarkNode) {
|
||||
require(item.type == BookmarkNodeType.ITEM)
|
||||
item.url?.let { url ->
|
||||
activity?.browsingModeManager?.mode =
|
||||
BrowsingModeManager.Mode.Private
|
||||
activity?.openToBrowserAndLoad(
|
||||
searchTermOrURL = url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromBookmarks
|
||||
)
|
||||
metrics.track(Event.OpenedBookmarkInPrivateTab)
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(node: BookmarkNode) {
|
||||
val eventType = when (node.type) {
|
||||
BookmarkNodeType.ITEM -> {
|
||||
Event.RemoveBookmark
|
||||
}
|
||||
BookmarkNodeType.FOLDER -> {
|
||||
Event.RemoveBookmarkFolder
|
||||
}
|
||||
BookmarkNodeType.SEPARATOR -> {
|
||||
throw IllegalStateException("Cannot delete separators")
|
||||
}
|
||||
}
|
||||
deleteBookmarkNodes(setOf(node), eventType)
|
||||
}
|
||||
|
||||
override fun deleteMulti(nodes: Set<BookmarkNode>) {
|
||||
deleteBookmarkNodes(nodes, Event.RemoveBookmarks)
|
||||
}
|
||||
|
||||
override fun backPressed() {
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
override fun clickedSignIn() {
|
||||
context.components.services.launchPairingSignIn(context, navController)
|
||||
}
|
||||
|
||||
override fun signedIn() {
|
||||
sharedViewModel.signedIn.postValue(true)
|
||||
}
|
||||
|
||||
override fun signedOut() {
|
||||
sharedViewModel.signedIn.postValue(false)
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/* 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 mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
|
||||
class BookmarkStore(
|
||||
initalState: BookmarkState
|
||||
) : Store<BookmarkState, BookmarkAction>(
|
||||
initalState, ::bookmarkStateReducer
|
||||
)
|
||||
|
||||
/**
|
||||
* The complete state of the bookmarks tree and multi-selection mode
|
||||
* @property tree The current tree of bookmarks, if one is loaded
|
||||
* @property mode The current bookmark multi-selection mode
|
||||
*/
|
||||
data class BookmarkState(val tree: BookmarkNode?, val mode: Mode = Mode.Normal) : State {
|
||||
sealed class Mode {
|
||||
object Normal : Mode()
|
||||
data class Selecting(val selectedItems: Set<BookmarkNode>) : Mode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions to dispatch through the `BookmarkStore` to modify `BookmarkState` through the reducer.
|
||||
*/
|
||||
sealed class BookmarkAction : Action {
|
||||
data class Change(val tree: BookmarkNode) : BookmarkAction()
|
||||
data class Select(val item: BookmarkNode) : BookmarkAction()
|
||||
data class Deselect(val item: BookmarkNode) : BookmarkAction()
|
||||
object DeselectAll : BookmarkAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the bookmarks state from the current state and an action performed on it.
|
||||
* @param state the current bookmarks state
|
||||
* @param action the action to perform
|
||||
* @return the new bookmarks state
|
||||
*/
|
||||
fun bookmarkStateReducer(state: BookmarkState, action: BookmarkAction): BookmarkState {
|
||||
return when (action) {
|
||||
is BookmarkAction.Change -> {
|
||||
val mode =
|
||||
if (state.mode is BookmarkState.Mode.Selecting) {
|
||||
val items = state.mode.selectedItems.filter {
|
||||
it in action.tree
|
||||
}.toSet()
|
||||
if (items.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(items)
|
||||
} else state.mode
|
||||
state.copy(tree = action.tree, mode = mode)
|
||||
}
|
||||
is BookmarkAction.Select -> {
|
||||
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
|
||||
state.mode.selectedItems + action.item
|
||||
} else setOf(action.item)
|
||||
state.copy(mode = BookmarkState.Mode.Selecting(selectedItems))
|
||||
}
|
||||
is BookmarkAction.Deselect -> {
|
||||
val selectedItems = if (state.mode is BookmarkState.Mode.Selecting) {
|
||||
state.mode.selectedItems - action.item
|
||||
} else setOf()
|
||||
val mode =
|
||||
if (selectedItems.isEmpty()) BookmarkState.Mode.Normal else BookmarkState.Mode.Selecting(selectedItems)
|
||||
state.copy(mode = mode)
|
||||
}
|
||||
BookmarkAction.DeselectAll -> {
|
||||
state.copy(mode = BookmarkState.Mode.Normal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
operator fun BookmarkNode.contains(item: BookmarkNode): Boolean {
|
||||
return children?.contains(item) ?: false
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
/* 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.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.functions.Consumer
|
||||
import kotlinx.android.synthetic.main.component_bookmark.view.*
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.support.base.feature.BackHandler
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.getColorIntFromAttr
|
||||
import org.mozilla.fenix.library.LibraryPageUIView
|
||||
|
||||
class BookmarkUIView(
|
||||
container: ViewGroup,
|
||||
actionEmitter: Observer<BookmarkAction>,
|
||||
changesObservable: Observable<BookmarkChange>
|
||||
) :
|
||||
LibraryPageUIView<BookmarkState, BookmarkAction, BookmarkChange>(container, actionEmitter, changesObservable),
|
||||
BackHandler {
|
||||
|
||||
var mode: BookmarkState.Mode = BookmarkState.Mode.Normal
|
||||
private set
|
||||
var tree: BookmarkNode? = null
|
||||
private set
|
||||
|
||||
private var canGoBack = false
|
||||
|
||||
override val view: LinearLayout = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_bookmark, container, true) as LinearLayout
|
||||
|
||||
private val bookmarkAdapter: BookmarkAdapter
|
||||
|
||||
init {
|
||||
view.bookmark_list.apply {
|
||||
bookmarkAdapter = BookmarkAdapter(view.bookmarks_empty_view, actionEmitter)
|
||||
adapter = bookmarkAdapter
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateView() = Consumer<BookmarkState> {
|
||||
canGoBack = !(listOf(null, BookmarkRoot.Root.id).contains(it.tree?.guid))
|
||||
if (it.tree != tree) {
|
||||
tree = it.tree
|
||||
}
|
||||
if (it.mode != mode) {
|
||||
mode = it.mode
|
||||
actionEmitter.onNext(BookmarkAction.SwitchMode)
|
||||
}
|
||||
when (val modeCopy = it.mode) {
|
||||
is BookmarkState.Mode.Normal -> setUIForNormalMode(it.tree)
|
||||
is BookmarkState.Mode.Selecting -> setUIForSelectingMode(it.tree, modeCopy)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return when {
|
||||
mode is BookmarkState.Mode.Selecting -> {
|
||||
actionEmitter.onNext(BookmarkAction.DeselectAll)
|
||||
true
|
||||
}
|
||||
canGoBack -> {
|
||||
actionEmitter.onNext(BookmarkAction.BackPressed)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelected(): Set<BookmarkNode> = bookmarkAdapter.selected
|
||||
|
||||
private fun setUIForSelectingMode(
|
||||
root: BookmarkNode?,
|
||||
mode: BookmarkState.Mode.Selecting
|
||||
) {
|
||||
bookmarkAdapter.updateData(root, mode)
|
||||
activity?.title =
|
||||
context.getString(R.string.bookmarks_multi_select_title, mode.selectedItems.size)
|
||||
setToolbarColors(
|
||||
R.color.white_color,
|
||||
R.attr.accentHighContrast.getColorIntFromAttr(context!!)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setUIForNormalMode(root: BookmarkNode?) {
|
||||
bookmarkAdapter.updateData(root, BookmarkState.Mode.Normal)
|
||||
setTitle(root)
|
||||
setToolbarColors(
|
||||
R.attr.primaryText.getColorIntFromAttr(context!!),
|
||||
R.attr.foundation.getColorIntFromAttr(context)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setTitle(root: BookmarkNode?) {
|
||||
activity?.title = when (root?.guid) {
|
||||
BookmarkRoot.Mobile.id, null -> context.getString(R.string.library_bookmarks)
|
||||
else -> root.title
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,216 @@
|
||||
/* 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.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.component_bookmark.view.*
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.support.base.feature.BackHandler
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.getColorIntFromAttr
|
||||
import org.mozilla.fenix.library.LibraryPageView
|
||||
|
||||
/**
|
||||
* Interface for the Bookmarks view.
|
||||
* This interface is implemented by objects that want to respond to user interaction on the bookmarks management UI.
|
||||
*/
|
||||
@SuppressWarnings("TooManyFunctions")
|
||||
interface BookmarkViewInteractor {
|
||||
|
||||
/**
|
||||
* Swaps the head of the bookmarks tree, replacing it with a new, updated bookmarks tree.
|
||||
*
|
||||
* @param node the head node of the new bookmarks tree
|
||||
*/
|
||||
fun change(node: BookmarkNode)
|
||||
|
||||
/**
|
||||
* Opens a tab for a bookmark item.
|
||||
*
|
||||
* @param item the bookmark item to open
|
||||
*/
|
||||
fun open(item: BookmarkNode)
|
||||
|
||||
/**
|
||||
* Expands a bookmark folder in the bookmarks tree, providing a view of a different folder elsewhere in the tree.
|
||||
*
|
||||
* @param folder the bookmark folder to expand
|
||||
*/
|
||||
fun expand(folder: BookmarkNode)
|
||||
|
||||
/**
|
||||
* Switches the current bookmark multi-selection mode.
|
||||
*
|
||||
* @param mode the multi-select mode to switch to
|
||||
*/
|
||||
fun switchMode(mode: BookmarkState.Mode)
|
||||
|
||||
/**
|
||||
* Opens up an interface to edit a bookmark node.
|
||||
*
|
||||
* @param node the bookmark node to edit
|
||||
*/
|
||||
fun edit(node: BookmarkNode)
|
||||
|
||||
/**
|
||||
* Selects a bookmark node in multi-selection.
|
||||
*
|
||||
* @param node the bookmark node to select
|
||||
*/
|
||||
fun select(node: BookmarkNode)
|
||||
|
||||
/**
|
||||
* De-selects a bookmark node in multi-selection.
|
||||
*
|
||||
* @param node the bookmark node to deselect
|
||||
*/
|
||||
fun deselect(node: BookmarkNode)
|
||||
|
||||
/**
|
||||
* De-selects all bookmark nodes, clearing the multi-selection mode.
|
||||
*
|
||||
*/
|
||||
fun deselectAll()
|
||||
|
||||
/**
|
||||
* Copies the URL of a bookmark item to the copy-paste buffer.
|
||||
*
|
||||
* @param item the bookmark item to copy the URL from
|
||||
*/
|
||||
fun copy(item: BookmarkNode)
|
||||
|
||||
/**
|
||||
* Opens the share sheet for a bookmark item.
|
||||
*
|
||||
* @param item the bookmark item to share
|
||||
*/
|
||||
fun share(item: BookmarkNode)
|
||||
|
||||
/**
|
||||
* Opens a bookmark item in a new tab.
|
||||
*
|
||||
* @param item the bookmark item to open in a new tab
|
||||
*/
|
||||
fun openInNewTab(item: BookmarkNode)
|
||||
|
||||
/**
|
||||
* Opens a bookmark item in a private tab.
|
||||
*
|
||||
* @param item the bookmark item to open in a private tab
|
||||
*/
|
||||
fun openInPrivateTab(item: BookmarkNode)
|
||||
|
||||
/**
|
||||
* Deletes a bookmark node.
|
||||
*
|
||||
* @param node the bookmark node to delete
|
||||
*/
|
||||
fun delete(node: BookmarkNode)
|
||||
|
||||
/**
|
||||
* Deletes a set of bookmark nodes.
|
||||
*
|
||||
* @param nodes the set of bookmark nodes to delete
|
||||
*/
|
||||
fun deleteMulti(nodes: Set<BookmarkNode>)
|
||||
|
||||
/**
|
||||
* Handles back presses for the bookmark screen, so navigation up the tree is possible.
|
||||
*
|
||||
*/
|
||||
fun backPressed()
|
||||
}
|
||||
|
||||
class BookmarkView(
|
||||
private val container: ViewGroup,
|
||||
val interactor: BookmarkViewInteractor
|
||||
) : LibraryPageView(container), LayoutContainer, BackHandler {
|
||||
|
||||
override val containerView: View?
|
||||
get() = container
|
||||
|
||||
var mode: BookmarkState.Mode = BookmarkState.Mode.Normal
|
||||
private set
|
||||
var tree: BookmarkNode? = null
|
||||
private set
|
||||
private var canGoBack = false
|
||||
|
||||
val view: LinearLayout = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_bookmark, container, true) as LinearLayout
|
||||
|
||||
private val bookmarkAdapter: BookmarkAdapter
|
||||
|
||||
init {
|
||||
view.bookmark_list.apply {
|
||||
bookmarkAdapter = BookmarkAdapter(view.bookmarks_empty_view, interactor)
|
||||
adapter = bookmarkAdapter
|
||||
}
|
||||
}
|
||||
|
||||
fun update(state: BookmarkState) {
|
||||
canGoBack = !(listOf(null, BookmarkRoot.Root.id).contains(state.tree?.guid))
|
||||
if (state.tree != tree) {
|
||||
tree = state.tree
|
||||
}
|
||||
if (state.mode != mode) {
|
||||
mode = state.mode
|
||||
interactor.switchMode(mode)
|
||||
}
|
||||
when (val modeCopy = state.mode) {
|
||||
is BookmarkState.Mode.Normal -> setUIForNormalMode(state.tree)
|
||||
is BookmarkState.Mode.Selecting -> setUIForSelectingMode(state.tree, modeCopy)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBackPressed(): Boolean {
|
||||
return when {
|
||||
mode is BookmarkState.Mode.Selecting -> {
|
||||
interactor.deselectAll()
|
||||
true
|
||||
}
|
||||
canGoBack -> {
|
||||
interactor.backPressed()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
fun getSelected(): Set<BookmarkNode> = bookmarkAdapter.selected
|
||||
|
||||
private fun setUIForSelectingMode(
|
||||
root: BookmarkNode?,
|
||||
mode: BookmarkState.Mode.Selecting
|
||||
) {
|
||||
bookmarkAdapter.updateData(root, mode)
|
||||
activity?.title =
|
||||
context.getString(R.string.bookmarks_multi_select_title, mode.selectedItems.size)
|
||||
setToolbarColors(
|
||||
R.color.white_color,
|
||||
R.attr.accentHighContrast.getColorIntFromAttr(context)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setUIForNormalMode(root: BookmarkNode?) {
|
||||
bookmarkAdapter.updateData(root, BookmarkState.Mode.Normal)
|
||||
setTitle(root)
|
||||
setToolbarColors(
|
||||
R.attr.primaryText.getColorIntFromAttr(context),
|
||||
R.attr.foundation.getColorIntFromAttr(context)
|
||||
)
|
||||
}
|
||||
|
||||
private fun setTitle(root: BookmarkNode?) {
|
||||
activity?.title = when (root?.guid) {
|
||||
BookmarkRoot.Mobile.id, null -> context.getString(R.string.library_bookmarks)
|
||||
else -> root.title
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
/* 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.view.ViewGroup
|
||||
import org.mozilla.fenix.mvi.ViewState
|
||||
import org.mozilla.fenix.mvi.Change
|
||||
import org.mozilla.fenix.mvi.Action
|
||||
import org.mozilla.fenix.mvi.ActionBusFactory
|
||||
import org.mozilla.fenix.mvi.Reducer
|
||||
import org.mozilla.fenix.mvi.UIComponent
|
||||
import org.mozilla.fenix.mvi.UIComponentViewModelBase
|
||||
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
|
||||
import org.mozilla.fenix.mvi.UIView
|
||||
|
||||
class SignInComponent(
|
||||
private val container: ViewGroup,
|
||||
bus: ActionBusFactory,
|
||||
viewModelProvider: UIComponentViewModelProvider<SignInState, SignInChange>
|
||||
) : UIComponent<SignInState, SignInAction, SignInChange>(
|
||||
bus.getManagedEmitter(SignInAction::class.java),
|
||||
bus.getSafeManagedObservable(SignInChange::class.java),
|
||||
viewModelProvider
|
||||
) {
|
||||
override fun initView(): UIView<SignInState, SignInAction, SignInChange> =
|
||||
SignInUIView(container, actionEmitter, changesObservable)
|
||||
|
||||
init {
|
||||
bind()
|
||||
}
|
||||
}
|
||||
|
||||
data class SignInState(val signedIn: Boolean) : ViewState
|
||||
|
||||
sealed class SignInAction : Action {
|
||||
object ClickedSignIn : SignInAction()
|
||||
}
|
||||
|
||||
sealed class SignInChange : Change {
|
||||
object SignedIn : SignInChange()
|
||||
object SignedOut : SignInChange()
|
||||
}
|
||||
|
||||
class SignInViewModel(
|
||||
initialState: SignInState
|
||||
) : UIComponentViewModelBase<SignInState, SignInChange>(initialState, reducer) {
|
||||
companion object {
|
||||
val reducer = object : Reducer<SignInState, SignInChange> {
|
||||
override fun invoke(state: SignInState, change: SignInChange): SignInState {
|
||||
return when (change) {
|
||||
SignInChange.SignedIn -> state.copy(signedIn = true)
|
||||
SignInChange.SignedOut -> state.copy(signedIn = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
/* 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.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.functions.Consumer
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.mvi.UIView
|
||||
|
||||
class SignInUIView(
|
||||
container: ViewGroup,
|
||||
actionEmitter: Observer<SignInAction>,
|
||||
changesObservable: Observable<SignInChange>
|
||||
) : UIView<SignInState, SignInAction, SignInChange>(container, actionEmitter, changesObservable) {
|
||||
|
||||
override val view: MaterialButton = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_sign_in, container, true)
|
||||
.findViewById(R.id.bookmark_folders_sign_in)
|
||||
|
||||
init {
|
||||
view.setOnClickListener {
|
||||
actionEmitter.onNext(SignInAction.ClickedSignIn)
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateView() = Consumer<SignInState> {
|
||||
view.visibility = if (it.signedIn) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/* 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.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
interface SignInInteractor {
|
||||
fun clickedSignIn()
|
||||
fun signedIn()
|
||||
fun signedOut()
|
||||
}
|
||||
|
||||
class SignInView(
|
||||
private val container: ViewGroup,
|
||||
private val interactor: SignInInteractor
|
||||
) : LayoutContainer {
|
||||
|
||||
override val containerView: View?
|
||||
get() = container
|
||||
|
||||
val view: MaterialButton = LayoutInflater.from(container.context)
|
||||
.inflate(R.layout.component_sign_in, container, true)
|
||||
.findViewById(R.id.bookmark_folders_sign_in)
|
||||
|
||||
init {
|
||||
view.setOnClickListener {
|
||||
interactor.clickedSignIn()
|
||||
}
|
||||
}
|
||||
|
||||
fun update(signedIn: Boolean) {
|
||||
view.visibility = if (signedIn) View.GONE else View.VISIBLE
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/* 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.selectfolder
|
||||
|
||||
import android.content.Context
|
||||
import androidx.navigation.NavController
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
|
||||
import org.mozilla.fenix.library.bookmarks.SignInInteractor
|
||||
|
||||
class SelectBookmarkFolderInteractor(
|
||||
private val context: Context,
|
||||
private val navController: NavController,
|
||||
private val sharedViewModel: BookmarksSharedViewModel
|
||||
) : SignInInteractor {
|
||||
|
||||
override fun clickedSignIn() {
|
||||
context.components.services.launchPairingSignIn(context, navController)
|
||||
}
|
||||
|
||||
override fun signedIn() {
|
||||
sharedViewModel.signedIn.postValue(true)
|
||||
}
|
||||
|
||||
override fun signedOut() {
|
||||
sharedViewModel.signedIn.postValue(false)
|
||||
}
|
||||
}
|
@ -0,0 +1,291 @@
|
||||
/* 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.spyk
|
||||
import io.mockk.verify
|
||||
import io.mockk.verifyOrder
|
||||
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.FenixApplication
|
||||
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.components.metrics.MetricController
|
||||
import org.mozilla.fenix.ext.asActivity
|
||||
import org.mozilla.fenix.ext.components
|
||||
|
||||
class BookmarkFragmentInteractorTest {
|
||||
|
||||
private lateinit var interactor: BookmarkFragmentInteractor
|
||||
|
||||
private val context: Context = mockk(relaxed = true)
|
||||
private val navController: NavController = mockk(relaxed = true)
|
||||
private val bookmarkStore = spyk(BookmarkStore(BookmarkState(null)))
|
||||
private val sharedViewModel: BookmarksSharedViewModel = mockk(relaxed = true)
|
||||
private val snackbarPresenter: FenixSnackbarPresenter = mockk(relaxed = true)
|
||||
private val deleteBookmarkNodes: (Set<BookmarkNode>, Event) -> Unit = mockk(relaxed = true)
|
||||
|
||||
private val applicationContext: FenixApplication = mockk(relaxed = true)
|
||||
private val homeActivity: HomeActivity = mockk(relaxed = true)
|
||||
private val metrics: MetricController = mockk(relaxed = true)
|
||||
|
||||
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 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, separator, childItem, subfolder)
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic(
|
||||
"org.mozilla.fenix.ext.ContextKt",
|
||||
"androidx.core.content.ContextCompat",
|
||||
"android.content.ClipData"
|
||||
)
|
||||
every { any<Context>().asActivity() } returns homeActivity
|
||||
every { context.applicationContext } returns applicationContext
|
||||
every { applicationContext.components.analytics.metrics } returns metrics
|
||||
every { navController.currentDestination } returns NavDestination("").apply { id = R.id.bookmarkFragment }
|
||||
every { bookmarkStore.dispatch(any()) } returns mockk()
|
||||
|
||||
interactor =
|
||||
BookmarkFragmentInteractor(
|
||||
context,
|
||||
navController,
|
||||
bookmarkStore,
|
||||
sharedViewModel,
|
||||
snackbarPresenter,
|
||||
deleteBookmarkNodes
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `update bookmarks tree`() {
|
||||
interactor.change(tree)
|
||||
|
||||
verify {
|
||||
bookmarkStore.dispatch(BookmarkAction.Change(tree))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open a bookmark item`() {
|
||||
interactor.open(item)
|
||||
|
||||
val url = item.url!!
|
||||
verifyOrder {
|
||||
homeActivity.openToBrowserAndLoad(
|
||||
searchTermOrURL = url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromBookmarks
|
||||
)
|
||||
}
|
||||
metrics.track(Event.OpenedBookmark)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expand a level of bookmarks`() {
|
||||
interactor.expand(tree)
|
||||
|
||||
verify {
|
||||
navController.navigate(BookmarkFragmentDirections.actionBookmarkFragmentSelf(tree.guid))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `switch between bookmark selection modes`() {
|
||||
interactor.switchMode(BookmarkState.Mode.Normal)
|
||||
|
||||
verify {
|
||||
homeActivity.invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `press the edit bookmark button`() {
|
||||
interactor.edit(item)
|
||||
|
||||
verify {
|
||||
navController.navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(item.guid))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `select a bookmark item`() {
|
||||
interactor.select(item)
|
||||
|
||||
verify {
|
||||
bookmarkStore.dispatch(BookmarkAction.Select(item))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deselect a bookmark item`() {
|
||||
interactor.deselect(item)
|
||||
|
||||
verify {
|
||||
bookmarkStore.dispatch(BookmarkAction.Deselect(item))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deselectAll bookmark items`() {
|
||||
interactor.deselectAll()
|
||||
|
||||
verify {
|
||||
bookmarkStore.dispatch(BookmarkAction.DeselectAll)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `copy a bookmark item`() {
|
||||
val clipboardManager: ClipboardManager = mockk(relaxed = true)
|
||||
every { any<Context>().getSystemService<ClipboardManager>() } returns clipboardManager
|
||||
every { ClipData.newPlainText(any(), any()) } returns mockk(relaxed = true)
|
||||
|
||||
interactor.copy(item)
|
||||
|
||||
verify {
|
||||
metrics.track(Event.CopyBookmark)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `share a bookmark item`() {
|
||||
interactor.share(item)
|
||||
|
||||
verifyOrder {
|
||||
navController.navigate(
|
||||
BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment(
|
||||
item.url,
|
||||
item.title
|
||||
)
|
||||
)
|
||||
metrics.track(Event.ShareBookmark)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open a bookmark item in a new tab`() {
|
||||
interactor.openInNewTab(item)
|
||||
|
||||
val url = item.url!!
|
||||
verifyOrder {
|
||||
homeActivity.openToBrowserAndLoad(
|
||||
searchTermOrURL = url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromBookmarks
|
||||
)
|
||||
metrics.track(Event.OpenedBookmarkInNewTab)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open a bookmark item in a private tab`() {
|
||||
interactor.openInPrivateTab(item)
|
||||
|
||||
val url = item.url!!
|
||||
verifyOrder {
|
||||
homeActivity.openToBrowserAndLoad(
|
||||
searchTermOrURL = url,
|
||||
newTab = true,
|
||||
from = BrowserDirection.FromBookmarks
|
||||
)
|
||||
metrics.track(Event.OpenedBookmarkInPrivateTab)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete a bookmark item`() {
|
||||
interactor.delete(item)
|
||||
|
||||
verify {
|
||||
deleteBookmarkNodes(setOf(item), Event.RemoveBookmark)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete a bookmark folder`() {
|
||||
interactor.delete(subfolder)
|
||||
|
||||
verify {
|
||||
deleteBookmarkNodes(setOf(subfolder), Event.RemoveBookmarkFolder)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete multiple bookmarks`() {
|
||||
interactor.deleteMulti(setOf(item, subfolder))
|
||||
|
||||
verify {
|
||||
deleteBookmarkNodes(setOf(item, subfolder), Event.RemoveBookmarks)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `press the back button`() {
|
||||
interactor.backPressed()
|
||||
|
||||
verify {
|
||||
navController.popBackStack()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicked sign in on bookmarks screen`() {
|
||||
val services: Services = mockk(relaxed = true)
|
||||
every { context.components.services } returns services
|
||||
|
||||
interactor.clickedSignIn()
|
||||
|
||||
verify {
|
||||
context.components.services
|
||||
services.launchPairingSignIn(context, navController)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `got signed in signal on bookmarks screen`() {
|
||||
interactor.signedIn()
|
||||
|
||||
verify {
|
||||
sharedViewModel.signedIn.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `got signed out signal on bookmarks screen`() {
|
||||
interactor.signedOut()
|
||||
|
||||
verify {
|
||||
sharedViewModel.signedIn.postValue(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
/* 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 androidx.fragment.app.testing.FragmentScenario
|
||||
import androidx.fragment.app.testing.launchFragmentInContainer
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.Navigation
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import mozilla.appservices.places.BookmarkRoot
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.TestApplication
|
||||
import org.mozilla.fenix.TestUtils
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(application = TestApplication::class)
|
||||
class BookmarkFragmentTest {
|
||||
|
||||
private lateinit var scenario: FragmentScenario<BookmarkFragment>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
TestUtils.setRxSchedulers()
|
||||
|
||||
val mockNavController = mockk<NavController>()
|
||||
every { mockNavController.addOnDestinationChangedListener(any()) } just Runs
|
||||
|
||||
val args = BookmarkFragmentArgs(BookmarkRoot.Mobile.id).toBundle()
|
||||
scenario =
|
||||
launchFragmentInContainer<BookmarkFragment>(fragmentArgs = args, themeResId = R.style.NormalTheme) {
|
||||
BookmarkFragment().also { fragment ->
|
||||
fragment.viewLifecycleOwnerLiveData.observeForever {
|
||||
if (it != null) {
|
||||
Navigation.setViewNavController(fragment.requireView(), mockNavController)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test initial bookmarks fragment ui`() {
|
||||
scenario.onFragment { fragment ->
|
||||
assertEquals(fragment.getString(R.string.library_bookmarks), fragment.activity?.title)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,161 @@
|
||||
/* 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 assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import mozilla.components.concept.storage.BookmarkNode
|
||||
import mozilla.components.concept.storage.BookmarkNodeType
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Test
|
||||
|
||||
class BookmarkStoreTest {
|
||||
|
||||
@Test
|
||||
fun `change the tree of bookmarks starting from an empty tree`() = runBlocking {
|
||||
val initialState = BookmarkState(null)
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
assertThat(BookmarkState(null, BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||
|
||||
store.dispatch(BookmarkAction.Change(tree)).join()
|
||||
|
||||
assertThat(initialState.copy(tree = tree)).isEqualTo(store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change the tree of bookmarks starting from an existing tree`() = runBlocking {
|
||||
val initialState = BookmarkState(tree)
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
assertThat(BookmarkState(tree, BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||
|
||||
store.dispatch(BookmarkAction.Change(newTree)).join()
|
||||
|
||||
assertThat(initialState.copy(tree = newTree)).isEqualTo(store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `change the tree of bookmarks to the same value`() = runBlocking {
|
||||
val initialState = BookmarkState(tree)
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
assertThat(BookmarkState(tree, BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||
|
||||
store.dispatch(BookmarkAction.Change(tree)).join()
|
||||
|
||||
assertSame(initialState, store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ensure selected items remain selected after a tree change`() = runBlocking {
|
||||
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(item, subfolder)))
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
store.dispatch(BookmarkAction.Change(newTree)).join()
|
||||
|
||||
assertThat(BookmarkState(newTree, BookmarkState.Mode.Selecting(setOf(subfolder)))).isEqualTo(store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `select and deselect bookmarks changes the mode`() = runBlocking {
|
||||
val initialState = BookmarkState(tree)
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
store.dispatch(BookmarkAction.Select(childItem)).join()
|
||||
|
||||
assertThat(BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(childItem)))).isEqualTo(store.state)
|
||||
|
||||
store.dispatch(BookmarkAction.Deselect(childItem)).join()
|
||||
|
||||
assertThat(BookmarkState(tree, BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `selecting the same item twice does nothing`() = runBlocking {
|
||||
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(item, subfolder)))
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
store.dispatch(BookmarkAction.Select(item)).join()
|
||||
|
||||
assertSame(initialState, store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deselecting an unselected bookmark does nothing`() = runBlocking {
|
||||
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(childItem)))
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
store.dispatch(BookmarkAction.Deselect(item)).join()
|
||||
|
||||
assertSame(initialState, store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deselecting while not in selecting mode does nothing`() = runBlocking {
|
||||
val initialState = BookmarkState(tree, BookmarkState.Mode.Normal)
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
store.dispatch(BookmarkAction.Deselect(item)).join()
|
||||
|
||||
assertSame(initialState, store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deselect all bookmarks changes the mode`() = runBlocking {
|
||||
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(item, childItem)))
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
store.dispatch(BookmarkAction.DeselectAll).join()
|
||||
|
||||
assertThat(initialState.copy(mode = BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deselect all bookmarks when none are selected`() = runBlocking {
|
||||
val initialState = BookmarkState(tree, BookmarkState.Mode.Normal)
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
store.dispatch(BookmarkAction.DeselectAll)
|
||||
|
||||
assertSame(initialState, store.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleting bookmarks changes the mode`() = runBlocking {
|
||||
val initialState = BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(item, childItem)))
|
||||
val store = BookmarkStore(initialState)
|
||||
|
||||
store.dispatch(BookmarkAction.Change(newTree)).join()
|
||||
|
||||
assertThat(initialState.copy(tree = newTree, mode = BookmarkState.Mode.Normal)).isEqualTo(store.state)
|
||||
}
|
||||
|
||||
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 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, separator, childItem, subfolder)
|
||||
)
|
||||
private val newTree = BookmarkNode(
|
||||
BookmarkNodeType.FOLDER,
|
||||
"123",
|
||||
null,
|
||||
0,
|
||||
"Mobile",
|
||||
null,
|
||||
listOf(separator, subfolder)
|
||||
)
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
/* 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 io.mockk.MockKAnnotations
|
||||
import io.reactivex.Observer
|
||||
import io.reactivex.observers.TestObserver
|
||||
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.TestUtils
|
||||
import org.mozilla.fenix.TestUtils.bus
|
||||
import org.mozilla.fenix.ext.minus
|
||||
import org.mozilla.fenix.mvi.getManagedEmitter
|
||||
|
||||
class BookmarkViewModelTest {
|
||||
|
||||
private lateinit var bookmarkViewModel: BookmarkViewModel
|
||||
private lateinit var bookmarkObserver: TestObserver<BookmarkState>
|
||||
private lateinit var emitter: Observer<BookmarkChange>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockKAnnotations.init(this)
|
||||
TestUtils.setRxSchedulers()
|
||||
|
||||
bookmarkViewModel = BookmarkViewModel.create()
|
||||
bookmarkObserver = bookmarkViewModel.state.test()
|
||||
bus.getSafeManagedObservable(BookmarkChange::class.java)
|
||||
.subscribe(bookmarkViewModel.changes::onNext)
|
||||
emitter = TestUtils.owner.getManagedEmitter()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `select and deselect a bookmark`() {
|
||||
val itemToSelect = BookmarkNode(BookmarkNodeType.ITEM, "234", "123", 0, "Mozilla", "http://mozilla.org", null)
|
||||
val separator = BookmarkNode(BookmarkNodeType.SEPARATOR, "345", "123", 1, null, null, null)
|
||||
val innerFolder = BookmarkNode(BookmarkNodeType.FOLDER, "456", "123", 2, "Web Browsers", null, null)
|
||||
val tree = BookmarkNode(
|
||||
BookmarkNodeType.FOLDER, "123", BookmarkRoot.Mobile.id, 0, "Best Sites", null,
|
||||
listOf(itemToSelect, separator, innerFolder)
|
||||
)
|
||||
|
||||
emitter.onNext(BookmarkChange.Change(tree))
|
||||
emitter.onNext(BookmarkChange.IsSelected(itemToSelect))
|
||||
emitter.onNext(BookmarkChange.IsDeselected(itemToSelect))
|
||||
|
||||
bookmarkObserver.assertSubscribed().awaitCount(2).assertNoErrors()
|
||||
.assertValues(
|
||||
BookmarkState(null, BookmarkState.Mode.Normal),
|
||||
BookmarkState(tree, BookmarkState.Mode.Normal),
|
||||
BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(itemToSelect))),
|
||||
BookmarkState(tree, BookmarkState.Mode.Normal)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `select and delete a bookmark`() {
|
||||
val itemToSelect = BookmarkNode(BookmarkNodeType.ITEM, "234", "123", 0, "Mozilla", "http://mozilla.org", null)
|
||||
val separator = BookmarkNode(BookmarkNodeType.SEPARATOR, "345", "123", 1, null, null, null)
|
||||
val innerFolder = BookmarkNode(BookmarkNodeType.FOLDER, "456", "123", 2, "Web Browsers", null, null)
|
||||
val tree = BookmarkNode(
|
||||
BookmarkNodeType.FOLDER, "123", BookmarkRoot.Mobile.id, 0, "Best Sites", null,
|
||||
listOf(itemToSelect, separator, innerFolder)
|
||||
)
|
||||
|
||||
emitter.onNext(BookmarkChange.Change(tree))
|
||||
emitter.onNext(BookmarkChange.IsSelected(itemToSelect))
|
||||
emitter.onNext(BookmarkChange.Change(tree - itemToSelect.guid))
|
||||
|
||||
bookmarkObserver.assertSubscribed().awaitCount(2).assertNoErrors()
|
||||
.assertValues(
|
||||
BookmarkState(null, BookmarkState.Mode.Normal),
|
||||
BookmarkState(tree, BookmarkState.Mode.Normal),
|
||||
BookmarkState(tree, BookmarkState.Mode.Selecting(setOf(itemToSelect))),
|
||||
BookmarkState(tree - itemToSelect.guid, BookmarkState.Mode.Normal)
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue