For #3987 - Convert History to Lib-State and add tests

nightly-build-test
Emily Kager 5 years ago committed by Emily Kager
parent 9ab67557cf
commit ae3d187909

@ -10,7 +10,6 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.viewholders.HistoryDeleteButtonViewHolder import org.mozilla.fenix.library.history.viewholders.HistoryDeleteButtonViewHolder
import org.mozilla.fenix.library.history.viewholders.HistoryHeaderViewHolder import org.mozilla.fenix.library.history.viewholders.HistoryHeaderViewHolder
@ -94,9 +93,8 @@ private class HistoryList(val history: List<HistoryItem>) {
} }
} }
class HistoryAdapter( class HistoryAdapter(private val historyInteractor: HistoryInteractor) :
private val actionEmitter: Observer<HistoryAction> AdapterWithJob<RecyclerView.ViewHolder>() {
) : AdapterWithJob<RecyclerView.ViewHolder>() {
private var historyList: HistoryList = HistoryList(emptyList()) private var historyList: HistoryList = HistoryList(emptyList())
private var mode: HistoryState.Mode = HistoryState.Mode.Normal private var mode: HistoryState.Mode = HistoryState.Mode.Normal
var selected = listOf<HistoryItem>() var selected = listOf<HistoryItem>()
@ -160,9 +158,16 @@ class HistoryAdapter(
val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false)
return when (viewType) { return when (viewType) {
HistoryDeleteButtonViewHolder.LAYOUT_ID -> HistoryDeleteButtonViewHolder(view, actionEmitter) HistoryDeleteButtonViewHolder.LAYOUT_ID -> HistoryDeleteButtonViewHolder(
view,
historyInteractor
)
HistoryHeaderViewHolder.LAYOUT_ID -> HistoryHeaderViewHolder(view) HistoryHeaderViewHolder.LAYOUT_ID -> HistoryHeaderViewHolder(view)
HistoryListItemViewHolder.LAYOUT_ID -> HistoryListItemViewHolder(view, actionEmitter, adapterJob) HistoryListItemViewHolder.LAYOUT_ID -> HistoryListItemViewHolder(
view,
historyInteractor,
adapterJob
)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }

@ -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.history
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.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
import org.mozilla.fenix.test.Mockable
data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long)
@Mockable
class HistoryComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
viewModelProvider: UIComponentViewModelProvider<HistoryState, HistoryChange>
) :
UIComponent<HistoryState, HistoryAction, HistoryChange>(
bus.getManagedEmitter(HistoryAction::class.java),
bus.getSafeManagedObservable(HistoryChange::class.java),
viewModelProvider
) {
override fun initView() = HistoryUIView(container, actionEmitter, changesObservable)
init {
bind()
}
}
data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : ViewState {
sealed class Mode {
object Normal : Mode()
data class Editing(val selectedItems: List<HistoryItem>) : Mode()
object Deleting : Mode()
}
}
sealed class HistoryAction : Action {
data class Open(val item: HistoryItem) : HistoryAction()
data class EnterEditMode(val item: HistoryItem) : HistoryAction()
object BackPressed : HistoryAction()
data class AddItemForRemoval(val item: HistoryItem) : HistoryAction()
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryAction()
object SwitchMode : HistoryAction()
sealed class Delete : HistoryAction() {
object All : Delete()
data class One(val item: HistoryItem) : Delete()
data class Some(val items: List<HistoryItem>) : Delete()
}
}
sealed class HistoryChange : Change {
data class Change(val list: List<HistoryItem>) : HistoryChange()
data class EnterEditMode(val item: HistoryItem) : HistoryChange()
object ExitEditMode : HistoryChange()
data class AddItemForRemoval(val item: HistoryItem) : HistoryChange()
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryChange()
object EnterDeletionMode : HistoryChange()
object ExitDeletionMode : HistoryChange()
}
class HistoryViewModel(
initialState: HistoryState
) : UIComponentViewModelBase<HistoryState, HistoryChange>(initialState, reducer) {
companion object {
fun create() = HistoryViewModel(HistoryState(emptyList(), HistoryState.Mode.Normal))
val reducer: (HistoryState, HistoryChange) -> HistoryState = { state, change ->
when (change) {
is HistoryChange.Change -> state.copy(mode = HistoryState.Mode.Normal, items = change.list)
is HistoryChange.EnterEditMode -> state.copy(mode = HistoryState.Mode.Editing(listOf(change.item)))
is HistoryChange.AddItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems + listOf(change.item)
state.copy(mode = mode.copy(selectedItems = items))
} else {
state
}
}
is HistoryChange.RemoveItemForRemoval -> {
var mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems.filter { it.id != change.item.id }
mode = if (items.isEmpty()) HistoryState.Mode.Normal else HistoryState.Mode.Editing(items)
state.copy(mode = mode)
} else {
state
}
}
is HistoryChange.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal)
is HistoryChange.EnterDeletionMode -> state.copy(mode = HistoryState.Mode.Deleting)
is HistoryChange.ExitDeletionMode -> state.copy(mode = HistoryState.Mode.Normal)
}
}
}
}

