#2124 some style fixes to bookmarks activity and view model

This commit is contained in:
Frans-Lukas Lövenvald 2020-06-08 10:51:52 +02:00
parent ad3f533872
commit 5d18f04740
7 changed files with 391 additions and 70 deletions

View File

@ -42,7 +42,7 @@ class BookmarkViewModel @Inject constructor(
private val filter = BehaviorProcessor.createDefault("")
private var latestSearchString = ""
private val compositeDisposable = CompositeDisposable()
private val showAllSwitchToggle =
val showAllSwitchToggle =
BehaviorProcessor.createDefault(sharedPreferenceUtil.showBookmarksAllBooks)
init {

View File

@ -70,7 +70,8 @@ class Repository @Inject internal constructor(
HeaderizableList(it as List<BooksOnDiskListItem>).foldOverAddingHeaders(
{ bookOnDisk -> LanguageItem((bookOnDisk as BookOnDisk).locale) },
{ current, next ->
(current as BookOnDisk).locale.displayName != (next as BookOnDisk).locale.displayName })
(current as BookOnDisk).locale.displayName != (next as BookOnDisk).locale.displayName
})
}
.map { it.toList() }
@ -93,11 +94,12 @@ class Repository @Inject internal constructor(
zimReaderContainer.zimCanonicalPath
)
).map {
HeaderizableList(it as List<HistoryListItem>).foldOverAddingHeaders(
{ historyItem -> DateItem((historyItem as HistoryItem).dateString) },
{ current, next ->
(current as HistoryItem).dateString != (next as HistoryItem).dateString })
}
HeaderizableList(it as List<HistoryListItem>).foldOverAddingHeaders(
{ historyItem -> DateItem((historyItem as HistoryItem).dateString) },
{ current, next ->
(current as HistoryItem).dateString != (next as HistoryItem).dateString
})
}
.subscribeOn(io)
.observeOn(mainThread)

View File

@ -0,0 +1,22 @@
package org.kiwix.kiwixmobile.core.bookmark.viewmodel
import org.kiwix.kiwixmobile.core.bookmark.adapter.BookmarkItem
// dateFormat = d MMM yyyy
// 5 Jul 2020
fun createSimpleBookmarkItem(
bookmarkTitle: String = "bookmarkTitle",
isSelected: Boolean = false,
id: Long = 2
): BookmarkItem {
return BookmarkItem(
id,
"zimId",
"zimName",
"zimFilePath",
"bookmarkUrl",
bookmarkTitle,
"favicon",
isSelected
)
}

View File

