For #352: Delete a download

upstream-sync
Kate Glazko 4 years ago committed by kglazko
parent c0e01373e1
commit 0ae268914b

@ -6,6 +6,7 @@ package org.mozilla.fenix.library.downloads
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder
@ -16,6 +17,7 @@ class DownloadAdapter(
private var downloads: List<DownloadItem> = listOf()
private var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal
override val selectedItems get() = mode.selectedItems
var pendingDeletionIds = emptySet<String>()
override fun getItemCount(): Int = downloads.size
override fun getItemViewType(position: Int): Int = DownloadsListItemViewHolder.LAYOUT_ID
@ -27,14 +29,38 @@ class DownloadAdapter(
fun updateMode(mode: DownloadFragmentState.Mode) {
this.mode = mode
// Update the delete button alpha that the first item holds
if (itemCount > 0) notifyItemChanged(0)
}
override fun onBindViewHolder(holder: DownloadsListItemViewHolder, position: Int) {
holder.bind(downloads[position])
val current = downloads[position]
val isPendingDeletion = pendingDeletionIds.contains(current.id)
holder.bind(downloads[position], position == 0, mode, isPendingDeletion)
}
fun updateDownloads(downloads: List<DownloadItem>) {
this.downloads = downloads
notifyDataSetChanged()
}
fun updatePendingDeletionIds(pendingDeletionIds: Set<String>) {
this.pendingDeletionIds = pendingDeletionIds
}
companion object {
private val downloadDiffCallback = object : DiffUtil.ItemCallback<DownloadItem>() {
override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
return oldItem == newItem
}
override fun getChangePayload(oldItem: DownloadItem, newItem: DownloadItem): Any? {
return newItem
}
}
}
}