@ -19,6 +19,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.whenStarted
import androidx.navigation.Navigation import androidx.navigation.Navigation
import kotlinx.android.synthetic.main.fragment_history.view.* import kotlinx.android.synthetic.main.fragment_history.view.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -26,46 +27,56 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.concept.storage.VisitType import mozilla.components.concept.storage.VisitType
import mozilla.components.lib.state.ext.observe
import mozilla.components.support.base.feature.BackHandler import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BrowsingModeManager import org.mozilla.fenix.BrowsingModeManager
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getHostFromUrl import org.mozilla.fenix.ext.getHostFromUrl
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
import org.mozilla.fenix.mvi.getManagedEmitter
import org.mozilla.fenix.share.ShareTab import org.mozilla.fenix.share.ShareTab
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@SuppressWarnings("TooManyFunctions") @SuppressWarnings("TooManyFunctions")
class HistoryFragment : Fragment(), BackHandler { class HistoryFragment : Fragment(), BackHandler {
private lateinit var historyStore: HistoryStore
private lateinit var historyComponent: HistoryComponent private lateinit var historyView: HistoryView
private val navigation by lazy { Navigation.findNavController(requireView()) } private lateinit var historyInteractor: HistoryInteractor
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? = inflater ): View? {
.inflate(R.layout.fragment_history, container, false).also { view -> val view = inflater.inflate(R.layout.fragment_history, container, false)
historyComponent = HistoryComponent( historyStore = StoreProvider.get(
view.history_layout, this,
ActionBusFactory.get(this), HistoryStore(
FenixViewModelProvider.create( HistoryState(
this, items = listOf(), mode = HistoryState.Mode.Normal
HistoryViewModel::class.java,
HistoryViewModel.Companion::create
) )
) )
} )
historyInteractor = HistoryInteractor(
historyStore,
::openItem,
::displayDeleteAllDialog,
::invalidateOptionsMenu,
::deleteHistoryItems
)
historyView = HistoryView(view.history_layout, historyInteractor)
return view
}
private fun invalidateOptionsMenu() {
activity?.invalidateOptionsMenu()
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -74,16 +85,28 @@ class HistoryFragment : Fragment(), BackHandler {
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
fun deleteHistoryItems(items: List<HistoryItem>) {
lifecycleScope.launch {
val storage = context?.components?.core?.historyStorage
for (item in items) {
storage?.deleteVisit(item.url, item.visitedAt)
}
reloadData()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch { reloadData() } historyStore.observe(view) {
} viewLifecycleOwner.lifecycleScope.launch {
whenStarted {
historyView.update(it)
}
}
}
override fun onStart() { lifecycleScope.launch { reloadData() }
super.onStart()
getAutoDisposeObservable<HistoryAction>()
.subscribe(this::handleNewHistoryAction)
} }
override fun onResume() { override fun onResume() {
@ -95,7 +118,7 @@ class HistoryFragment : Fragment(), BackHandler {
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
val mode = (historyComponent.uiView as HistoryUIView).mode val mode = historyStore.state.mode
when (mode) { when (mode) {
HistoryState.Mode.Normal -> HistoryState.Mode.Normal ->
R.menu.library_menu R.menu.library_menu
@ -117,7 +140,8 @@ class HistoryFragment : Fragment(), BackHandler {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.share_history_multi_select -> { R.id.share_history_multi_select -> {
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected() val selectedHistory =
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
when { when {
selectedHistory.size == 1 -> selectedHistory.size == 1 ->
share(selectedHistory.first().url) share(selectedHistory.first().url)
@ -135,7 +159,8 @@ class HistoryFragment : Fragment(), BackHandler {
} }
R.id.delete_history_multi_select -> { R.id.delete_history_multi_select -> {
val components = context?.applicationContext?.components!! val components = context?.applicationContext?.components!!
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected() val selectedHistory =
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
lifecycleScope.launch(Main) { lifecycleScope.launch(Main) {
deleteSelectedHistory(selectedHistory, components) deleteSelectedHistory(selectedHistory, components)
@ -144,7 +169,8 @@ class HistoryFragment : Fragment(), BackHandler {
true true
} }
R.id.open_history_in_new_tabs_multi_select -> { R.id.open_history_in_new_tabs_multi_select -> {
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected() val selectedHistory =
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
requireComponents.useCases.tabsUseCases.addTab.let { useCase -> requireComponents.useCases.tabsUseCases.addTab.let { useCase ->
for (selectedItem in selectedHistory) { for (selectedItem in selectedHistory) {
useCase.invoke(selectedItem.url) useCase.invoke(selectedItem.url)
@ -155,11 +181,15 @@ class HistoryFragment : Fragment(), BackHandler {
browsingModeManager.mode = BrowsingModeManager.Mode.Normal browsingModeManager.mode = BrowsingModeManager.Mode.Normal
supportActionBar?.hide() supportActionBar?.hide()
} }
nav(R.id.historyFragment, HistoryFragmentDirections.actionHistoryFragmentToHomeFragment()) nav(
R.id.historyFragment,
HistoryFragmentDirections.actionHistoryFragmentToHomeFragment()
)
true true
} }
R.id.open_history_in_private_tabs_multi_select -> { R.id.open_history_in_private_tabs_multi_select -> {
val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected() val selectedHistory =
(historyStore.state.mode as? HistoryState.Mode.Editing)?.selectedItems ?: listOf()
requireComponents.useCases.tabsUseCases.addPrivateTab.let { useCase -> requireComponents.useCases.tabsUseCases.addPrivateTab.let { useCase ->
for (selectedItem in selectedHistory) { for (selectedItem in selectedHistory) {
useCase.invoke(selectedItem.url) useCase.invoke(selectedItem.url)
@ -170,47 +200,18 @@ class HistoryFragment : Fragment(), BackHandler {
browsingModeManager.mode = BrowsingModeManager.Mode.Private browsingModeManager.mode = BrowsingModeManager.Mode.Private
supportActionBar?.hide() supportActionBar?.hide()
} }
nav(R.id.historyFragment, HistoryFragmentDirections.actionHistoryFragmentToHomeFragment()) nav(
R.id.historyFragment,
HistoryFragmentDirections.actionHistoryFragmentToHomeFragment()
)
true true
} }
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
override fun onBackPressed(): Boolean = (historyComponent.uiView as HistoryUIView).onBackPressed() override fun onBackPressed(): Boolean = historyView.onBackPressed()
private fun handleNewHistoryAction(action: HistoryAction) {
when (action) {
is HistoryAction.Open ->
openItem(action.item)
is HistoryAction.EnterEditMode ->
emitChange { HistoryChange.EnterEditMode(action.item) }
is HistoryAction.AddItemForRemoval ->
emitChange { HistoryChange.AddItemForRemoval(action.item) }
is HistoryAction.RemoveItemForRemoval ->
emitChange { HistoryChange.RemoveItemForRemoval(action.item) }
is HistoryAction.BackPressed ->
emitChange { HistoryChange.ExitEditMode }
is HistoryAction.Delete.All ->
displayDeleteAllDialog()
is HistoryAction.Delete.One -> lifecycleScope.launch {
requireComponents.core
.historyStorage
.deleteVisit(action.item.url, action.item.visitedAt)
reloadData()
}
is HistoryAction.Delete.Some -> lifecycleScope.launch {
val storage = requireComponents.core.historyStorage
for (item in action.items) {
storage.deleteVisit(item.url, item.visitedAt)
}
reloadData()
}
is HistoryAction.SwitchMode ->
activity?.invalidateOptionsMenu()
}
}
private fun openItem(item: HistoryItem) { fun openItem(item: HistoryItem) {
requireComponents.analytics.metrics.track(Event.HistoryItemOpened) requireComponents.analytics.metrics.track(Event.HistoryItemOpened)
(activity as HomeActivity).openToBrowserAndLoad( (activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = item.url, searchTermOrURL = item.url,
@ -219,7 +220,7 @@ class HistoryFragment : Fragment(), BackHandler {
) )
} }
private fun displayDeleteAllDialog() { fun displayDeleteAllDialog() {
activity?.let { activity -> activity?.let { activity ->
AlertDialog.Builder(activity).apply { AlertDialog.Builder(activity).apply {
setMessage(R.string.history_delete_all_dialog) setMessage(R.string.history_delete_all_dialog)
@ -227,13 +228,13 @@ class HistoryFragment : Fragment(), BackHandler {
dialog.cancel() dialog.cancel()
} }
setPositiveButton(R.string.history_clear_dialog) { dialog: DialogInterface, _ -> setPositiveButton(R.string.history_clear_dialog) { dialog: DialogInterface, _ ->
emitChange { HistoryChange.EnterDeletionMode } historyStore.dispatch(HistoryAction.EnterDeletionMode)
lifecycleScope.launch { lifecycleScope.launch {
requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved) requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved)
requireComponents.core.historyStorage.deleteEverything() requireComponents.core.historyStorage.deleteEverything()
reloadData() reloadData()
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
emitChange { HistoryChange.ExitDeletionMode } historyStore.dispatch(HistoryAction.ExitDeletionMode)
} }
} }
@ -275,7 +276,7 @@ class HistoryFragment : Fragment(), BackHandler {
.toList() .toList()
withContext(Main) { withContext(Main) {
emitChange { HistoryChange.Change(items) } historyStore.dispatch(HistoryAction.Change(items))
} }
} }
@ -300,10 +301,6 @@ class HistoryFragment : Fragment(), BackHandler {
nav(R.id.historyFragment, directions) nav(R.id.historyFragment, directions)
} }
private inline fun emitChange(producer: () -> HistoryChange) {
getManagedEmitter<HistoryChange>().onNext(producer())
}
companion object { companion object {
private const val HISTORY_TIME_DAYS = 3L private const val HISTORY_TIME_DAYS = 3L
} }

@ -0,0 +1,53 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.library.history
/**
* Interactor for the history screen
* Provides implementations for the HistoryViewInteractor
*/
class HistoryInteractor(
private val store: HistoryStore,
private val openToBrowser: (item: HistoryItem) -> Unit,
private val displayDeleteAll: () -> Unit,
private val invalidateOptionsMenu: () -> Unit,
private val deleteHistoryItems: (List<HistoryItem>) -> Unit
) : HistoryViewInteractor {
override fun onHistoryItemOpened(item: HistoryItem) {
openToBrowser(item)
}
override fun onEnterEditMode(selectedItem: HistoryItem) {
store.dispatch(HistoryAction.EnterEditMode(selectedItem))
}
override fun onBackPressed() {
store.dispatch(HistoryAction.ExitEditMode)
}
override fun onItemAddedForRemoval(item: HistoryItem) {
store.dispatch(HistoryAction.AddItemForRemoval(item))
}
override fun onItemRemovedForRemoval(item: HistoryItem) {
store.dispatch(HistoryAction.RemoveItemForRemoval(item))
}
override fun onModeSwitched() {
invalidateOptionsMenu.invoke()
}
override fun onDeleteAll() {
displayDeleteAll.invoke()
}
override fun onDeleteOne(item: HistoryItem) {
deleteHistoryItems.invoke(listOf(item))
}
override fun onDeleteSome(items: List<HistoryItem>) {
deleteHistoryItems.invoke(items)
}
}

@ -0,0 +1,88 @@
/* 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.history
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* Class representing a history entry
* @property id Unique id of the history item
* @property title Title of the history item
* @property url URL of the history item
* @property visitedAt Timestamp of when this history item was visited
*/
data class HistoryItem(val id: Int, val title: String, val url: String, val visitedAt: Long)
/**
* The [Store] for holding the [HistoryState] and applying [HistoryAction]s.
*/
class HistoryStore(initialState: HistoryState) :
Store<HistoryState, HistoryAction>(initialState, ::historyStateReducer)
/**
* Actions to dispatch through the `HistoryStore` to modify `HistoryState` through the reducer.
*/
sealed class HistoryAction : Action {
data class Change(val list: List<HistoryItem>) : HistoryAction()
data class EnterEditMode(val item: HistoryItem) : HistoryAction()
object ExitEditMode : HistoryAction()
data class AddItemForRemoval(val item: HistoryItem) : HistoryAction()
data class RemoveItemForRemoval(val item: HistoryItem) : HistoryAction()
object EnterDeletionMode : HistoryAction()
object ExitDeletionMode : HistoryAction()
}
/**
* The state for the History Screen
* @property items List of HistoryItem to display
* @property mode Current Mode of History
*/
data class HistoryState(val items: List<HistoryItem>, val mode: Mode) : State {
sealed class Mode {
object Normal : Mode()
data class Editing(val selectedItems: List<HistoryItem>) : Mode()
object Deleting : Mode()
}
}
/**
* The HistoryState Reducer.
*/
fun historyStateReducer(state: HistoryState, action: HistoryAction): HistoryState {
return when (action) {
is HistoryAction.Change -> state.copy(mode = HistoryState.Mode.Normal, items = action.list)
is HistoryAction.EnterEditMode -> state.copy(
mode = HistoryState.Mode.Editing(listOf(action.item))
)
is HistoryAction.AddItemForRemoval -> {
val mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems + listOf(action.item)
state.copy(mode = HistoryState.Mode.Editing(items))
} else {
state
}
}
is HistoryAction.RemoveItemForRemoval -> {
var mode = state.mode
if (mode is HistoryState.Mode.Editing) {
val items = mode.selectedItems.filter { it.id != action.item.id }
mode = if (items.isEmpty()) HistoryState.Mode.Normal else HistoryState.Mode.Editing(
items
)
state.copy(mode = mode)
} else {
state
}
}
is HistoryAction.ExitEditMode -> state.copy(mode = HistoryState.Mode.Normal)
is HistoryAction.EnterDeletionMode -> state.copy(mode = HistoryState.Mode.Deleting)
is HistoryAction.ExitDeletionMode -> state.copy(mode = HistoryState.Mode.Normal)
}
}

@ -1,102 +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.history
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import io.reactivex.Observable
import io.reactivex.Observer
import io.reactivex.functions.Consumer
import kotlinx.android.synthetic.main.component_history.*
import kotlinx.android.synthetic.main.component_history.view.*
import kotlinx.android.synthetic.main.delete_history_button.*
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 HistoryUIView(
container: ViewGroup,
actionEmitter: Observer<HistoryAction>,
changesObservable: Observable<HistoryChange>
) :
LibraryPageUIView<HistoryState, HistoryAction, HistoryChange>(container, actionEmitter, changesObservable),
BackHandler {
var mode: HistoryState.Mode = HistoryState.Mode.Normal
private set
private val historyAdapter: HistoryAdapter
private var items: List<HistoryItem> = listOf()
fun getSelected(): List<HistoryItem> = historyAdapter.selected
override val view: ConstraintLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_history, container, true)
.findViewById(R.id.history_wrapper)
init {
view.history_list.apply {
historyAdapter = HistoryAdapter(actionEmitter)
adapter = historyAdapter
layoutManager = LinearLayoutManager(container.context)
}
}
override fun updateView() = Consumer<HistoryState> {
view.progress_bar.visibility = if (it.mode is HistoryState.Mode.Deleting) View.VISIBLE else View.GONE
if (it.mode != mode) {
mode = it.mode
actionEmitter.onNext(HistoryAction.SwitchMode)
}
(view.history_list.adapter as HistoryAdapter).updateData(it.items, it.mode)
items = it.items
when (val modeCopy = mode) {
is HistoryState.Mode.Normal -> setUIForNormalMode(items.isEmpty())
is HistoryState.Mode.Editing -> setUIForSelectingMode(modeCopy)
}
}
private fun setUIForSelectingMode(
mode: HistoryState.Mode.Editing
) {
activity?.title =
context.getString(R.string.history_multi_select_title, mode.selectedItems.size)
setToolbarColors(
R.color.white_color,
R.attr.accentHighContrast.getColorIntFromAttr(context!!)
)
}
private fun setUIForNormalMode(isEmpty: Boolean) {
activity?.title = context.getString(R.string.library_history)
delete_history_button?.isVisible = !isEmpty
history_empty_view.isVisible = isEmpty
setToolbarColors(
R.attr.primaryText.getColorIntFromAttr(context!!),
R.attr.foundation.getColorIntFromAttr(context)
)
}
override fun onBackPressed(): Boolean {
return when (mode) {
is HistoryState.Mode.Editing -> {
mode = HistoryState.Mode.Normal
historyAdapter.updateData(items, mode)
setUIForNormalMode(items.isEmpty())
actionEmitter.onNext(HistoryAction.BackPressed)
true
}
else -> false
}
}
}

@ -0,0 +1,200 @@
/* 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.history
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_history.*
import kotlinx.android.synthetic.main.component_history.view.*
import kotlinx.android.synthetic.main.delete_history_button.*
import mozilla.components.support.base.feature.BackHandler
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.asActivity
import org.mozilla.fenix.ext.getColorIntFromAttr
/**
* Interface for the HistoryViewInteractor. This interface is implemented by objects that want
* to respond to user interaction on the HistoryView
*/
interface HistoryViewInteractor {
/**
* Called whenever a history item is tapped to open that history entry in the browser
* @param item the history item to open in browser
*/
fun onHistoryItemOpened(item: HistoryItem)
/**
* Called when a history item is long pressed and edit mode is launched
* @param selectedItem the history item to start selected for deletion in edit mode
*/
fun onEnterEditMode(selectedItem: HistoryItem)
/**
* Called on backpressed to exit edit mode
*/
fun onBackPressed()
/**
* Called when a history item is tapped in edit mode and added for removal
* @param item the history item to add to selected items for deletion in edit mode
*/
fun onItemAddedForRemoval(item: HistoryItem)
/**
* Called when a selected history item is tapped in edit mode and removed from removal
* @param item the history item to remove from the selected items for deletion in edit mode
*/
fun onItemRemovedForRemoval(item: HistoryItem)
/**
* Called when the mode is switched so we can invalidate the menu
*/
fun onModeSwitched()
/**
* Called when delete all is tapped
*/
fun onDeleteAll()
/**
* Called when one history item is deleted
* @param item the history item to delete
*/
fun onDeleteOne(item: HistoryItem)
/**
* Called when multiple history items are deleted
* @param items the history items to delete
*/
fun onDeleteSome(items: List<HistoryItem>)
}
/**
* View that contains and configures the History List
*/
class HistoryView(
private val container: ViewGroup,
val interactor: HistoryInteractor
) : LayoutContainer, BackHandler {
val view: ConstraintLayout = LayoutInflater.from(container.context)
.inflate(R.layout.component_history, container, true)
.findViewById(R.id.history_wrapper)
override val containerView: View?
get() = container
private val historyAdapter: HistoryAdapter
private var items: List<HistoryItem> = listOf()
private val context = container.context
var mode: HistoryState.Mode = HistoryState.Mode.Normal
private set
private val activity = context?.asActivity()
init {
view.history_list.apply {
historyAdapter = HistoryAdapter(interactor)
adapter = historyAdapter
layoutManager = LinearLayoutManager(container.context)
}
}
fun update(state: HistoryState) {
view.progress_bar.visibility =
if (state.mode is HistoryState.Mode.Deleting) View.VISIBLE else View.GONE
if (state.mode != mode) {
mode = state.mode
interactor.onModeSwitched()
}
(view.history_list.adapter as HistoryAdapter).updateData(state.items, state.mode)
items = state.items
when (val mode = mode) {
is HistoryState.Mode.Normal -> setUIForNormalMode(items.isEmpty())
is HistoryState.Mode.Editing -> setUIForSelectingMode(mode.selectedItems.size)
}
}
private fun setUIForSelectingMode(selectedItemSize: Int) {
activity?.title =
context.getString(R.string.history_multi_select_title, selectedItemSize)
setToolbarColors(
R.color.white_color,
R.attr.accentHighContrast.getColorIntFromAttr(context!!)
)
}
private fun setUIForNormalMode(isEmpty: Boolean) {
activity?.title = context.getString(R.string.library_history)
delete_history_button?.isVisible = !isEmpty
history_empty_view.isVisible = isEmpty
setToolbarColors(
R.attr.primaryText.getColorIntFromAttr(context!!),
R.attr.foundation.getColorIntFromAttr(context)
)
}
private fun setToolbarColors(foreground: Int, background: Int) {
val toolbar = (activity as AppCompatActivity).findViewById<Toolbar>(R.id.navigationToolbar)
val colorFilter = PorterDuffColorFilter(
ContextCompat.getColor(context, foreground),
PorterDuff.Mode.SRC_IN
)
toolbar.setBackgroundColor(ContextCompat.getColor(context, background))
toolbar.setTitleTextColor(ContextCompat.getColor(context, foreground))
themeToolbar(
toolbar, foreground,
background, colorFilter
)
}
private fun themeToolbar(
toolbar: Toolbar,
textColor: Int,
backgroundColor: Int,
colorFilter: PorterDuffColorFilter? = null
) {
toolbar.setTitleTextColor(ContextCompat.getColor(context!!, textColor))
toolbar.setBackgroundColor(ContextCompat.getColor(context, backgroundColor))
if (colorFilter == null) {
return
}
toolbar.overflowIcon?.colorFilter = colorFilter
(0 until toolbar.childCount).forEach {
when (val item = toolbar.getChildAt(it)) {
is ImageButton -> item.drawable.colorFilter = colorFilter
}
}
}
override fun onBackPressed(): Boolean {
return when (mode) {
is HistoryState.Mode.Editing -> {
mode = HistoryState.Mode.Normal
historyAdapter.updateData(items, mode)
setUIForNormalMode(items.isEmpty())
interactor.onBackPressed()
true
}
else -> false
}
}
}

@ -6,15 +6,14 @@ package org.mozilla.fenix.library.history.viewholders
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.synthetic.main.delete_history_button.view.* import kotlinx.android.synthetic.main.delete_history_button.view.*
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.HistoryAction import org.mozilla.fenix.library.history.HistoryInteractor
import org.mozilla.fenix.library.history.HistoryState import org.mozilla.fenix.library.history.HistoryState
class HistoryDeleteButtonViewHolder( class HistoryDeleteButtonViewHolder(
view: View, view: View,
private val actionEmitter: Observer<HistoryAction> historyInteractor: HistoryInteractor
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
private var mode: HistoryState.Mode? = null private var mode: HistoryState.Mode? = null
private val buttonView = view.delete_history_button private val buttonView = view.delete_history_button
@ -22,13 +21,10 @@ class HistoryDeleteButtonViewHolder(
init { init {
buttonView.setOnClickListener { buttonView.setOnClickListener {
mode?.also { mode?.also {
val action = when (it) { when (it) {
is HistoryState.Mode.Normal -> HistoryAction.Delete.All is HistoryState.Mode.Normal -> historyInteractor.onDeleteAll()
is HistoryState.Mode.Editing -> HistoryAction.Delete.Some(it.selectedItems) is HistoryState.Mode.Editing -> historyInteractor.onDeleteSome(it.selectedItems)
is HistoryState.Mode.Deleting -> null }
} ?: return@also
actionEmitter.onNext(action)
} }
} }
} }

@ -8,7 +8,6 @@ import android.view.View
import android.widget.CompoundButton import android.widget.CompoundButton
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.synthetic.main.history_list_item.view.* import kotlinx.android.synthetic.main.history_list_item.view.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -19,7 +18,7 @@ import mozilla.components.browser.menu.BrowserMenu
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ThemeManager import org.mozilla.fenix.ThemeManager
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.library.history.HistoryAction import org.mozilla.fenix.library.history.HistoryInteractor
import org.mozilla.fenix.library.history.HistoryItem import org.mozilla.fenix.library.history.HistoryItem
import org.mozilla.fenix.library.history.HistoryItemMenu import org.mozilla.fenix.library.history.HistoryItemMenu
import org.mozilla.fenix.library.history.HistoryState import org.mozilla.fenix.library.history.HistoryState
@ -27,7 +26,7 @@ import kotlin.coroutines.CoroutineContext
class HistoryListItemViewHolder( class HistoryListItemViewHolder(
view: View, view: View,
private val actionEmitter: Observer<HistoryAction>, private val historyInteractor: HistoryInteractor,
val job: Job val job: Job
) : RecyclerView.ViewHolder(view), CoroutineScope { ) : RecyclerView.ViewHolder(view), CoroutineScope {
@ -48,13 +47,11 @@ class HistoryListItemViewHolder(
} }
item?.apply { item?.apply {
val action = if (isChecked) { if (isChecked) {
HistoryAction.AddItemForRemoval(this) historyInteractor.onItemAddedForRemoval(this)
} else { } else {
HistoryAction.RemoveItemForRemoval(this) historyInteractor.onItemRemovedForRemoval(this)
} }
actionEmitter.onNext(action)
} }
} }
@ -63,7 +60,7 @@ class HistoryListItemViewHolder(
view.setOnLongClickListener { view.setOnLongClickListener {
item?.apply { item?.apply {
actionEmitter.onNext(HistoryAction.EnterEditMode(this)) historyInteractor.onEnterEditMode(this)
} }
true true
@ -72,7 +69,8 @@ class HistoryListItemViewHolder(
menuButton.setOnClickListener { menuButton.setOnClickListener {
historyMenu.menuBuilder.build(view.context).show( historyMenu.menuBuilder.build(view.context).show(
anchor = it, anchor = it,
orientation = BrowserMenu.Orientation.DOWN) orientation = BrowserMenu.Orientation.DOWN
)
} }
} }
@ -97,7 +95,8 @@ class HistoryListItemViewHolder(
} else { } else {
ThemeManager.resolveAttribute(R.attr.neutral, itemView.context) ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
} }
val backgroundTintList = ContextCompat.getColorStateList(itemView.context, backgroundTint) val backgroundTintList =
ContextCompat.getColorStateList(itemView.context, backgroundTint)
favicon.backgroundTintList = backgroundTintList favicon.backgroundTintList = backgroundTintList
if (selected) { if (selected) {
@ -107,7 +106,8 @@ class HistoryListItemViewHolder(
} }
} else { } else {
val backgroundTint = ThemeManager.resolveAttribute(R.attr.neutral, itemView.context) val backgroundTint = ThemeManager.resolveAttribute(R.attr.neutral, itemView.context)
val backgroundTintList = ContextCompat.getColorStateList(itemView.context, backgroundTint) val backgroundTintList =
ContextCompat.getColorStateList(itemView.context, backgroundTint)
favicon.backgroundTintList = backgroundTintList favicon.backgroundTintList = backgroundTintList
updateFavIcon(item.url) updateFavIcon(item.url)
} }
@ -117,7 +117,7 @@ class HistoryListItemViewHolder(
this.historyMenu = HistoryItemMenu(itemView.context) { this.historyMenu = HistoryItemMenu(itemView.context) {
when (it) { when (it) {
is HistoryItemMenu.Item.Delete -> { is HistoryItemMenu.Item.Delete -> {
item?.apply { actionEmitter.onNext(HistoryAction.Delete.One(this)) } item?.apply { historyInteractor.onDeleteOne(this) }
} }
} }
} }
@ -139,11 +139,13 @@ class HistoryListItemViewHolder(
) { ) {
itemView.history_layout.setOnClickListener { itemView.history_layout.setOnClickListener {
if (mode == HistoryState.Mode.Normal) { if (mode == HistoryState.Mode.Normal) {
actionEmitter.onNext(HistoryAction.Open(item)) historyInteractor.onHistoryItemOpened(item)
} else { } else {
if (selected) actionEmitter.onNext(HistoryAction.RemoveItemForRemoval(item)) else actionEmitter.onNext( if (selected) {
HistoryAction.AddItemForRemoval(item) historyInteractor.onItemRemovedForRemoval(item)
) } else {
historyInteractor.onItemAddedForRemoval(item)
}
} }
} }
} }

@ -0,0 +1,129 @@
/* 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.history
import io.mockk.mockk
import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Test
class HistoryInteractorTest {
@Test
fun onHistoryItemOpened() {
var historyItemReceived: HistoryItem? = null
val historyItem = HistoryItem(0, "title", "url", 0.toLong())
val interactor = HistoryInteractor(
mockk(),
{ historyItemReceived = it },
mockk(),
mockk(),
mockk()
)
interactor.onHistoryItemOpened(historyItem)
assertEquals(historyItem, historyItemReceived)
}
@Test
fun onEnterEditMode() {
val store: HistoryStore = mockk(relaxed = true)
val newHistoryItem: HistoryItem = mockk(relaxed = true)
val interactor =
HistoryInteractor(store, mockk(), mockk(), mockk(), mockk())
interactor.onEnterEditMode(newHistoryItem)
verify { store.dispatch(HistoryAction.EnterEditMode(newHistoryItem)) }
}
@Test
fun onBackPressed() {
val store: HistoryStore = mockk(relaxed = true)
val interactor =
HistoryInteractor(store, mockk(), mockk(), mockk(), mockk())
interactor.onBackPressed()
verify { store.dispatch(HistoryAction.ExitEditMode) }
}
@Test
fun onItemAddedForRemoval() {
val store: HistoryStore = mockk(relaxed = true)
val newHistoryItem: HistoryItem = mockk(relaxed = true)
val interactor =
HistoryInteractor(store, mockk(), mockk(), mockk(), mockk())
interactor.onItemAddedForRemoval(newHistoryItem)
verify { store.dispatch(HistoryAction.AddItemForRemoval(newHistoryItem)) }
}
@Test
fun onItemRemovedForRemoval() {
val store: HistoryStore = mockk(relaxed = true)
val newHistoryItem: HistoryItem = mockk(relaxed = true)
val interactor =
HistoryInteractor(store, mockk(), mockk(), mockk(), mockk())
interactor.onItemRemovedForRemoval(newHistoryItem)
verify { store.dispatch(HistoryAction.RemoveItemForRemoval(newHistoryItem)) }
}
@Test
fun onModeSwitched() {
var menuInvalidated = false
val interactor = HistoryInteractor(
mockk(),
mockk(),
mockk(),
{ menuInvalidated = true },
mockk()
)
interactor.onModeSwitched()
assertEquals(true, menuInvalidated)
}
@Test
fun onDeleteAll() {
var deleteAllDialogShown = false
val interactor = HistoryInteractor(
mockk(),
mockk(),
{ deleteAllDialogShown = true },
mockk(),
mockk()
)
interactor.onDeleteAll()
assertEquals(true, deleteAllDialogShown)
}
@Test
fun onDeleteOne() {
var itemsToDelete: List<HistoryItem>? = null
val historyItem = HistoryItem(0, "title", "url", 0.toLong())
val interactor =
HistoryInteractor(
mockk(),
mockk(),
mockk(),
mockk(),
{ itemsToDelete = it }
)
interactor.onDeleteOne(historyItem)
assertEquals(itemsToDelete, listOf(historyItem))
}
@Test
fun onDeleteSome() {
var itemsToDelete: List<HistoryItem>? = null
val historyItem = HistoryItem(0, "title", "url", 0.toLong())
val newHistoryItem = HistoryItem(1, "title", "url", 0.toLong())
val interactor =
HistoryInteractor(
mockk(),
mockk(),
mockk(),
mockk(),
{ itemsToDelete = it }
)
interactor.onDeleteSome(listOf(historyItem, newHistoryItem))
assertEquals(itemsToDelete, listOf(historyItem, newHistoryItem))
}
}

@ -0,0 +1,73 @@
/* 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.history
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
class HistoryStoreTest {
private val historyItem = HistoryItem(0, "title", "url", 0.toLong())
private val newHistoryItem = HistoryItem(1, "title", "url", 0.toLong())
@Test
fun enterEditMode() = runBlocking {
val initialState = emptyDefaultState()
val store = HistoryStore(initialState)
store.dispatch(HistoryAction.EnterEditMode(historyItem)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.mode, HistoryState.Mode.Editing(listOf(historyItem)))
}
@Test
fun exitEditMode() = runBlocking {
val initialState = oneItemEditState()
val store = HistoryStore(initialState)
store.dispatch(HistoryAction.ExitEditMode).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.mode, HistoryState.Mode.Normal)
}
@Test
fun itemAddedForRemoval() = runBlocking {
val initialState = oneItemEditState()
val store = HistoryStore(initialState)
store.dispatch(HistoryAction.AddItemForRemoval(newHistoryItem)).join()
assertNotSame(initialState, store.state)
assertEquals(
store.state.mode,
HistoryState.Mode.Editing(listOf(historyItem, newHistoryItem))
)
}
@Test
fun removeItemForRemoval() = runBlocking {
val initialState = twoItemEditState()
val store = HistoryStore(initialState)
store.dispatch(HistoryAction.RemoveItemForRemoval(newHistoryItem)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.mode, HistoryState.Mode.Editing(listOf(historyItem)))
}
private fun emptyDefaultState(): HistoryState = HistoryState(
items = listOf(),
mode = HistoryState.Mode.Normal
)
private fun oneItemEditState(): HistoryState = HistoryState(
items = listOf(),
mode = HistoryState.Mode.Editing(listOf(historyItem))
)
private fun twoItemEditState(): HistoryState = HistoryState(
items = listOf(),
mode = HistoryState.Mode.Editing(listOf(historyItem, newHistoryItem))
)
}

@ -1,95 +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.history
import io.mockk.MockKAnnotations
import io.reactivex.Observer
import io.reactivex.observers.TestObserver
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.TestUtils.bus
import org.mozilla.fenix.TestUtils.owner
import org.mozilla.fenix.TestUtils.setRxSchedulers
import org.mozilla.fenix.mvi.getManagedEmitter
class HistoryViewModelTest {
private lateinit var historyViewModel: HistoryViewModel
private lateinit var historyObserver: TestObserver<HistoryState>
private lateinit var emitter: Observer<HistoryChange>
@Before
fun setup() {
MockKAnnotations.init(this)
setRxSchedulers()
historyViewModel = HistoryViewModel.create()
historyObserver = historyViewModel.state.test()
bus.getSafeManagedObservable(HistoryChange::class.java)
.subscribe(historyViewModel.changes::onNext)
emitter = owner.getManagedEmitter()
}
@Test
fun `select two items for removal, then deselect one, then select it again`() {
val historyItem = HistoryItem(1, "Mozilla", "http://mozilla.org", 0)
val historyItem2 = HistoryItem(2, "Mozilla", "http://mozilla.org", 0)
emitter.onNext(HistoryChange.Change(listOf(historyItem, historyItem2)))
emitter.onNext(HistoryChange.EnterEditMode(historyItem))
emitter.onNext(HistoryChange.AddItemForRemoval(historyItem2))
emitter.onNext(HistoryChange.RemoveItemForRemoval(historyItem))
emitter.onNext(HistoryChange.AddItemForRemoval(historyItem))
emitter.onNext(HistoryChange.ExitEditMode)
historyObserver.assertSubscribed().awaitCount(7).assertNoErrors()
.assertValues(
HistoryState(listOf(), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem))),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem, historyItem2))),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem2))),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Editing(listOf(historyItem2, historyItem))),
HistoryState(listOf(historyItem, historyItem2), HistoryState.Mode.Normal)
)
}
@Test
fun `deselecting all items triggers normal mode`() {
val historyItem = HistoryItem(123, "Mozilla", "http://mozilla.org", 0)
emitter.onNext(HistoryChange.Change(listOf(historyItem)))
emitter.onNext(HistoryChange.EnterEditMode(historyItem))
emitter.onNext(HistoryChange.RemoveItemForRemoval(historyItem))
historyObserver.assertSubscribed().awaitCount(6).assertNoErrors()
.assertValues(
HistoryState(listOf(), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem), HistoryState.Mode.Normal),
HistoryState(listOf(historyItem), HistoryState.Mode.Editing(listOf(historyItem))),
HistoryState(listOf(historyItem), HistoryState.Mode.Normal)
)
}
@Test
fun `try making changes when not in edit mode`() {
val historyItems = listOf(
HistoryItem(1337, "Reddit", "http://reddit.com", 0),
HistoryItem(31337, "Haxor", "http://leethaxor.com", 0)
)
emitter.onNext(HistoryChange.Change(historyItems))
emitter.onNext(HistoryChange.AddItemForRemoval(historyItems[0]))
emitter.onNext(HistoryChange.EnterEditMode(historyItems[0]))
emitter.onNext(HistoryChange.ExitEditMode)
historyObserver.assertSubscribed().awaitCount(4).assertNoErrors()
.assertValues(
HistoryState(listOf(), HistoryState.Mode.Normal),
HistoryState(historyItems, HistoryState.Mode.Normal),
HistoryState(historyItems, HistoryState.Mode.Editing(listOf(historyItems[0]))),
HistoryState(historyItems, HistoryState.Mode.Normal)
)
}
}
Loading…
Cancel
Save