@ -0,0 +1,332 @@
/*
* Kiwix Android
* Copyright (c) 2020 Kiwix <android.kiwix.org>
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.kiwix.kiwixmobile.core.bookmark.viewmodel
import DeleteSelectedOrAllBookmarkItems
import OpenBookmark
import com.jraska.livedata.test
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.mockk
import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler
import junit.framework.Assert
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.kiwix.kiwixmobile.core.base.SideEffect
import org.kiwix.kiwixmobile.core.bookmark.adapter.BookmarkItem
import org.kiwix.kiwixmobile.core.bookmark.viewmodel.Action.Filter
import org.kiwix.kiwixmobile.core.bookmark.viewmodel.State.NoResults
import org.kiwix.kiwixmobile.core.bookmark.viewmodel.State.Results
import org.kiwix.kiwixmobile.core.bookmark.viewmodel.State.SelectionResults
import org.kiwix.kiwixmobile.core.bookmark.viewmodel.effects.ToggleShowAllBookmarksSwitchAndSaveItsStateToPrefs
import org.kiwix.kiwixmobile.core.data.Repository
import org.kiwix.kiwixmobile.core.history.viewmodel.effects.ShowDeleteBookmarkDialog
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.Finish
import org.kiwix.kiwixmobile.core.utils.KiwixDialog
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.sharedFunctions.InstantExecutorExtension
import org.kiwix.sharedFunctions.setScheduler
import java.util.concurrent.TimeUnit
@ExtendWith(InstantExecutorExtension::class)
internal class BookmarkViewModelTest {
private val bookmarksRepository: Repository = mockk()
private val zimReaderContainer: ZimReaderContainer = mockk()
private val sharedPreferenceUtil: SharedPreferenceUtil = mockk()
lateinit var viewModel: BookmarkViewModel
private val testScheduler = TestScheduler()
private var latestItems: List<BookmarkItem> = listOf()
init {
setScheduler(testScheduler)
}
@BeforeEach
fun init() {
clearAllMocks()
every { zimReaderContainer.id } returns "id"
every { zimReaderContainer.name } returns "zimName"
every { sharedPreferenceUtil.showBookmarksAllBooks } returns true
every { bookmarksRepository.getBookmarks(any()) } returns Single.just(latestItems)
viewModel = BookmarkViewModel(bookmarksRepository, zimReaderContainer, sharedPreferenceUtil)
}
private fun resultsIn(st: State) {
viewModel.state.test()
.also { testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS) }
.assertValue(st)
}
private fun emissionOf(searchTerm: String, databaseResults: List<BookmarkItem>) {
latestItems = databaseResults
every { bookmarksRepository.getBookmarks(true) } returns Single.just(latestItems)
viewModel.actions.offer(Filter(searchTerm))
}
@Nested
inner class StateTests {
@Test
fun `initial state is Initialising`() {
viewModel.state.test().assertValue(NoResults(listOf()))
}
@Test
fun `non empty search term with search results shows Results`() {
val searchTerm = "searchTerm"
val item = createSimpleBookmarkItem("searchTermTitle")
emissionOf(
searchTerm = searchTerm,
databaseResults = listOf(item)
)
resultsIn(Results((listOf(item))))
}
@Test
fun `non empty search string with no search results is NoResults`() {
emissionOf(
searchTerm = "a",
databaseResults = listOf(
createSimpleBookmarkItem(
""
)
)
)
resultsIn(NoResults(emptyList()))
}
@Test
fun `empty search string with database results shows Results`() {
val item = createSimpleBookmarkItem()
emissionOf(searchTerm = "", databaseResults = listOf(item))
resultsIn(Results(listOf(item)))
}
@Test
fun `empty search string with no database results is NoResults`() {
emissionOf(
searchTerm = "",
databaseResults = emptyList()
)
resultsIn(NoResults(emptyList()))
}
@Test
fun `only latest search term is used`() {
val item =
createSimpleBookmarkItem("b")
emissionOf(
searchTerm = "a",
databaseResults = emptyList()
)
emissionOf(
searchTerm = "b",
databaseResults = listOf(item)
)
resultsIn(Results(listOf(item)))
}
@Test
fun `enters selection state if item is selected`() {
val item =
createSimpleBookmarkItem(
"b", isSelected = true
)
emissionOf(
searchTerm = "a",
databaseResults = emptyList()
)
emissionOf(
searchTerm = "b",
databaseResults = listOf(item)
)
resultsIn(SelectionResults(listOf(item)))
}
@Test
fun `OnItemLongClick enters selection state`() {
val item1 =
createSimpleBookmarkItem(
"a"
)
emissionOf(
searchTerm = "",
databaseResults = listOf(item1)
)
viewModel.actions.offer(Action.OnItemLongClick(item1))
item1.isSelected = true
resultsIn(SelectionResults(listOf(item1)))
}
@Test
fun `Deselection via OnItemClick exits selection state if last item is deselected`() {
val item1 = createSimpleBookmarkItem("a")
val item2 = createSimpleBookmarkItem("a")
emissionOf(searchTerm = "", databaseResults = listOf(item1, item2))
viewModel.actions.offer(Action.OnItemLongClick(item1))
viewModel.actions.offer(Action.OnItemClick(item1))
resultsIn(Results(listOf(item1, item2)))
}
@Test
fun `Deselection via OnItemLongClick exits selection state if last item is deselected`() {
val item1 = createSimpleBookmarkItem("a")
val item2 = createSimpleBookmarkItem("a")
emissionOf(searchTerm = "", databaseResults = listOf(item1, item2))
viewModel.actions.offer(Action.OnItemLongClick(item1))
viewModel.actions.offer(Action.OnItemLongClick(item1))
resultsIn(Results(listOf(item1, item2)))
}
@Test
fun `ExitActionMode deselects all items`() {
val item1 = createSimpleBookmarkItem("a", isSelected = true)
val item2 = createSimpleBookmarkItem("a", isSelected = true)
emissionOf(searchTerm = "", databaseResults = listOf(item1, item2))
viewModel.actions.offer(Action.ExitActionModeMenu)
item1.isSelected = false
item2.isSelected = false
resultsIn(Results(listOf(item1, item2)))
}
}
@Nested
inner class ActionMapping {
@Test
fun `ExitedSearch offers Finish`() {
actionResultsInEffects(Action.ExitBookmarks, Finish)
}
@Test
fun `ExitActionModeMenu deselects all history items from state`() {
val item1 = createSimpleBookmarkItem("a", isSelected = true)
emissionOf(searchTerm = "", databaseResults = listOf(item1))
viewModel.actions.offer(Action.ExitActionModeMenu)
assertItemIsDeselected(item1)
}
@Test
fun `OnItemLongClick selects history item from state`() {
val item1 = createSimpleBookmarkItem("a")
emissionOf(searchTerm = "", databaseResults = listOf(item1))
viewModel.actions.offer(Action.OnItemLongClick(item1))
assertItemIsSelected(item1)
}
@Test
fun `OnItemLongClick selects history item from state if in SelectionMode`() {
val item1 = createSimpleBookmarkItem("a", id = 2)
val item2 = createSimpleBookmarkItem("a", id = 3)
emissionOf(searchTerm = "", databaseResults = listOf(item1, item2))
viewModel.actions.offer(Action.OnItemLongClick(item1))
viewModel.actions.offer(Action.OnItemLongClick(item2))
assertItemIsSelected(item1)
assertItemIsSelected(item2)
}
@Test
fun `OnItemLongClick deselects history item from state if in SelectionMode`() {
val item1 = createSimpleBookmarkItem("a", id = 2)
emissionOf(searchTerm = "", databaseResults = listOf(item1))
viewModel.actions.offer(Action.OnItemLongClick(item1))
viewModel.actions.offer(Action.OnItemLongClick(item1))
assertItemIsDeselected(item1)
}
@Test
fun `OnItemClick selects history item from state if in SelectionMode`() {
val item1 = createSimpleBookmarkItem("a", id = 2)
val item2 = createSimpleBookmarkItem("a", id = 3)
emissionOf(searchTerm = "", databaseResults = listOf(item1, item2))
viewModel.actions.offer(Action.OnItemLongClick(item1))
viewModel.actions.offer(Action.OnItemClick(item2))
assertItemIsSelected(item1)
assertItemIsSelected(item2)
}
@Test
fun `OnItemClick offers OpenHistoryItem if not in selection mode `() {
val item1 = createSimpleBookmarkItem("a", id = 2)
emissionOf(searchTerm = "", databaseResults = listOf(item1))
actionResultsInEffects(Action.OnItemClick(item1), OpenBookmark(item1, zimReaderContainer))
}
@Test
fun `ToggleShowHistoryFromAllBooks switches show all books toggle`() {
actionResultsInEffects(
Action.ToggleShowBookmarksFromAllBooks(true),
ToggleShowAllBookmarksSwitchAndSaveItsStateToPrefs(
viewModel.showAllSwitchToggle,
sharedPreferenceUtil,
true
)
)
}
@Test
fun `RequestDeleteAllHistoryItems opens dialog to request deletion`() {
actionResultsInEffects(
Action.RequestDeleteAllBookmarks,
ShowDeleteBookmarkDialog(viewModel.actions, KiwixDialog.DeleteBookmarks)
)
}
@Test
fun `RequestDeleteSelectedBookmarks opens dialog to request deletion`() {
actionResultsInEffects(
Action.RequestDeleteSelectedBookmarks,
ShowDeleteBookmarkDialog(viewModel.actions, KiwixDialog.DeleteBookmarks)
)
}
@Test
fun `DeleteHistoryItems calls DeleteSelectedOrAllHistoryItems side effect`() {
actionResultsInEffects(
Action.DeleteBookmarks,
DeleteSelectedOrAllBookmarkItems(viewModel.state, bookmarksRepository, viewModel.actions)
)
}
private fun actionResultsInEffects(action: Action, vararg effects: SideEffect<*>) {
viewModel.effects.test().also { viewModel.actions.offer(action) }.assertValues(*effects)
}
private fun assertItemIsDeselected(item: BookmarkItem) {
Assert.assertFalse(
viewModel.state.value?.bookmarkItems?.find {
it.databaseId == item.databaseId
}?.isSelected == true
)
}
private fun assertItemIsSelected(item: BookmarkItem) {
Assert.assertTrue(
viewModel.state.value?.bookmarkItems?.find {
it.databaseId == item.databaseId
}?.isSelected == true
)
}
}
}

View File

@ -31,7 +31,7 @@ import org.kiwix.kiwixmobile.core.history.viewmodel.Action.RequestDeleteAllHisto
import org.kiwix.kiwixmobile.core.history.viewmodel.Action.RequestDeleteSelectedHistoryItems
import org.kiwix.kiwixmobile.core.history.viewmodel.Action.ToggleShowHistoryFromAllBooks
import org.kiwix.kiwixmobile.core.history.viewmodel.State.SelectionResults
import org.kiwix.kiwixmobile.core.history.viewmodel.effects.ShowDeleteBookmarkDialog
import org.kiwix.kiwixmobile.core.history.viewmodel.effects.ShowDeleteHistoryDialog
import org.kiwix.kiwixmobile.core.history.viewmodel.effects.ToggleShowAllHistorySwitchAndSaveItsStateToPrefs
import org.kiwix.kiwixmobile.core.search.viewmodel.effects.Finish
import org.kiwix.kiwixmobile.core.utils.KiwixDialog.DeleteAllHistory
@ -269,6 +269,7 @@ internal class HistoryViewModelTest {
viewModel.actions.offer(OnItemClick(item1))
resultsIn(Results(listOf(date, item1, item2)))
}
@Test
fun `Deselection via OnItemLongClick exits selection state if last item is deselected`() {
val item1 =
@ -320,46 +321,25 @@ internal class HistoryViewModelTest {
@Test
fun `ExitActionModeMenu deselects all history items from state`() {
val item1 =
createSimpleHistoryItem(
"a", "1 Aug 2020", isSelected = true
)
emissionOf(
searchTerm = "",
databaseResults = listOf(item1)
)
val item1 = createSimpleHistoryItem("a", "1 Aug 2020", isSelected = true)
emissionOf(searchTerm = "", databaseResults = listOf(item1))
viewModel.actions.offer(ExitActionModeMenu)
assertItemIsDeselected(item1)
}
@Test
fun `OnItemLongClick selects history item from state`() {
val item1 =
createSimpleHistoryItem(
"a", "1 Aug 2020"
)
emissionOf(
searchTerm = "",
databaseResults = listOf(item1)
)
val item1 = createSimpleHistoryItem("a", "1 Aug 2020")
emissionOf(searchTerm = "", databaseResults = listOf(item1))
viewModel.actions.offer(OnItemLongClick(item1))
assertItemIsSelected(item1)
}
@Test
fun `OnItemLongClick selects history item from state if in SelectionMode`() {
val item1 =
createSimpleHistoryItem(
"a", "1 Aug 2020", id = 2
)
val item2 =
createSimpleHistoryItem(
"b", "1 Aug 2020", id = 3
)
emissionOf(
searchTerm = "",
databaseResults = listOf(item1, item2)
)
val item1 = createSimpleHistoryItem("a", "1 Aug 2020", id = 2)
val item2 = createSimpleHistoryItem("a", "1 Aug 2020", id = 3)
emissionOf(searchTerm = "", databaseResults = listOf(item1, item2))
viewModel.actions.offer(OnItemLongClick(item1))
viewModel.actions.offer(OnItemLongClick(item2))
assertItemIsSelected(item1)
@ -371,6 +351,7 @@ internal class HistoryViewModelTest {
(viewModel.state.value?.historyItems?.find { it.id == item.id } as HistoryItem).isSelected
)
}
private fun assertItemIsDeselected(item: HistoryItem) {
assertFalse(
(viewModel.state.value?.historyItems?.find { it.id == item.id } as HistoryItem).isSelected
@ -379,14 +360,8 @@ internal class HistoryViewModelTest {
@Test
fun `OnItemLongClick deselects history item from state if in SelectionMode`() {
val item1 =
createSimpleHistoryItem(
"a", "1 Aug 2020", id = 2
)
emissionOf(
searchTerm = "",
databaseResults = listOf(item1)
)
val item1 = createSimpleHistoryItem("a", "1 Aug 2020", id = 2)
emissionOf(searchTerm = "", databaseResults = listOf(item1))
viewModel.actions.offer(OnItemLongClick(item1))
viewModel.actions.offer(OnItemLongClick(item1))
assertItemIsDeselected(item1)
@ -394,18 +369,9 @@ internal class HistoryViewModelTest {
@Test
fun `OnItemClick selects history item from state if in SelectionMode`() {
val item1 =
createSimpleHistoryItem(
"a", "1 Aug 2020", id = 2
)
val item2 =
createSimpleHistoryItem(
"b", "1 Aug 2020", id = 3
)
emissionOf(
searchTerm = "",
databaseResults = listOf(item1, item2)
)
val item1 = createSimpleHistoryItem("a", "1 Aug 2020", id = 2)
val item2 = createSimpleHistoryItem("b", "1 Aug 2020", id = 3)
emissionOf(searchTerm = "", databaseResults = listOf(item1, item2))
viewModel.actions.offer(OnItemLongClick(item1))
viewModel.actions.offer(OnItemClick(item2))
assertItemIsSelected(item1)
@ -414,14 +380,8 @@ internal class HistoryViewModelTest {
@Test
fun `OnItemClick offers OpenHistoryItem if not in selection mode `() {
val item1 =
createSimpleHistoryItem(
"a", "1 Aug 2020", id = 2
)
emissionOf(
searchTerm = "",
databaseResults = listOf(item1)
)
val item1 = createSimpleHistoryItem("a", "1 Aug 2020", id = 2)
emissionOf(searchTerm = "", databaseResults = listOf(item1))
actionResultsInEffects(OnItemClick(item1), OpenHistoryItem(item1, zimReaderContainer))
}
@ -432,28 +392,33 @@ internal class HistoryViewModelTest {
ToggleShowAllHistorySwitchAndSaveItsStateToPrefs(
viewModel.showAllSwitchToggle,
sharedPreferenceUtil,
true))
true
)
)
}
@Test
fun `RequestDeleteAllHistoryItems opens dialog to request deletion`() {
actionResultsInEffects(
RequestDeleteAllHistoryItems,
ShowDeleteBookmarkDialog(viewModel.actions, DeleteAllHistory))
ShowDeleteHistoryDialog(viewModel.actions, DeleteAllHistory)
)
}
@Test
fun `RequestDeleteSelectedHistoryItems opens dialog to request deletion`() {
actionResultsInEffects(
RequestDeleteSelectedHistoryItems,
ShowDeleteBookmarkDialog(viewModel.actions, DeleteSelectedHistory))
ShowDeleteHistoryDialog(viewModel.actions, DeleteSelectedHistory)
)
}
@Test
fun `DeleteHistoryItems calls DeleteSelectedOrAllHistoryItems side effect`() {
actionResultsInEffects(
DeleteHistoryItems,
DeleteSelectedOrAllHistoryItems(viewModel.state, historyDao))
DeleteSelectedOrAllHistoryItems(viewModel.state, historyDao)
)
}
private fun actionResultsInEffects(

View File

@ -18,7 +18,7 @@ internal class ShowDeleteHistoryDialogTest {
fun `invoke with shows dialog that offers ConfirmDelete action`() {
val actions = mockk<PublishProcessor<Action>>(relaxed = true)
val activity = mockk<HistoryActivity>()
val showDeleteHistoryDialog = ShowDeleteBookmarkDialog(actions, DeleteSelectedHistory)
val showDeleteHistoryDialog = ShowDeleteHistoryDialog(actions, DeleteSelectedHistory)
val dialogShower = mockk<DialogShower>()
every { activity.activityComponent.inject(showDeleteHistoryDialog) } answers {
showDeleteHistoryDialog.dialogShower = dialogShower

View File

@ -38,7 +38,7 @@ internal class ShowDeleteSearchDialogTest {
val actions = mockk<PublishProcessor<Action>>(relaxed = true)
val searchListItem = RecentSearchListItem("")
val activity = mockk<SearchActivity>()
val showDeleteSearchDialog = ShowDeleteSearchDialo(searchListItem, actions)
val showDeleteSearchDialog = ShowDeleteSearchDialog(searchListItem, actions)
val dialogShower = mockk<DialogShower>()
every { activity.activityComponent.inject(showDeleteSearchDialog) } answers {
showDeleteSearchDialog.dialogShower = dialogShower