[fenix] For https://github.com/mozilla-mobile/fenix/issues/3987 - Convert History to Lib-State and add tests
parent
1b4b03a841
commit
ef062ecd5b
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -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…
Reference in New Issue