@ -8,17 +8,33 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
interface DownloadController {
fun handleOpen(item: DownloadItem, mode: BrowsingMode? = null)
fun handleSelect(item: DownloadItem)
fun handleDeselect(item: DownloadItem)
fun handleBackPressed(): Boolean
fun handleModeSwitched()
fun handleDeleteSome(items: Set<DownloadItem>)
fun handleDeleteAll()
}
class DefaultDownloadController(
private val store: DownloadFragmentStore,
private val openToFileManager: (item: DownloadItem, mode: BrowsingMode?) -> Unit
private val openToFileManager: (item: DownloadItem, mode: BrowsingMode?) -> Unit,
private val displayDeleteAll: () -> Unit,
private val invalidateOptionsMenu: () -> Unit,
private val deleteDownloadItems: (Set<DownloadItem>) -> Unit
) : DownloadController {
override fun handleOpen(item: DownloadItem, mode: BrowsingMode?) {
openToFileManager(item, mode)
}
override fun handleSelect(item: DownloadItem) {
store.dispatch(DownloadFragmentAction.AddItemForRemoval(item))
}
override fun handleDeselect(item: DownloadItem) {
store.dispatch(DownloadFragmentAction.RemoveItemForRemoval(item))
}
override fun handleBackPressed(): Boolean {
return if (store.state.mode is DownloadFragmentState.Mode.Editing) {
store.dispatch(DownloadFragmentAction.ExitEditMode)
@ -27,4 +43,16 @@ class DefaultDownloadController(
false
}
}
override fun handleModeSwitched() {
invalidateOptionsMenu.invoke()
}
override fun handleDeleteAll() {
displayDeleteAll.invoke()
}
override fun handleDeleteSome(items: Set<DownloadItem>) {
deleteDownloadItems.invoke(items)
}
}

@ -4,33 +4,49 @@
package org.mozilla.fenix.library.downloads
import android.content.DialogInterface
import android.os.Bundle
import android.text.SpannableString
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.lifecycle.lifecycleScope
import kotlinx.android.synthetic.main.fragment_downloads.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.state.BrowserState
import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.addons.showSnackBar
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.filterNotExistsOnDisk
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.setTextColor
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.utils.allowUndo
@SuppressWarnings("TooManyFunctions", "LargeClass")
class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHandler {
private lateinit var downloadStore: DownloadFragmentStore
private lateinit var downloadView: DownloadView
private lateinit var downloadInteractor: DownloadInteractor
private var undoScope: CoroutineScope? = null
private var pendingDownloadDeletionJob: (suspend () -> Unit)? = null
override fun onCreateView(
inflater: LayoutInflater,
@ -45,14 +61,18 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
DownloadFragmentStore(
DownloadFragmentState(
items = items,
mode = DownloadFragmentState.Mode.Normal
mode = DownloadFragmentState.Mode.Normal,
pendingDeletionIds = emptySet(),
isDeletingItems = false
)
)
}
val downloadController: DownloadController = DefaultDownloadController(
downloadStore,
::openItem
::openItem,
::displayDeleteAll,
::invalidateOptionsMenu,
::deleteDownloadItems
)
downloadInteractor = DownloadInteractor(
downloadController
@ -82,12 +102,55 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
override val selectedItems get() = downloadStore.state.mode.selectedItems
private fun invalidateOptionsMenu() {
activity?.invalidateOptionsMenu()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
requireComponents.analytics.metrics.track(Event.HistoryOpened)
private fun displayDeleteAll() {
activity?.let { activity ->
AlertDialog.Builder(activity).apply {
setMessage(R.string.download_delete_all_dialog)
setNegativeButton(R.string.delete_browsing_data_prompt_cancel) { dialog: DialogInterface, _ ->
dialog.cancel()
}
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
// Use fragment's lifecycle; the view may be gone by the time dialog is interacted with.
lifecycleScope.launch(IO) {
context.let {
it.components.useCases.downloadUseCases.removeAllDownloads()
}
updatePendingDownloadToDelete(downloadStore.state.items.toSet())
launch(Dispatchers.Main) {
showSnackBar(
requireView(),
getString(R.string.download_delete_multiple_items_snackbar)
)
}
}
dialog.dismiss()
}
create()
}.show()
}
}
setHasOptionsMenu(false)
private fun deleteDownloadItems(items: Set<DownloadItem>) {
updatePendingDownloadToDelete(items)
undoScope = CoroutineScope(IO)
undoScope?.allowUndo(
requireView(),
getMultiSelectSnackBarMessage(items),
getString(R.string.bookmark_undo_deletion),
{
undoPendingDeletion(items)
},
getDeleteDownloadItemsOperation(items)
)
}
@ExperimentalCoroutinesApi
@ -104,7 +167,52 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
showToolbar(getString(R.string.library_downloads))
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
val menuRes = when (downloadStore.state.mode) {
is DownloadFragmentState.Mode.Normal -> R.menu.library_menu
is DownloadFragmentState.Mode.Editing -> R.menu.download_select_multi
}
inflater.inflate(menuRes, menu)
menu.findItem(R.id.delete_downloads_multi_select)?.title =
SpannableString(getString(R.string.bookmark_menu_delete_button)).apply {
setTextColor(requireContext(), R.attr.destructive)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
R.id.close_history -> {
close()
true
}
R.id.delete_downloads_multi_select -> {
deleteDownloadItems(downloadStore.state.mode.selectedItems)
downloadStore.dispatch(DownloadFragmentAction.ExitEditMode)
true
}
else -> super.onOptionsItemSelected(item)
}
private fun getMultiSelectSnackBarMessage(downloadItems: Set<DownloadItem>): String {
return if (downloadItems.size > 1) {
getString(R.string.download_delete_multiple_items_snackbar)
} else {
String.format(
requireContext().getString(
R.string.history_delete_single_item_snackbar
), downloadItems.first().fileName
)
}
}
override fun onPause() {
invokePendingDeletion()
super.onPause()
}
override fun onBackPressed(): Boolean {
invokePendingDeletion()
return downloadView.onBackPressed()
}
@ -119,4 +227,41 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
)
}
}
private fun getDeleteDownloadItemsOperation(items: Set<DownloadItem>): (suspend () -> Unit) {
return {
CoroutineScope(IO).launch {
downloadStore.dispatch(DownloadFragmentAction.EnterDeletionMode)
context?.let {
for (item in items) {
it.components.useCases.downloadUseCases.removeDownload(item.id)
}
}
downloadStore.dispatch(DownloadFragmentAction.ExitDeletionMode)
pendingDownloadDeletionJob = null
}
}
}
private fun updatePendingDownloadToDelete(items: Set<DownloadItem>) {
pendingDownloadDeletionJob = getDeleteDownloadItemsOperation(items)
val ids = items.map { item -> item.id }.toSet()
downloadStore.dispatch(DownloadFragmentAction.AddPendingDeletionSet(ids))
}
private fun undoPendingDeletion(items: Set<DownloadItem>) {
pendingDownloadDeletionJob = null
val ids = items.map { item -> item.id }.toSet()
downloadStore.dispatch(DownloadFragmentAction.UndoPendingDeletionSet(ids))
}
private fun invokePendingDeletion() {
pendingDownloadDeletionJob?.let {
viewLifecycleOwner.lifecycleScope.launch {
it.invoke()
}.invokeOnCompletion {
pendingDownloadDeletionJob = null
}
}
}
}

@ -10,7 +10,7 @@ import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
/**
* Class representing a history entry
* Class representing a downloads entry
* @property id Unique id of the download item
* @property fileName File name of the download item
* @property filePath Full path of the download item
@ -35,8 +35,15 @@ class DownloadFragmentStore(initialState: DownloadFragmentState) :
/**
* Actions to dispatch through the `DownloadStore` to modify `DownloadState` through the reducer.
*/
sealed class DownloadFragmentAction : Action {
object ExitEditMode : DownloadFragmentAction()
data class AddItemForRemoval(val item: DownloadItem) : DownloadFragmentAction()
data class RemoveItemForRemoval(val item: DownloadItem) : DownloadFragmentAction()
data class AddPendingDeletionSet(val itemIds: Set<String>) : DownloadFragmentAction()
data class UndoPendingDeletionSet(val itemIds: Set<String>) : DownloadFragmentAction()
object EnterDeletionMode : DownloadFragmentAction()
object ExitDeletionMode : DownloadFragmentAction()
}
/**
@ -46,7 +53,9 @@ sealed class DownloadFragmentAction : Action {
*/
data class DownloadFragmentState(
val items: List<DownloadItem>,
val mode: Mode
val mode: Mode,
val pendingDeletionIds: Set<String>,
val isDeletingItems: Boolean
) : State {
sealed class Mode {
open val selectedItems = emptySet<DownloadItem>()
@ -64,6 +73,28 @@ private fun downloadStateReducer(
action: DownloadFragmentAction
): DownloadFragmentState {
return when (action) {
is DownloadFragmentAction.AddItemForRemoval ->
state.copy(mode = DownloadFragmentState.Mode.Editing(state.mode.selectedItems + action.item))
is DownloadFragmentAction.RemoveItemForRemoval -> {
val selected = state.mode.selectedItems - action.item
state.copy(
mode = if (selected.isEmpty()) {
DownloadFragmentState.Mode.Normal
} else {
DownloadFragmentState.Mode.Editing(selected)
}
)
}
is DownloadFragmentAction.ExitEditMode -> state.copy(mode = DownloadFragmentState.Mode.Normal)
is DownloadFragmentAction.EnterDeletionMode -> state.copy(isDeletingItems = true)
is DownloadFragmentAction.ExitDeletionMode -> state.copy(isDeletingItems = false)
is DownloadFragmentAction.AddPendingDeletionSet ->
state.copy(
pendingDeletionIds = state.pendingDeletionIds + action.itemIds
)
is DownloadFragmentAction.UndoPendingDeletionSet ->
state.copy(
pendingDeletionIds = state.pendingDeletionIds - action.itemIds
)
}
}

@ -15,11 +15,27 @@ class DownloadInteractor(
downloadController.handleOpen(item)
}
override fun select(item: DownloadItem) { /* noop */ }
override fun select(item: DownloadItem) {
downloadController.handleSelect(item)
}
override fun deselect(item: DownloadItem) { /* noop */ }
override fun deselect(item: DownloadItem) {
downloadController.handleDeselect(item)
}
override fun onBackPressed(): Boolean {
return downloadController.handleBackPressed()
}
override fun onModeSwitched() {
downloadController.handleModeSwitched()
}
override fun onDeleteSome(items: Set<DownloadItem>) {
downloadController.handleDeleteSome(items)
}
override fun onDeleteAll() {
downloadController.handleDeleteAll()
}
}

@ -0,0 +1,44 @@
/* 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.downloads
import android.content.Context
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.menu2.BrowserMenuController
import mozilla.components.concept.menu.MenuController
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.concept.menu.candidate.TextStyle
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R
class DownloadItemMenu(
private val context: Context,
private val onItemTapped: (Item) -> Unit
) {
enum class Item {
Delete
}
val menuController: MenuController by lazy {
BrowserMenuController().apply {
submitList(menuItems())
}
}
@VisibleForTesting
internal fun menuItems(): List<TextMenuCandidate> {
return listOf(
TextMenuCandidate(
text = context.getString(R.string.history_delete_item),
textStyle = TextStyle(
color = context.getColorFromAttr(R.attr.destructive)
)
) {
onItemTapped.invoke(Item.Delete)
}
)
}
}

@ -12,6 +12,8 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.SimpleItemAnimator
import kotlinx.android.synthetic.main.component_downloads.*
import kotlinx.android.synthetic.main.component_downloads.view.*
import kotlinx.android.synthetic.main.component_history.view.progress_bar
import kotlinx.android.synthetic.main.component_history.view.swipe_refresh
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.R
import org.mozilla.fenix.library.LibraryPageView
@ -27,6 +29,22 @@ interface DownloadViewInteractor : SelectionInteractor<DownloadItem> {
* Called on backpressed to exit edit mode
*/
fun onBackPressed(): Boolean
/**
* Called when the mode is switched so we can invalidate the menu
*/
fun onModeSwitched()
/**
* Called when multiple downloads items are deleted
* @param items the downloads items to delete
*/
fun onDeleteSome(items: Set<DownloadItem>)
/**
* Called when all downloads items are deleted
*/
fun onDeleteAll()
}
/**
@ -55,18 +73,41 @@ class DownloadView(
}
fun update(state: DownloadFragmentState) {
val oldMode = mode
view.progress_bar.isVisible = state.isDeletingItems
view.swipe_refresh.isEnabled = false
mode = state.mode
updateEmptyState(state.items.isNotEmpty())
downloadAdapter.updatePendingDeletionIds(state.pendingDeletionIds)
updateEmptyState(state.pendingDeletionIds.size != state.items.size)
downloadAdapter.updateMode(state.mode)
downloadAdapter.updateDownloads(state.items)
setUiForNormalMode(
context.getString(R.string.library_downloads)
)
if (state.mode::class != oldMode::class) {
interactor.onModeSwitched()
}
when (val mode = state.mode) {
is DownloadFragmentState.Mode.Normal -> {
setUiForNormalMode(
context.getString(R.string.library_downloads)
)
}
is DownloadFragmentState.Mode.Editing -> {
val unselectedItems = oldMode.selectedItems - state.mode.selectedItems
state.mode.selectedItems.union(unselectedItems).forEach { item ->
val index = state.items.indexOf(item)
downloadAdapter.notifyItemChanged(index)
}
setUiForSelectingMode(
context.getString(R.string.download_multi_select_title, mode.selectedItems.size)
)
}
}
}
fun updateEmptyState(userHasDownloads: Boolean) {

@ -5,16 +5,19 @@
package org.mozilla.fenix.library.downloads.viewholders
import android.view.View
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.download_list_item.view.*
import kotlinx.android.synthetic.main.library_site_item.view.*
import mozilla.components.feature.downloads.toMegabyteOrKilobyteString
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.hideAndDisable
import org.mozilla.fenix.library.SelectionHolder
import org.mozilla.fenix.library.downloads.DownloadInteractor
import org.mozilla.fenix.library.downloads.DownloadItem
import org.mozilla.fenix.ext.getIcon
import org.mozilla.fenix.ext.showAndEnable
import org.mozilla.fenix.library.downloads.DownloadFragmentState
import org.mozilla.fenix.library.downloads.DownloadItemMenu
class DownloadsListItemViewHolder(
view: View,
@ -24,24 +27,77 @@ class DownloadsListItemViewHolder(
private var item: DownloadItem? = null
init {
setupMenu()
itemView.delete_downloads_button.setOnClickListener {
val selected = selectionHolder.selectedItems
if (selected.isEmpty()) {
downloadInteractor.onDeleteAll()
} else {
downloadInteractor.onDeleteSome(selected)
}
}
}
fun bind(
item: DownloadItem
item: DownloadItem,
showDeleteButton: Boolean,
mode: DownloadFragmentState.Mode,
isPendingDeletion: Boolean = false
) {
itemView.download_layout.visibility = View.VISIBLE
itemView.download_layout.visibility = if (isPendingDeletion) {
View.GONE
} else {
View.VISIBLE
}
itemView.download_layout.titleView.text = item.fileName
itemView.download_layout.urlView.text = item.size.toLong().toMegabyteOrKilobyteString()
toggleTopContent(showDeleteButton, mode == DownloadFragmentState.Mode.Normal)
itemView.download_layout.setSelectionInteractor(item, selectionHolder, downloadInteractor)
itemView.download_layout.changeSelected(item in selectionHolder.selectedItems)
itemView.overflow_menu.hideAndDisable()
itemView.favicon.setImageResource(item.getIcon())
itemView.favicon.isClickable = false
itemView.overflow_menu.showAndEnable()
this.item = item
}
private fun toggleTopContent(
showTopContent: Boolean,
isNormalMode: Boolean
) {
itemView.delete_downloads_button.isVisible = showTopContent
if (showTopContent) {
itemView.delete_downloads_button.run {
if (isNormalMode) {
isEnabled = true
alpha = 1f
} else {
isEnabled = false
alpha = DELETE_BUTTON_DISABLED_ALPHA
}
}
}
}
private fun setupMenu() {
val downloadMenu = DownloadItemMenu(itemView.context) {
val item = this.item ?: return@DownloadItemMenu
if (it == DownloadItemMenu.Item.Delete) {
downloadInteractor.onDeleteSome(setOf(item))
}
}
itemView.download_layout.attachMenu(downloadMenu.menuController)
}
companion object {
const val DELETE_BUTTON_DISABLED_ALPHA = 0.4f
const val LAYOUT_ID = R.layout.download_list_item
}
}

@ -9,6 +9,13 @@
android:layout_width="match_parent" xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<com.google.android.material.button.MaterialButton
android:id="@+id/delete_downloads_button"
style="@style/DestructiveButton"
android:layout_marginHorizontal="16dp"
android:text="@string/download_delete_all"
android:visibility="gone" />
<org.mozilla.fenix.library.LibrarySiteItemView
android:id="@+id/download_layout"
android:layout_width="match_parent"

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/delete_downloads_multi_select"
android:title="@string/download_delete_item"
app:showAsAction="never" />
</menu>

@ -665,11 +665,22 @@
<string name="history_empty_message">No history here</string>
<!-- Downloads -->
<!-- Text for the button to clear all downloads -->
<string name="download_delete_all">Delete downloads</string>
<!-- Text for the dialog to confirm clearing all downloads -->
<string name="download_delete_all_dialog">Are you sure you want to clear your downloads?</string>
<!-- Text for the snackbar to confirm that multiple downloads items have been deleted -->
<string name="download_delete_multiple_items_snackbar">Downloads Deleted</string>
<!-- Text shown when no download exists -->
<string name="download_empty_message">No downloads here</string>
<!-- History multi select title in app bar
The first parameter is the number of downloads selected -->
<string name="download_multi_select_title">%1$d selected</string>
<!-- History overflow menu open in new tab button -->
<string name="download_menu_open">Open</string>
<!-- Text for the button to delete a single history item -->
<string name="download_delete_item">Delete</string>
<!-- Crashes -->
<!-- Title text displayed on the tab crash page. This first parameter is the name of the application (For example: Fenix) -->

@ -31,7 +31,7 @@ class DownloadAdapterTest {
}
@Test
fun `getItemCount should return the number of tab collections`() {
fun `getItemCount should return the number of downloads`() {
val download = mockk<DownloadItem>()
assertEquals(0, adapter.itemCount)

@ -26,9 +26,15 @@ class DownloadControllerTest {
private val store: DownloadFragmentStore = mockk(relaxed = true)
private val state: DownloadFragmentState = mockk(relaxed = true)
private val openToFileManager: (DownloadItem, BrowsingMode?) -> Unit = mockk(relaxed = true)
private val displayDeleteAll: () -> Unit = mockk(relaxed = true)
private val invalidateOptionsMenu: () -> Unit = mockk(relaxed = true)
private val deleteDownloadItems: (Set<DownloadItem>) -> Unit = mockk(relaxed = true)
private val controller = DefaultDownloadController(
store,
openToFileManager
openToFileManager,
displayDeleteAll,
invalidateOptionsMenu,
deleteDownloadItems
)
@Before
@ -65,4 +71,55 @@ class DownloadControllerTest {
assertFalse(controller.handleBackPressed())
}
@Test
fun onPressDownloadItemInEditMode() {
every { state.mode } returns DownloadFragmentState.Mode.Editing(setOf())
controller.handleSelect(downloadItem)
verify {
store.dispatch(DownloadFragmentAction.AddItemForRemoval(downloadItem))
}
}
@Test
fun onPressSelectedDownloadItemInEditMode() {
every { state.mode } returns DownloadFragmentState.Mode.Editing(setOf(downloadItem))
controller.handleDeselect(downloadItem)
verify {
store.dispatch(DownloadFragmentAction.RemoveItemForRemoval(downloadItem))
}
}
@Test
fun onModeSwitched() {
controller.handleModeSwitched()
verify {
invalidateOptionsMenu.invoke()
}
}
@Test
fun onDeleteAll() {
controller.handleDeleteAll()
verify {
displayDeleteAll.invoke()
}
}
@Test
fun onDeleteSome() {
val itemsToDelete = setOf(downloadItem)
controller.handleDeleteSome(itemsToDelete)
verify {
deleteDownloadItems(itemsToDelete)
}
}
}

@ -0,0 +1,70 @@
/* 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.downloads
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.state.state.content.DownloadState
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
import org.junit.Test
class DownloadFragmentStoreTest {
private val downloadItem = DownloadItem("0", "title", "url", "77", "jpg", DownloadState.Status.COMPLETED)
private val newDownloadItem = DownloadItem("1", "title", "url", "77", "jpg", DownloadState.Status.COMPLETED)
@Test
fun exitEditMode() = runBlocking {
val initialState = oneItemEditState()
val store = DownloadFragmentStore(initialState)
store.dispatch(DownloadFragmentAction.ExitEditMode).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.mode, DownloadFragmentState.Mode.Normal)
}
@Test
fun itemAddedForRemoval() = runBlocking {
val initialState = emptyDefaultState()
val store = DownloadFragmentStore(initialState)
store.dispatch(DownloadFragmentAction.AddItemForRemoval(newDownloadItem)).join()
assertNotSame(initialState, store.state)
assertEquals(
store.state.mode,
DownloadFragmentState.Mode.Editing(setOf(newDownloadItem))
)
}
@Test
fun removeItemForRemoval() = runBlocking {
val initialState = twoItemEditState()
val store = DownloadFragmentStore(initialState)
store.dispatch(DownloadFragmentAction.RemoveItemForRemoval(newDownloadItem)).join()
assertNotSame(initialState, store.state)
assertEquals(store.state.mode, DownloadFragmentState.Mode.Editing(setOf(downloadItem)))
}
private fun emptyDefaultState(): DownloadFragmentState = DownloadFragmentState(
items = listOf(),
mode = DownloadFragmentState.Mode.Normal,
pendingDeletionIds = emptySet(),
isDeletingItems = false
)
private fun oneItemEditState(): DownloadFragmentState = DownloadFragmentState(
items = listOf(),
mode = DownloadFragmentState.Mode.Editing(setOf(downloadItem)),
pendingDeletionIds = emptySet(),
isDeletingItems = false
)
private fun twoItemEditState(): DownloadFragmentState = DownloadFragmentState(
items = listOf(),
mode = DownloadFragmentState.Mode.Editing(setOf(downloadItem, newDownloadItem)),
pendingDeletionIds = emptySet(),
isDeletingItems = false
)
}

@ -25,6 +25,24 @@ class DownloadInteractorTest {
}
}
@Test
fun onSelect() {
interactor.select(downloadItem)
verifyAll {
controller.handleSelect(downloadItem)
}
}
@Test
fun onDeselect() {
interactor.deselect(downloadItem)
verifyAll {
controller.handleDeselect(downloadItem)
}
}
@Test
fun onBackPressed() {
every {
@ -38,4 +56,32 @@ class DownloadInteractorTest {
}
assertTrue(backpressHandled)
}
@Test
fun onModeSwitched() {
interactor.onModeSwitched()
verifyAll {
controller.handleModeSwitched()
}
}
@Test
fun onDeleteAll() {
interactor.onDeleteAll()
verifyAll {
controller.handleDeleteAll()
}
}
@Test
fun onDeleteSome() {
val items = setOf(downloadItem)
interactor.onDeleteSome(items)
verifyAll {
controller.handleDeleteSome(items)
}
}
}

Loading…
Cancel
Save