From 03f771bef12ef316419fe995156c970e1c272ef6 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Fri, 16 May 2025 15:12:31 +0530 Subject: [PATCH 1/2] Refactored the `RxJava` to coroutines in `PageViewModel`. * Refactored the child viewModels (BookmarkViewModel, HistoryViewModel, and NotesViewModel) to use coroutines. * Refactored all DAO classes to use coroutines instead of RxJava. * Refactored all related code to align with coroutines. --- .../language/viewmodel/LanguageViewModel.kt | 25 ++-- .../kiwix/kiwixmobile/core/dao/HistoryDao.kt | 9 +- .../kiwixmobile/core/dao/HistoryRoomDao.kt | 7 +- .../kiwixmobile/core/dao/LibkiwixBookmarks.kt | 7 +- .../kiwixmobile/core/dao/NewBookmarksDao.kt | 22 ++-- .../kiwix/kiwixmobile/core/dao/NewNoteDao.kt | 9 +- .../kiwixmobile/core/dao/NotesRoomDao.kt | 9 +- .../org/kiwix/kiwixmobile/core/dao/PageDao.kt | 4 +- .../kiwixmobile/core/page/PageFragment.kt | 101 +++++++++------ .../bookmark/viewmodel/BookmarkViewModel.kt | 2 +- .../effects/ShowDeleteBookmarksDialog.kt | 6 +- .../history/viewmodel/HistoryViewModel.kt | 2 +- .../effects/ShowDeleteHistoryDialog.kt | 6 +- .../page/notes/viewmodel/NotesViewModel.kt | 2 +- .../effects/ShowDeleteNotesDialog.kt | 6 +- .../viewmodel/effects/ShowOpenNoteDialog.kt | 8 +- .../core/page/viewmodel/PageState.kt | 9 +- .../core/page/viewmodel/PageViewModel.kt | 68 +++++----- .../viewmodel/BookmarkViewModelTest.kt | 21 ++- .../effects/ShowDeleteBookmarksDialogTest.kt | 6 +- .../history/viewmodel/HistoryViewModelTest.kt | 19 ++- .../effects/ShowDeleteHistoryDialogTest.kt | 6 +- .../core/page/viewmodel/PageViewModelTest.kt | 120 ++++++++---------- 23 files changed, 241 insertions(+), 233 deletions(-) diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt index e78ffb302..434dcb871 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/viewmodel/LanguageViewModel.kt @@ -25,8 +25,9 @@ import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.onEach import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.NewLanguagesDao import org.kiwix.kiwixmobile.language.composables.LanguageListItem.LanguageItem @@ -55,21 +56,17 @@ class LanguageViewModel @Inject constructor( } private fun observeActions() = - viewModelScope.launch { - actions - .map { action -> reduce(action, state.value) } - .distinctUntilChanged() - .collect { newState -> state.value = newState } - } + actions + .map { action -> reduce(action, state.value) } + .distinctUntilChanged() + .onEach { newState -> state.value = newState } + .launchIn(viewModelScope) private fun observeLanguages() = - viewModelScope.launch { - languageDao.languages() - .filter { it.isNotEmpty() } - .collect { languages -> - actions.tryEmit(UpdateLanguages(languages)) - } - } + languageDao.languages() + .filter { it.isNotEmpty() } + .onEach { languages -> actions.tryEmit(UpdateLanguages(languages)) } + .launchIn(viewModelScope) override fun onCleared() { coroutineJobs.forEach { diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryDao.kt index ec0c180a6..daaf8b335 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryDao.kt @@ -20,7 +20,8 @@ package org.kiwix.kiwixmobile.core.dao import io.objectbox.Box import io.objectbox.kotlin.query import io.objectbox.query.QueryBuilder -import io.reactivex.Flowable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity import org.kiwix.kiwixmobile.core.dao.entities.HistoryEntity_ import org.kiwix.kiwixmobile.core.page.adapter.Page @@ -29,8 +30,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseV import javax.inject.Inject class HistoryDao @Inject constructor(val box: Box) : PageDao { - fun history(): Flowable> = - box.asFlowable( + fun history(): Flow> = + box.asFlow( box.query { orderDesc(HistoryEntity_.timeStamp) } @@ -46,7 +47,7 @@ class HistoryDao @Inject constructor(val box: Box) : PageDao { } } - override fun pages(): Flowable> = history() + override fun pages(): Flow> = history() override fun deletePages(pagesToDelete: List) = deleteHistory(pagesToDelete as List) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryRoomDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryRoomDao.kt index c0eecf80b..af52395f6 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryRoomDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/HistoryRoomDao.kt @@ -24,7 +24,8 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.TypeConverter import androidx.room.Update -import io.reactivex.Flowable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import org.kiwix.kiwixmobile.core.dao.entities.HistoryRoomEntity import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem @@ -33,9 +34,9 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource @Dao abstract class HistoryRoomDao : PageDao { @Query("SELECT * FROM HistoryRoomEntity ORDER BY HistoryRoomEntity.timeStamp DESC") - abstract fun historyRoomEntity(): Flowable> + abstract fun historyRoomEntity(): Flow> - fun history(): Flowable> = + fun history(): Flow> = historyRoomEntity().map { it.map { historyEntity -> historyEntity.zimFilePath?.let { filePath -> diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt index e8ddf3f86..e5e674e00 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/LibkiwixBookmarks.kt @@ -21,7 +21,6 @@ package org.kiwix.kiwixmobile.core.dao import android.os.Build import android.os.Environment import android.util.Base64 -import io.reactivex.Flowable import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -30,7 +29,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.reactive.asPublisher import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import org.kiwix.kiwixmobile.core.CoreApp @@ -121,10 +119,7 @@ class LibkiwixBookmarks @Inject constructor( bookmarkListFlow .map { it } - // Currently kept in RxJava Flowable because `PageViewModel` still expects RxJava streams. - // This can be refactored to use Kotlin Flow once `PageViewModel` is migrated to coroutines. - override fun pages(): Flowable> = - Flowable.fromPublisher(bookmarks().asPublisher()) + override fun pages(): Flow> = bookmarks() override fun deletePages(pagesToDelete: List) = deleteBookmarks(pagesToDelete as List) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookmarksDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookmarksDao.kt index 665be515d..298235249 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookmarksDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookmarksDao.kt @@ -20,8 +20,11 @@ package org.kiwix.kiwixmobile.core.dao import io.objectbox.Box import io.objectbox.kotlin.query import io.objectbox.query.QueryBuilder -import io.reactivex.Flowable -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import org.kiwix.kiwixmobile.core.dao.entities.BookmarkEntity import org.kiwix.kiwixmobile.core.dao.entities.BookmarkEntity_ import org.kiwix.kiwixmobile.core.page.adapter.Page @@ -31,8 +34,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseV import javax.inject.Inject class NewBookmarksDao @Inject constructor(val box: Box) : PageDao { - fun bookmarks(): Flowable> = - box.asFlowable( + fun bookmarks(): Flow> = + box.asFlow( box.query { order(BookmarkEntity_.bookmarkTitle) } @@ -48,7 +51,7 @@ class NewBookmarksDao @Inject constructor(val box: Box) : PageDa } } - override fun pages(): Flowable> = bookmarks() + override fun pages(): Flow> = bookmarks() override fun deletePages(pagesToDelete: List) = deleteBookmarks(pagesToDelete as List) @@ -71,8 +74,11 @@ class NewBookmarksDao @Inject constructor(val box: Box) : PageDa .toList() .distinct() - fun bookmarkUrlsForCurrentBook(zimFileReader: ZimFileReader?): Flowable> = - box.asFlowable( + fun bookmarkUrlsForCurrentBook( + zimFileReader: ZimFileReader?, + dispatcher: CoroutineDispatcher = Dispatchers.IO + ): Flow> = + box.asFlow( box.query { equal( BookmarkEntity_.zimId, @@ -88,7 +94,7 @@ class NewBookmarksDao @Inject constructor(val box: Box) : PageDa order(BookmarkEntity_.bookmarkTitle) } ).map { it.map(BookmarkEntity::bookmarkUrl) } - .subscribeOn(Schedulers.io()) + .flowOn(dispatcher) fun saveBookmark(bookmarkItem: BookmarkItem) { box.put(BookmarkEntity(bookmarkItem)) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewNoteDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewNoteDao.kt index 3d9e1b953..96685f4d7 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewNoteDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewNoteDao.kt @@ -21,7 +21,8 @@ package org.kiwix.kiwixmobile.core.dao import io.objectbox.Box import io.objectbox.kotlin.query import io.objectbox.query.QueryBuilder -import io.reactivex.Flowable +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity import org.kiwix.kiwixmobile.core.dao.entities.NotesEntity_ import org.kiwix.kiwixmobile.core.page.adapter.Page @@ -30,8 +31,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderSource.Companion.fromDatabaseV import javax.inject.Inject class NewNoteDao @Inject constructor(val box: Box) : PageDao { - fun notes(): Flowable> = - box.asFlowable( + fun notes(): Flow> = + box.asFlow( box.query { order(NotesEntity_.noteTitle) } @@ -47,7 +48,7 @@ class NewNoteDao @Inject constructor(val box: Box) : PageDao { } } - override fun pages(): Flowable> = notes() + override fun pages(): Flow> = notes() override fun deletePages(pagesToDelete: List) = deleteNotes(pagesToDelete as List) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NotesRoomDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NotesRoomDao.kt index 0df91e5f1..4e66bf125 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NotesRoomDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NotesRoomDao.kt @@ -22,10 +22,11 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import io.reactivex.Flowable import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.kiwix.kiwixmobile.core.dao.entities.NotesRoomEntity import org.kiwix.kiwixmobile.core.extensions.deleteFile @@ -38,9 +39,9 @@ import java.io.File @Dao abstract class NotesRoomDao : PageDao { @Query("SELECT * FROM NotesRoomEntity ORDER BY NotesRoomEntity.noteTitle") - abstract fun notesAsEntity(): Flowable> + abstract fun notesAsEntity(): Flow> - fun notes(): Flowable> = + fun notes(): Flow> = notesAsEntity().map { it.map { notesEntity -> notesEntity.zimFilePath?.let { filePath -> @@ -53,7 +54,7 @@ abstract class NotesRoomDao : PageDao { } } - override fun pages(): Flowable> = notes() + override fun pages(): Flow> = notes() override fun deletePages(pagesToDelete: List) = deleteNotes(pagesToDelete as List) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/PageDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/PageDao.kt index ac6091666..89428b063 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/PageDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/PageDao.kt @@ -18,10 +18,10 @@ package org.kiwix.kiwixmobile.core.dao -import io.reactivex.Flowable +import kotlinx.coroutines.flow.Flow import org.kiwix.kiwixmobile.core.page.adapter.Page interface PageDao { - fun pages(): Flowable> + fun pages(): Flow> fun deletePages(pagesToDelete: List) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragment.kt index f71cd2d42..400cf3e49 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/PageFragment.kt @@ -32,13 +32,16 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.referentialEqualityPolicy import androidx.compose.ui.platform.ComposeView -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider -import io.reactivex.disposables.CompositeDisposable +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.base.BaseFragment import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions import org.kiwix.kiwixmobile.core.downloader.downloadManager.ZERO +import org.kiwix.kiwixmobile.core.extensions.update import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.page.adapter.OnItemClickListener import org.kiwix.kiwixmobile.core.page.adapter.Page @@ -66,7 +69,7 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv @Inject lateinit var alertDialogShower: AlertDialogShower private var actionMode: ActionMode? = null - val compositeDisposable = CompositeDisposable() + private val coroutineJobs = mutableListOf() abstract val screenTitle: Int abstract val noItemsString: String abstract val switchString: String @@ -116,36 +119,51 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { if (item.itemId == R.id.menu_context_delete) { - pageViewModel.actions.offer(Action.UserClickedDeleteSelectedPages) + pageViewModel.actions.tryEmit(Action.UserClickedDeleteSelectedPages) return true } - pageViewModel.actions.offer(Action.ExitActionModeMenu) + pageViewModel.actions.tryEmit(Action.ExitActionModeMenu) return false } override fun onDestroyActionMode(mode: ActionMode) { - pageViewModel.actions.offer(Action.ExitActionModeMenu) + pageViewModel.actions.tryEmit(Action.ExitActionModeMenu) actionMode = null } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - pageScreenState.value = pageScreenState.value.copy( - searchQueryHint = searchQueryHint, - searchText = "", - searchValueChangedListener = { onTextChanged(it) }, - clearSearchButtonClickListener = { onTextChanged("") }, - screenTitle = screenTitle, - noItemsString = noItemsString, - switchString = switchString, - switchIsChecked = switchIsChecked, - onSwitchCheckedChanged = { onSwitchCheckedChanged(it).invoke() }, - deleteIconTitle = deleteIconTitle - ) + pageScreenState.update { + copy( + searchQueryHint = this@PageFragment.searchQueryHint, + searchText = "", + searchValueChangedListener = { onTextChanged(it) }, + clearSearchButtonClickListener = { onTextChanged("") }, + screenTitle = this@PageFragment.screenTitle, + noItemsString = this@PageFragment.noItemsString, + switchString = this@PageFragment.switchString, + switchIsChecked = this@PageFragment.switchIsChecked, + onSwitchCheckedChanged = { onSwitchChanged(it).invoke() }, + deleteIconTitle = this@PageFragment.deleteIconTitle + ) + } val activity = requireActivity() as CoreMainActivity - compositeDisposable.add(pageViewModel.effects.subscribe { it.invokeWith(activity) }) - pageViewModel.state.observe(viewLifecycleOwner, Observer(::render)) + cancelCoroutineJobs() + coroutineJobs.apply { + add( + pageViewModel + .effects + .onEach { it.invokeWith(activity) } + .launchIn(lifecycleScope) + ) + add( + pageViewModel + .state + .onEach { render(it) } + .launchIn(lifecycleScope) + ) + } pageViewModel.alertDialogShower = alertDialogShower } @@ -168,9 +186,9 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv isSearchActive = pageScreenState.value.isSearchActive, onSearchClick = { // Set the `isSearchActive` when the search button is clicked. - pageScreenState.value = pageScreenState.value.copy(isSearchActive = true) + pageScreenState.update { copy(isSearchActive = true) } }, - onDeleteClick = { pageViewModel.actions.offer(Action.UserClickedDeleteButton) } + onDeleteClick = { pageViewModel.actions.tryEmit(Action.UserClickedDeleteButton) } ) ) DialogHost(alertDialogShower) @@ -186,8 +204,8 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv * @param searchText The current text entered in the search bar. */ private fun onTextChanged(searchText: String) { - pageScreenState.value = pageScreenState.value.copy(searchText = searchText) - pageViewModel.actions.offer(Action.Filter(searchText)) + pageScreenState.update { copy(searchText = searchText) } + pageViewModel.actions.tryEmit(Action.Filter(searchText)) } /** @@ -197,9 +215,9 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv * * @param isChecked The new checked state of the switch. */ - private fun onSwitchCheckedChanged(isChecked: Boolean): () -> Unit = { - pageScreenState.value = pageScreenState.value.copy(switchIsChecked = isChecked) - pageViewModel.actions.offer(Action.UserClickedShowAllToggle(isChecked)) + private fun onSwitchChanged(isChecked: Boolean): () -> Unit = { + pageScreenState.update { copy(switchIsChecked = isChecked) } + pageViewModel.actions.tryEmit(Action.UserClickedShowAllToggle(isChecked)) } /** @@ -209,7 +227,7 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv */ private fun navigationIconClick(): () -> Unit = { if (pageScreenState.value.isSearchActive) { - pageScreenState.value = pageScreenState.value.copy(isSearchActive = false) + pageScreenState.update { copy(isSearchActive = false) } onTextChanged("") } else { requireActivity().onBackPressedDispatcher.onBackPressed() @@ -255,21 +273,20 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv override fun onDestroyView() { super.onDestroyView() - compositeDisposable.clear() + cancelCoroutineJobs() + } + + private fun cancelCoroutineJobs() { + coroutineJobs.forEach { + it.cancel() + } + coroutineJobs.clear() } private fun render(state: PageState<*>) { - pageScreenState.value = pageScreenState.value.copy( - switchIsEnabled = !state.isInSelectionState, - // First, assign the existing state to force Compose to recognize a change. - // This helps when internal properties of items (like `isSelected`) change, - // but the list reference itself remains the same — Compose won't detect it otherwise. - pageState = pageState.value - ) - // Then, assign the actual updated state to trigger full recomposition. - pageScreenState.value = pageScreenState.value.copy( - pageState = state - ) + pageScreenState.update { + copy(switchIsEnabled = !state.isInSelectionState, pageState = state) + } if (state.isInSelectionState) { if (actionMode == null) { actionMode = @@ -282,9 +299,9 @@ abstract class PageFragment : OnItemClickListener, BaseFragment(), FragmentActiv } override fun onItemClick(page: Page) { - pageViewModel.actions.offer(Action.OnItemClick(page)) + pageViewModel.actions.tryEmit(Action.OnItemClick(page)) } override fun onItemLongClick(page: Page): Boolean = - pageViewModel.actions.offer(Action.OnItemLongClick(page)) + pageViewModel.actions.tryEmit(Action.OnItemLongClick(page)) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModel.kt index 97dd73cd0..d8b4fa2ee 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModel.kt @@ -57,7 +57,7 @@ class BookmarkViewModel @Inject constructor( action: Action.UserClickedShowAllToggle, state: BookmarkState ): BookmarkState { - effects.offer(UpdateAllBookmarksPreference(sharedPreferenceUtil, action.isChecked)) + effects.tryEmit(UpdateAllBookmarksPreference(sharedPreferenceUtil, action.isChecked)) return state.copy(showAll = action.isChecked) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/effects/ShowDeleteBookmarksDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/effects/ShowDeleteBookmarksDialog.kt index e9dd9cdbf..6c68e2050 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/effects/ShowDeleteBookmarksDialog.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/effects/ShowDeleteBookmarksDialog.kt @@ -19,8 +19,8 @@ package org.kiwix.kiwixmobile.core.page.bookmark.viewmodel.effects */ import androidx.appcompat.app.AppCompatActivity -import io.reactivex.processors.PublishProcessor import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.PageDao import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent @@ -32,7 +32,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteAllBookmarks import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedBookmarks data class ShowDeleteBookmarksDialog( - private val effects: PublishProcessor>, + private val effects: MutableSharedFlow>, private val state: PageState, private val pageDao: PageDao, private val viewModelScope: CoroutineScope, @@ -42,7 +42,7 @@ data class ShowDeleteBookmarksDialog( activity.cachedComponent.inject(this) dialogShower.show( if (state.isInSelectionState) DeleteSelectedBookmarks else DeleteAllBookmarks, - { effects.offer(DeletePageItems(state, pageDao, viewModelScope)) } + { effects.tryEmit(DeletePageItems(state, pageDao, viewModelScope)) } ) } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModel.kt index 8c97c3181..7e7c37a17 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModel.kt @@ -53,7 +53,7 @@ class HistoryViewModel @Inject constructor( action: Action.UserClickedShowAllToggle, state: HistoryState ): HistoryState { - effects.offer(UpdateAllHistoryPreference(sharedPreferenceUtil, action.isChecked)) + effects.tryEmit(UpdateAllHistoryPreference(sharedPreferenceUtil, action.isChecked)) return state.copy(showAll = action.isChecked) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/effects/ShowDeleteHistoryDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/effects/ShowDeleteHistoryDialog.kt index 4b54bd7a6..21e99f82c 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/effects/ShowDeleteHistoryDialog.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/effects/ShowDeleteHistoryDialog.kt @@ -19,8 +19,8 @@ package org.kiwix.kiwixmobile.core.page.history.viewmodel.effects */ import androidx.appcompat.app.AppCompatActivity -import io.reactivex.processors.PublishProcessor import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.PageDao import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent @@ -31,7 +31,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteAllHistory import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedHistory data class ShowDeleteHistoryDialog( - private val effects: PublishProcessor>, + private val effects: MutableSharedFlow>, private val state: HistoryState, private val pageDao: PageDao, private val viewModelScope: CoroutineScope, @@ -40,7 +40,7 @@ data class ShowDeleteHistoryDialog( override fun invokeWith(activity: AppCompatActivity) { activity.cachedComponent.inject(this) dialogShower.show(if (state.isInSelectionState) DeleteSelectedHistory else DeleteAllHistory, { - effects.offer(DeletePageItems(state, pageDao, viewModelScope)) + effects.tryEmit(DeletePageItems(state, pageDao, viewModelScope)) }) } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesViewModel.kt index ae0ae3b7e..dbb74fa04 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/NotesViewModel.kt @@ -55,7 +55,7 @@ class NotesViewModel @Inject constructor( action: Action.UserClickedShowAllToggle, state: NotesState ): NotesState { - effects.offer(UpdateAllNotesPreference(sharedPreferenceUtil, action.isChecked)) + effects.tryEmit(UpdateAllNotesPreference(sharedPreferenceUtil, action.isChecked)) return state.copy(showAll = action.isChecked) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowDeleteNotesDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowDeleteNotesDialog.kt index a3b76858a..858e96471 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowDeleteNotesDialog.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowDeleteNotesDialog.kt @@ -19,8 +19,8 @@ package org.kiwix.kiwixmobile.core.page.notes.viewmodel.effects */ import androidx.appcompat.app.AppCompatActivity -import io.reactivex.processors.PublishProcessor import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.PageDao import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent @@ -31,7 +31,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteAllNotes import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedNotes data class ShowDeleteNotesDialog( - private val effects: PublishProcessor>, + private val effects: MutableSharedFlow>, private val state: NotesState, private val pageDao: PageDao, private val viewModelScope: CoroutineScope, @@ -42,7 +42,7 @@ data class ShowDeleteNotesDialog( dialogShower.show( if (state.isInSelectionState) DeleteSelectedNotes else DeleteAllNotes, { - effects.offer(DeletePageItems(state, pageDao, viewModelScope)) + effects.tryEmit(DeletePageItems(state, pageDao, viewModelScope)) } ) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowOpenNoteDialog.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowOpenNoteDialog.kt index 82fb549c2..a2f0715da 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowOpenNoteDialog.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/notes/viewmodel/effects/ShowOpenNoteDialog.kt @@ -19,7 +19,7 @@ package org.kiwix.kiwixmobile.core.page.notes.viewmodel.effects import androidx.appcompat.app.AppCompatActivity -import io.reactivex.processors.PublishProcessor +import kotlinx.coroutines.flow.MutableSharedFlow import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.extensions.ActivityExtensions.cachedComponent import org.kiwix.kiwixmobile.core.page.adapter.Page @@ -31,7 +31,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.DialogShower import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.ShowNoteDialog data class ShowOpenNoteDialog( - private val effects: PublishProcessor>, + private val effects: MutableSharedFlow>, private val page: Page, private val zimReaderContainer: ZimReaderContainer, private val dialogShower: DialogShower @@ -40,10 +40,10 @@ data class ShowOpenNoteDialog( activity.cachedComponent.inject(this) dialogShower.show( ShowNoteDialog, - { effects.offer(OpenPage(page, zimReaderContainer)) }, + { effects.tryEmit(OpenPage(page, zimReaderContainer)) }, { val item = page as NoteListItem - effects.offer(OpenNote(item)) + effects.tryEmit(OpenNote(item)) } ) } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt index 7957da624..0bc898e7e 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt @@ -21,6 +21,8 @@ package org.kiwix.kiwixmobile.core.page.viewmodel import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.adapter.PageRelated import org.kiwix.kiwixmobile.core.page.bookmark.adapter.LibkiwixBookmarkItem +import org.kiwix.kiwixmobile.core.page.history.adapter.HistoryListItem.HistoryItem +import org.kiwix.kiwixmobile.core.page.notes.adapter.NoteListItem abstract class PageState { abstract val pageItems: List @@ -43,8 +45,11 @@ abstract class PageState { val currentItemIdentifier = if (it is LibkiwixBookmarkItem) it.url else it.id val pageIdentifier = if (it is LibkiwixBookmarkItem) page.url else page.id if (currentItemIdentifier == pageIdentifier) { - it.apply { - isSelected = !isSelected + when (it) { + is LibkiwixBookmarkItem -> it.copy(isSelected = !it.isSelected) as T + is HistoryItem -> it.copy(isSelected = !it.isSelected) as T + is NoteListItem -> it.copy(isSelected = !it.isSelected) as T + else -> it } } else { it diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt index 2f7a1b6dc..5171316cc 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt @@ -18,14 +18,20 @@ package org.kiwix.kiwixmobile.core.page.viewmodel -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import io.reactivex.disposables.CompositeDisposable -import io.reactivex.disposables.Disposable -import io.reactivex.processors.PublishProcessor -import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.PageDao import org.kiwix.kiwixmobile.core.page.adapter.Page @@ -54,32 +60,29 @@ abstract class PageViewModel>( lateinit var alertDialogShower: AlertDialogShower private lateinit var pageViewModelClickListener: PageViewModelClickListener + private val _state = MutableStateFlow(initialState()) + val state: StateFlow = _state.asStateFlow() + val effects = MutableSharedFlow>(extraBufferCapacity = Int.MAX_VALUE) + val actions = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) + private val coroutineJobs = mutableListOf() - val state: MutableLiveData by lazy { - MutableLiveData().apply { - value = initialState() + init { + coroutineJobs.apply { + add(observeActions()) + add(observePages()) } } - private val compositeDisposable = CompositeDisposable() - val effects = PublishProcessor.create>() - val actions = PublishProcessor.create() + private fun observeActions() = + actions.map { action -> reduce(action, state.value) } + .onEach { newState -> _state.value = newState } + .launchIn(viewModelScope) - init { - addDisposablesToCompositeDisposable() - } - - private fun viewStateReducer(): Disposable = - actions.map { state.value?.let { value -> reduce(it, value) } } - .subscribe(state::postValue, Throwable::printStackTrace) - - protected fun addDisposablesToCompositeDisposable() { - compositeDisposable.addAll( - viewStateReducer(), - pageDao.pages().subscribeOn(Schedulers.io()) - .subscribe({ actions.offer(UpdatePages(it)) }, Throwable::printStackTrace) - ) - } + private fun observePages(dispatcher: CoroutineDispatcher = Dispatchers.IO) = + pageDao.pages() + .flowOn(dispatcher) + .onEach { actions.tryEmit(UpdatePages(it)) } + .launchIn(viewModelScope) private fun reduce(action: Action, state: S): S = when (action) { @@ -103,7 +106,7 @@ abstract class PageViewModel>( ): S private fun offerShowDeleteDialog(state: S): S { - effects.offer(createDeletePageDialogEffect(state, viewModelScope = viewModelScope)) + effects.tryEmit(createDeletePageDialogEffect(state, viewModelScope = viewModelScope)) return state } @@ -117,9 +120,9 @@ abstract class PageViewModel>( return copyWithNewItems(state, state.getItemsAfterToggleSelectionOfItem(action.page)) } if (::pageViewModelClickListener.isInitialized) { - effects.offer(pageViewModelClickListener.onItemClick(action.page)) + effects.tryEmit(pageViewModelClickListener.onItemClick(action.page)) } else { - effects.offer(OpenPage(action.page, zimReaderContainer)) + effects.tryEmit(OpenPage(action.page, zimReaderContainer)) } return state } @@ -131,12 +134,15 @@ abstract class PageViewModel>( abstract fun deselectAllPages(state: S): S private fun exitFragment(state: S): S { - effects.offer(PopFragmentBackstack) + effects.tryEmit(PopFragmentBackstack) return state } override fun onCleared() { - compositeDisposable.clear() + coroutineJobs.forEach { + it.cancel() + } + coroutineJobs.clear() super.onCleared() } diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModelTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModelTest.kt index 978a0b298..26ffa04f8 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModelTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModelTest.kt @@ -21,11 +21,9 @@ package org.kiwix.kiwixmobile.core.page.bookmark.viewmodel import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk -import io.reactivex.Flowable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.reactive.asPublisher import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -34,7 +32,6 @@ import org.junit.jupiter.api.extension.ExtendWith import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.bookmark.viewmodel.effects.ShowDeleteBookmarksDialog -import org.kiwix.kiwixmobile.core.page.bookmark.viewmodel.effects.UpdateAllBookmarksPreference import org.kiwix.kiwixmobile.core.page.bookmarkState import org.kiwix.kiwixmobile.core.page.libkiwixBookmarkItem import org.kiwix.kiwixmobile.core.page.viewmodel.Action @@ -67,9 +64,7 @@ internal class BookmarkViewModelTest { every { zimReaderContainer.name } returns "zimName" every { sharedPreferenceUtil.showBookmarksAllBooks } returns true every { libkiwixBookMarks.bookmarks() } returns itemsFromDb - every { libkiwixBookMarks.pages() } returns Flowable.fromPublisher( - libkiwixBookMarks.bookmarks().asPublisher() - ) + every { libkiwixBookMarks.pages() } returns libkiwixBookMarks.bookmarks() viewModel = BookmarkViewModel(libkiwixBookMarks, zimReaderContainer, sharedPreferenceUtil).apply { alertDialogShower = dialogShower @@ -106,13 +101,13 @@ internal class BookmarkViewModelTest { } @Test - fun `offerUpdateToShowAllToggle offers UpdateAllBookmarksPreference`() { - viewModel.effects.test().also { - viewModel.offerUpdateToShowAllToggle( - Action.UserClickedShowAllToggle(false), - bookmarkState() - ) - }.assertValues(UpdateAllBookmarksPreference(sharedPreferenceUtil, false)) + fun `offerUpdateToShowAllToggle offers UpdateAllBookmarksPreference`() = runTest { + // viewModel.effects.test().also { + // viewModel.offerUpdateToShowAllToggle( + // Action.UserClickedShowAllToggle(false), + // bookmarkState() + // ) + // }.assertValues(UpdateAllBookmarksPreference(sharedPreferenceUtil, false)) } @Test diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/effects/ShowDeleteBookmarksDialogTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/effects/ShowDeleteBookmarksDialogTest.kt index 3ab2e6058..7de2c1adf 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/effects/ShowDeleteBookmarksDialogTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/effects/ShowDeleteBookmarksDialogTest.kt @@ -22,9 +22,9 @@ import io.mockk.every import io.mockk.mockk import io.mockk.slot import io.mockk.verify -import io.reactivex.processors.PublishProcessor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.base.SideEffect @@ -40,7 +40,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedBookmar import java.util.UUID internal class ShowDeleteBookmarksDialogTest { - val effects = mockk>>(relaxed = true) + val effects = mockk>>(relaxed = true) private val newBookmarksDao = mockk() val activity = mockk() private val dialogShower = mockk(relaxed = true) @@ -61,7 +61,7 @@ internal class ShowDeleteBookmarksDialogTest { showDeleteBookmarksDialog.invokeWith(activity) verify { dialogShower.show(any(), capture(lambdaSlot)) } lambdaSlot.captured.invoke() - verify { effects.offer(DeletePageItems(bookmarkState(), newBookmarksDao, viewModelScope)) } + verify { effects.tryEmit(DeletePageItems(bookmarkState(), newBookmarksDao, viewModelScope)) } } private fun mockkActivityInjection(showDeleteBookmarksDialog: ShowDeleteBookmarksDialog) { diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModelTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModelTest.kt index 4d94568e9..3971d944d 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModelTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModelTest.kt @@ -4,11 +4,11 @@ import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk import io.reactivex.plugins.RxJavaPlugins -import io.reactivex.processors.PublishProcessor import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.TestScheduler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runTest import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach @@ -17,7 +17,6 @@ import org.junit.jupiter.api.extension.ExtendWith import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.history.viewmodel.effects.ShowDeleteHistoryDialog -import org.kiwix.kiwixmobile.core.page.history.viewmodel.effects.UpdateAllHistoryPreference import org.kiwix.kiwixmobile.core.page.historyItem import org.kiwix.kiwixmobile.core.page.historyState import org.kiwix.kiwixmobile.core.page.viewmodel.Action.Filter @@ -47,8 +46,8 @@ internal class HistoryViewModelTest { RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } } - private val itemsFromDb: PublishProcessor> = - PublishProcessor.create() + private val itemsFromDb: MutableSharedFlow> = + MutableSharedFlow>(0) @BeforeEach fun init() { @@ -90,12 +89,12 @@ internal class HistoryViewModelTest { @Test fun `offerUpdateToShowAllToggle offers UpdateAllHistoryPreference`() { - viewModel.effects.test().also { - viewModel.offerUpdateToShowAllToggle( - UserClickedShowAllToggle(false), - historyState() - ) - }.assertValues(UpdateAllHistoryPreference(sharedPreferenceUtil, false)) + // viewModel.effects.test().also { + // viewModel.offerUpdateToShowAllToggle( + // UserClickedShowAllToggle(false), + // historyState() + // ) + // }.assertValues(UpdateAllHistoryPreference(sharedPreferenceUtil, false)) } @Test diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/effects/ShowDeleteHistoryDialogTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/effects/ShowDeleteHistoryDialogTest.kt index a5036d6fd..e93010936 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/effects/ShowDeleteHistoryDialogTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/effects/ShowDeleteHistoryDialogTest.kt @@ -4,9 +4,9 @@ import io.mockk.every import io.mockk.mockk import io.mockk.slot import io.mockk.verify -import io.reactivex.processors.PublishProcessor import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.Test import org.kiwix.kiwixmobile.core.base.SideEffect @@ -20,7 +20,7 @@ import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteAllHistory import org.kiwix.kiwixmobile.core.utils.dialog.KiwixDialog.DeleteSelectedHistory internal class ShowDeleteHistoryDialogTest { - val effects = mockk>>(relaxed = true) + val effects = mockk>>(relaxed = true) private val historyDao = mockk() val activity = mockk() private val dialogShower = mockk(relaxed = true) @@ -42,7 +42,7 @@ internal class ShowDeleteHistoryDialogTest { showDeleteHistoryDialog.invokeWith(activity) verify { dialogShower.show(any(), capture(lambdaSlot)) } lambdaSlot.captured.invoke() - verify { effects.offer(DeletePageItems(historyState(), historyDao, viewModelScope)) } + verify { effects.tryEmit(DeletePageItems(historyState(), historyDao, viewModelScope)) } } @Test diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt index 44a849e18..c73c1d516 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt @@ -18,41 +18,25 @@ package org.kiwix.kiwixmobile.core.page.viewmodel -import com.jraska.livedata.test import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk import io.reactivex.plugins.RxJavaPlugins -import io.reactivex.processors.PublishProcessor import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.TestScheduler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain -import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.kiwix.kiwixmobile.core.dao.PageDao -import org.kiwix.kiwixmobile.core.page.PageImpl import org.kiwix.kiwixmobile.core.page.adapter.Page -import org.kiwix.kiwixmobile.core.page.pageState -import org.kiwix.kiwixmobile.core.page.viewmodel.Action.Exit -import org.kiwix.kiwixmobile.core.page.viewmodel.Action.ExitActionModeMenu -import org.kiwix.kiwixmobile.core.page.viewmodel.Action.Filter -import org.kiwix.kiwixmobile.core.page.viewmodel.Action.OnItemClick -import org.kiwix.kiwixmobile.core.page.viewmodel.Action.OnItemLongClick -import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UpdatePages -import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedDeleteButton -import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedDeleteSelectedPages -import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedShowAllToggle -import org.kiwix.kiwixmobile.core.page.viewmodel.effects.OpenPage import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer -import org.kiwix.kiwixmobile.core.reader.ZimReaderSource -import org.kiwix.kiwixmobile.core.search.viewmodel.effects.PopFragmentBackstack import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.sharedFunctions.InstantExecutorExtension import org.kiwix.sharedFunctions.setScheduler @@ -66,8 +50,8 @@ internal class PageViewModelTest { private lateinit var viewModel: TestablePageViewModel private val testScheduler = TestScheduler() - private val itemsFromDb: PublishProcessor> = - PublishProcessor.create() + private val itemsFromDb: MutableSharedFlow> = + MutableSharedFlow>(0) init { setScheduler(testScheduler) @@ -92,94 +76,94 @@ internal class PageViewModelTest { @Test fun `initial state is Initialising`() { - viewModel.state.test().assertValue(pageState()) + // viewModel.state.test().assertValue(pageState()) } @Test fun `Exit calls PopFragmentBackstack`() { - viewModel.effects.test().also { viewModel.actions.offer(Exit) } - .assertValue(PopFragmentBackstack) - viewModel.state.test().assertValue(pageState()) + // viewModel.effects.test().also { viewModel.actions.offer(Exit) } + // .assertValue(PopFragmentBackstack) + // viewModel.state.test().assertValue(pageState()) } @Test fun `ExitActionModeMenu calls deslectAllPages`() { - viewModel.actions.offer(ExitActionModeMenu) - viewModel.state.test().assertValue(TestablePageState(searchTerm = "deselectAllPagesCalled")) + // viewModel.actions.offer(ExitActionModeMenu) + // viewModel.state.test().assertValue(TestablePageState(searchTerm = "deselectAllPagesCalled")) } @Test fun `UserClickedShowAllToggle calls offerUpdateToShowAllToggle`() { - val action = UserClickedShowAllToggle(true) - viewModel.actions.offer(action) - viewModel.state.test() - .assertValue(TestablePageState(searchTerm = "offerUpdateToShowAllToggleCalled")) + // val action = UserClickedShowAllToggle(true) + // viewModel.actions.offer(action) + // viewModel.state.test() + // .assertValue(TestablePageState(searchTerm = "offerUpdateToShowAllToggleCalled")) } @Test fun `UserClickedDeleteButton calls createDeletePageDialogEffect`() { - viewModel.actions.offer(UserClickedDeleteButton) - assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) + // viewModel.actions.offer(UserClickedDeleteButton) + // assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) } @Test fun `UserClickedDeleteSelectedPages calls createDeletePageDialogEffect`() { - viewModel.actions.offer(UserClickedDeleteSelectedPages) - assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) + // viewModel.actions.offer(UserClickedDeleteSelectedPages) + // assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) } @Test internal fun `OnItemClick selects item if one is selected`() { - val zimReaderSource: ZimReaderSource = mockk() - val page = PageImpl(isSelected = true, zimReaderSource = zimReaderSource) - viewModel.state.postValue(TestablePageState(listOf(page))) - viewModel.actions.offer(OnItemClick(page)) - viewModel.state.test() - .assertValue(TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))) + // val zimReaderSource: ZimReaderSource = mockk() + // val page = PageImpl(isSelected = true, zimReaderSource = zimReaderSource) + // viewModel.state.postValue(TestablePageState(listOf(page))) + // viewModel.actions.offer(OnItemClick(page)) + // viewModel.state.test() + // .assertValue(TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))) } @Test internal fun `OnItemClick offers OpenPage if none is selected`() { - val zimReaderSource: ZimReaderSource = mockk() - viewModel.state.postValue( - TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource))) - ) - viewModel.effects.test() - .also { viewModel.actions.offer(OnItemClick(PageImpl(zimReaderSource = zimReaderSource))) } - .assertValue(OpenPage(PageImpl(zimReaderSource = zimReaderSource), zimReaderContainer)) - viewModel.state.test() - .assertValue(TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))) + // val zimReaderSource: ZimReaderSource = mockk() + // viewModel.state.postValue( + // TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource))) + // ) + // viewModel.effects.test() + // .also { viewModel.actions.offer(OnItemClick(PageImpl(zimReaderSource = zimReaderSource))) } + // .assertValue(OpenPage(PageImpl(zimReaderSource = zimReaderSource), zimReaderContainer)) + // viewModel.state.test() + // .assertValue(TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))) } @Test internal fun `OnItemLongClick selects item if none is selected`() { - val zimReaderSource: ZimReaderSource = mockk() - val page = PageImpl(zimReaderSource = zimReaderSource) - viewModel.state.postValue(TestablePageState(listOf(page))) - viewModel.actions.offer(OnItemLongClick(page)) - viewModel.state.test().assertValue( - TestablePageState( - listOf( - PageImpl( - isSelected = true, - zimReaderSource = zimReaderSource - ) - ) - ) - ) + // val zimReaderSource: ZimReaderSource = mockk() + // val page = PageImpl(zimReaderSource = zimReaderSource) + // viewModel.state.postValue(TestablePageState(listOf(page))) + // viewModel.actions.offer(OnItemLongClick(page)) + // viewModel.state.test().assertValue( + // TestablePageState( + // listOf( + // PageImpl( + // isSelected = true, + // zimReaderSource = zimReaderSource + // ) + // ) + // ) + // ) } @Test fun `Filter calls updatePagesBasedOnFilter`() { - viewModel.actions.offer(Filter("Called")) - viewModel.state.test() - .assertValue(TestablePageState(searchTerm = "updatePagesBasedOnFilterCalled")) + // viewModel.actions.offer(Filter("Called")) + // viewModel.state.test() + // .assertValue(TestablePageState(searchTerm = "updatePagesBasedOnFilterCalled")) } @Test fun `UpdatePages calls updatePages`() { - viewModel.actions.offer(UpdatePages(emptyList())) - viewModel.state.test() - .assertValue(TestablePageState(searchTerm = "updatePagesCalled")) + // viewModel.actions.offer(UpdatePages(emptyList())) + // viewModel.state.test() + // .assertValue(TestablePageState(searchTerm = "updatePagesCalled")) } } From 913068505d42a0e326a58258a25c9825271947c2 Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Fri, 16 May 2025 17:38:18 +0530 Subject: [PATCH 2/2] Refactored the all unit, and UI test cases. * Removed the some unused code from project. --- .../kiwixmobile/KiwixRoomDatabaseTest.kt | 20 +- .../ObjectBoxToRoomMigratorTest.kt | 30 +-- .../kiwixmobile/roomDao/HistoryRoomDaoTest.kt | 21 +- .../kiwixmobile/roomDao/NoteRoomDaoTest.kt | 21 +- .../kiwixmobile/language/LanguageFragment.kt | 3 - .../kiwixmobile/core/base/BasePresenter.kt | 5 - .../kiwixmobile/core/dao/NewLanguagesDao.kt | 9 - .../core/page/viewmodel/PageState.kt | 3 +- .../core/page/viewmodel/PageViewModel.kt | 4 + .../viewmodel/BookmarkViewModelTest.kt | 25 +- .../history/viewmodel/HistoryViewModelTest.kt | 34 +-- .../core/page/viewmodel/PageViewModelTest.kt | 226 ++++++++++++------ .../core/utils/files/FileSearchTest.kt | 8 - 13 files changed, 245 insertions(+), 164 deletions(-) diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/KiwixRoomDatabaseTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/KiwixRoomDatabaseTest.kt index 73ccbf444..edab6c479 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/KiwixRoomDatabaseTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/KiwixRoomDatabaseTest.kt @@ -112,7 +112,7 @@ class KiwixRoomDatabaseTest { // test inserting into history database historyRoomDao.saveHistory(historyItem) - var historyList = historyRoomDao.historyRoomEntity().blockingFirst() + var historyList = historyRoomDao.historyRoomEntity().first() with(historyList.first()) { assertThat(historyTitle, equalTo(historyItem.title)) assertThat(zimId, equalTo(historyItem.zimId)) @@ -126,7 +126,7 @@ class KiwixRoomDatabaseTest { // test deleting the history historyRoomDao.deleteHistory(listOf(historyItem)) - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertEquals(historyList.size, 0) // test deleting all history @@ -134,10 +134,10 @@ class KiwixRoomDatabaseTest { historyRoomDao.saveHistory( getHistoryItem(databaseId = 2) ) - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertEquals(historyList.size, 2) historyRoomDao.deleteAllHistory() - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertEquals(historyList.size, 0) } @@ -146,7 +146,7 @@ class KiwixRoomDatabaseTest { runBlocking { notesRoomDao = db.notesRoomDao() // delete all the notes from database to properly run the test cases. - notesRoomDao.deleteNotes(notesRoomDao.notes().blockingFirst() as List) + notesRoomDao.deleteNotes(notesRoomDao.notes().first() as List) val noteItem = getNoteListItem( zimUrl = "http://kiwix.app/MainPage", @@ -155,7 +155,7 @@ class KiwixRoomDatabaseTest { // Save and retrieve a notes item notesRoomDao.saveNote(noteItem) - var notesList = notesRoomDao.notes().blockingFirst() as List + var notesList = notesRoomDao.notes().first() as List with(notesList.first()) { assertThat(zimId, equalTo(noteItem.zimId)) assertThat(zimUrl, equalTo(noteItem.zimUrl)) @@ -168,7 +168,7 @@ class KiwixRoomDatabaseTest { // test deleting the history notesRoomDao.deleteNotes(listOf(noteItem)) - notesList = notesRoomDao.notes().blockingFirst() as List + notesList = notesRoomDao.notes().first() as List assertEquals(notesList.size, 0) // test deleting all notes @@ -179,10 +179,10 @@ class KiwixRoomDatabaseTest { zimUrl = "http://kiwix.app/Installing" ) ) - notesList = notesRoomDao.notes().blockingFirst() as List + notesList = notesRoomDao.notes().first() as List assertEquals(notesList.size, 2) - notesRoomDao.deletePages(notesRoomDao.notes().blockingFirst()) - notesList = notesRoomDao.notes().blockingFirst() as List + notesRoomDao.deletePages(notesRoomDao.notes().first()) + notesList = notesRoomDao.notes().first() as List assertEquals(notesList.size, 0) } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/ObjectBoxToRoomMigratorTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/ObjectBoxToRoomMigratorTest.kt index 0ea1de241..6f1a350f9 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/ObjectBoxToRoomMigratorTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/ObjectBoxToRoomMigratorTest.kt @@ -215,7 +215,7 @@ class ObjectBoxToRoomMigratorTest { kiwixRoomDatabase.recentSearchRoomDao().deleteSearchHistory() kiwixRoomDatabase.historyRoomDao().deleteAllHistory() kiwixRoomDatabase.notesRoomDao() - .deletePages(kiwixRoomDatabase.notesRoomDao().notes().blockingFirst()) + .deletePages(kiwixRoomDatabase.notesRoomDao().notes().first()) box.removeAll() } @@ -238,7 +238,7 @@ class ObjectBoxToRoomMigratorTest { // migrate data into room database objectBoxToRoomMigrator.migrateHistory(box) // check if data successfully migrated to room - val actual = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() + val actual = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first() with(actual.first()) { assertThat(historyTitle, equalTo(historyItem.title)) assertThat(zimId, equalTo(historyItem.zimId)) @@ -254,7 +254,7 @@ class ObjectBoxToRoomMigratorTest { // Migrate data from empty ObjectBox database objectBoxToRoomMigrator.migrateHistory(box) - var actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() + var actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first() assertTrue(actualData.isEmpty()) // Test if data successfully migrated to Room and existing data is preserved @@ -262,7 +262,7 @@ class ObjectBoxToRoomMigratorTest { box.put(HistoryEntity(historyItem2)) // Migrate data into Room database objectBoxToRoomMigrator.migrateHistory(box) - actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() + actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first() assertEquals(2, actualData.size) val existingItem = actualData.find { @@ -281,7 +281,7 @@ class ObjectBoxToRoomMigratorTest { kiwixRoomDatabase.historyRoomDao().saveHistory(historyItem) box.put(HistoryEntity(historyItem)) objectBoxToRoomMigrator.migrateHistory(box) - actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() + actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first() assertEquals(1, actualData.size) clearRoomAndBoxStoreDatabases(box) @@ -296,7 +296,7 @@ class ObjectBoxToRoomMigratorTest { kiwixRoomDatabase.historyRoomDao().saveHistory(historyItem4) box.put(HistoryEntity(historyItem)) objectBoxToRoomMigrator.migrateHistory(box) - actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() + actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first() assertEquals(2, actualData.size) clearRoomAndBoxStoreDatabases(box) @@ -310,7 +310,7 @@ class ObjectBoxToRoomMigratorTest { } catch (_: Exception) { } // Ensure Room database remains empty or unaffected by the invalid data - actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() + actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first() assertTrue(actualData.isEmpty()) // Test large data migration for recent searches @@ -332,7 +332,7 @@ class ObjectBoxToRoomMigratorTest { val endTime = System.currentTimeMillis() val migrationTime = endTime - startTime // Check if data successfully migrated to Room - actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().blockingFirst() + actualData = kiwixRoomDatabase.historyRoomDao().historyRoomEntity().first() assertEquals(numEntities, actualData.size) // Assert that the migration completes within a reasonable time frame assertTrue( @@ -367,7 +367,7 @@ class ObjectBoxToRoomMigratorTest { // migrate data into room database objectBoxToRoomMigrator.migrateNotes(box) // check if data successfully migrated to room - var notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List + var notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List with(notesList.first()) { assertThat(zimId, equalTo(noteItem.zimId)) assertThat(zimUrl, equalTo(noteItem.zimUrl)) @@ -382,7 +382,7 @@ class ObjectBoxToRoomMigratorTest { // Migrate data from empty ObjectBox database objectBoxToRoomMigrator.migrateNotes(box) - notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List + notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List assertTrue(notesList.isEmpty()) // Test if data successfully migrated to Room and existing data is preserved @@ -390,7 +390,7 @@ class ObjectBoxToRoomMigratorTest { box.put(NotesEntity(noteItem)) // Migrate data into Room database objectBoxToRoomMigrator.migrateNotes(box) - notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List + notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List assertEquals(noteItem.title, notesList.first().title) assertEquals(2, notesList.size) val existingItem = @@ -411,7 +411,7 @@ class ObjectBoxToRoomMigratorTest { box.put(NotesEntity(noteItem1)) // Migrate data into Room database objectBoxToRoomMigrator.migrateNotes(box) - notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List + notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List assertEquals(1, notesList.size) clearRoomAndBoxStoreDatabases(box) @@ -426,7 +426,7 @@ class ObjectBoxToRoomMigratorTest { kiwixRoomDatabase.notesRoomDao().saveNote(noteItem1) box.put(NotesEntity(noteItem2)) objectBoxToRoomMigrator.migrateNotes(box) - notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List + notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List assertEquals(2, notesList.size) clearRoomAndBoxStoreDatabases(box) @@ -440,7 +440,7 @@ class ObjectBoxToRoomMigratorTest { } catch (_: Exception) { } // Ensure Room database remains empty or unaffected by the invalid data - notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List + notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List assertTrue(notesList.isEmpty()) // Test large data migration for recent searches @@ -462,7 +462,7 @@ class ObjectBoxToRoomMigratorTest { val endTime = System.currentTimeMillis() val migrationTime = endTime - startTime // Check if data successfully migrated to Room - notesList = kiwixRoomDatabase.notesRoomDao().notes().blockingFirst() as List + notesList = kiwixRoomDatabase.notesRoomDao().notes().first() as List assertEquals(numEntities, notesList.size) // Assert that the migration completes within a reasonable time frame assertTrue( diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/roomDao/HistoryRoomDaoTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/roomDao/HistoryRoomDaoTest.kt index 79e1da8c1..7c9d6abab 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/roomDao/HistoryRoomDaoTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/roomDao/HistoryRoomDaoTest.kt @@ -22,6 +22,7 @@ import android.content.Context import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo @@ -66,7 +67,7 @@ class HistoryRoomDaoTest { // Save and retrieve a history item historyRoomDao.saveHistory(historyItem) - var historyList = historyRoomDao.historyRoomEntity().blockingFirst() + var historyList = historyRoomDao.historyRoomEntity().first() with(historyList.first()) { assertThat(historyTitle, equalTo(historyItem.title)) assertThat(zimId, equalTo(historyItem.zimId)) @@ -80,26 +81,26 @@ class HistoryRoomDaoTest { // Test to update the same day history for url historyRoomDao.saveHistory(historyItem) - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertEquals(historyList.size, 1) // Delete the saved history item historyRoomDao.deleteHistory(listOf(historyItem)) - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertEquals(historyList.size, 0) // Save and delete all history items historyRoomDao.saveHistory(historyItem) historyRoomDao.saveHistory(getHistoryItem(databaseId = 2, dateString = "31 May 2024")) historyRoomDao.deleteAllHistory() - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertThat(historyList.size, equalTo(0)) // Save history item with empty fields val emptyHistoryUrl = "" val emptyTitle = "" historyRoomDao.saveHistory(getHistoryItem(emptyTitle, emptyHistoryUrl, databaseId = 1)) - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertThat(historyList.size, equalTo(1)) historyRoomDao.deleteAllHistory() @@ -113,14 +114,14 @@ class HistoryRoomDaoTest { dateString = "31 May 2024" ) historyRoomDao.saveHistory(historyItem1) - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertThat(historyList.size, equalTo(2)) historyRoomDao.deleteAllHistory() // Save two entity with same and database id with same date to see if it's updated or not. historyRoomDao.saveHistory(historyItem) historyRoomDao.saveHistory(historyItem) - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertThat(historyList.size, equalTo(1)) historyRoomDao.deleteAllHistory() @@ -132,7 +133,7 @@ class HistoryRoomDaoTest { "Undefined value was saved into database", false ) - } catch (e: Exception) { + } catch (_: Exception) { assertThat("Undefined value was not saved, as expected.", true) } @@ -140,13 +141,13 @@ class HistoryRoomDaoTest { val unicodeTitle = "title \u03A3" // Unicode character for Greek capital letter Sigma val historyItem2 = getHistoryItem(title = unicodeTitle, databaseId = 2) historyRoomDao.saveHistory(historyItem2) - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertThat(historyList.first().historyTitle, equalTo("title Σ")) // Test deletePages function historyRoomDao.saveHistory(historyItem) historyRoomDao.deletePages(listOf(historyItem, historyItem2)) - historyList = historyRoomDao.historyRoomEntity().blockingFirst() + historyList = historyRoomDao.historyRoomEntity().first() assertThat(historyList.size, equalTo(0)) } } diff --git a/app/src/androidTest/java/org/kiwix/kiwixmobile/roomDao/NoteRoomDaoTest.kt b/app/src/androidTest/java/org/kiwix/kiwixmobile/roomDao/NoteRoomDaoTest.kt index 332f011b5..1c3d08f13 100644 --- a/app/src/androidTest/java/org/kiwix/kiwixmobile/roomDao/NoteRoomDaoTest.kt +++ b/app/src/androidTest/java/org/kiwix/kiwixmobile/roomDao/NoteRoomDaoTest.kt @@ -22,6 +22,7 @@ import android.content.Context import androidx.room.Room import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo @@ -65,7 +66,7 @@ class NoteRoomDaoTest { // Save and retrieve a notes item notesRoomDao.saveNote(noteItem) - var notesList = notesRoomDao.notes().blockingFirst() as List + var notesList = notesRoomDao.notes().first() as List with(notesList.first()) { assertThat(zimId, equalTo(noteItem.zimId)) assertThat(zimUrl, equalTo(noteItem.zimUrl)) @@ -78,25 +79,25 @@ class NoteRoomDaoTest { // Test update the existing note notesRoomDao.saveNote(noteItem) - notesList = notesRoomDao.notes().blockingFirst() as List + notesList = notesRoomDao.notes().first() as List assertEquals(notesList.size, 1) // Delete the saved note item with all delete methods available in NoteRoomDao. // delete via noteTitle notesRoomDao.deleteNote(noteItem.title) - notesList = notesRoomDao.notes().blockingFirst() as List + notesList = notesRoomDao.notes().first() as List assertEquals(notesList.size, 0) // delete with deletePages method notesRoomDao.saveNote(noteItem) notesRoomDao.deletePages(listOf(noteItem)) - notesList = notesRoomDao.notes().blockingFirst() as List + notesList = notesRoomDao.notes().first() as List assertEquals(notesList.size, 0) // delete with list of NoteListItem notesRoomDao.saveNote(noteItem) notesRoomDao.deleteNotes(listOf(noteItem)) - notesList = notesRoomDao.notes().blockingFirst() as List + notesList = notesRoomDao.notes().first() as List assertEquals(notesList.size, 0) // Save note with empty title @@ -107,7 +108,7 @@ class NoteRoomDaoTest { noteFilePath = "/storage/emulated/0/Download/Notes/Alpine linux/Installing.txt" ) ) - notesList = notesRoomDao.notes().blockingFirst() as List + notesList = notesRoomDao.notes().first() as List assertEquals(notesList.size, 1) clearNotes() @@ -127,7 +128,7 @@ class NoteRoomDaoTest { ) kiwixRoomDatabase.notesRoomDao().saveNote(noteItem2) kiwixRoomDatabase.notesRoomDao().saveNote(noteItem3) - notesList = notesRoomDao.notes().blockingFirst() as List + notesList = notesRoomDao.notes().first() as List assertEquals(2, notesList.size) clearNotes() @@ -139,7 +140,7 @@ class NoteRoomDaoTest { "Undefined value was saved into database", false ) - } catch (e: Exception) { + } catch (_: Exception) { assertThat("Undefined value was not saved, as expected.", true) } @@ -148,11 +149,11 @@ class NoteRoomDaoTest { val noteListItem2 = getNoteListItem(title = unicodeTitle, zimUrl = "http://kiwix.app/Installing") notesRoomDao.saveNote(noteListItem2) - notesList = notesRoomDao.notes().blockingFirst() as List + notesList = notesRoomDao.notes().first() as List assertThat(notesList.first().title, equalTo("title Σ")) } private suspend fun clearNotes() { - notesRoomDao.deleteNotes(notesRoomDao.notes().blockingFirst() as List) + notesRoomDao.deleteNotes(notesRoomDao.notes().first() as List) } } diff --git a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt index b1f0f7810..84963aacf 100644 --- a/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt +++ b/app/src/main/java/org/kiwix/kiwixmobile/language/LanguageFragment.kt @@ -31,7 +31,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.ViewModelProvider -import io.reactivex.disposables.CompositeDisposable import org.kiwix.kiwixmobile.cachedComponent import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.base.BaseActivity @@ -55,7 +54,6 @@ class LanguageFragment : BaseFragment() { @Inject lateinit var viewModelFactory: ViewModelProvider.Factory private var composeView: ComposeView? = null - private val compositeDisposable = CompositeDisposable() override fun inject(baseActivity: BaseActivity) { baseActivity.cachedComponent.inject(this) @@ -153,7 +151,6 @@ class LanguageFragment : BaseFragment() { override fun onDestroyView() { super.onDestroyView() - compositeDisposable.clear() composeView?.disposeComposition() composeView = null } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/base/BasePresenter.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/base/BasePresenter.kt index 51cfb6e35..0db426ea6 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/base/BasePresenter.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/base/BasePresenter.kt @@ -17,7 +17,6 @@ */ package org.kiwix.kiwixmobile.core.base -import io.reactivex.disposables.CompositeDisposable import org.kiwix.kiwixmobile.core.base.BaseContract.Presenter import org.kiwix.kiwixmobile.core.base.BaseContract.View @@ -26,9 +25,6 @@ import org.kiwix.kiwixmobile.core.base.BaseContract.View */ @Suppress("UnnecessaryAbstractClass") abstract class BasePresenter?> : Presenter { - @JvmField - val compositeDisposable = CompositeDisposable() - @JvmField var view: T? = null @@ -38,6 +34,5 @@ abstract class BasePresenter?> : Presenter { override fun detachView() { view = null - compositeDisposable.clear() } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewLanguagesDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewLanguagesDao.kt index a603b4311..fcf536612 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewLanguagesDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewLanguagesDao.kt @@ -21,9 +21,6 @@ import io.objectbox.Box import io.objectbox.kotlin.flow import io.objectbox.kotlin.query import io.objectbox.query.Query -import io.objectbox.rx.RxQuery -import io.reactivex.BackpressureStrategy -import io.reactivex.BackpressureStrategy.LATEST import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -53,9 +50,3 @@ fun Box.asFlow(query: Query = query {}): Flow> { .map { it.toList() } .distinctUntilChanged() } - -internal fun Box.asFlowable( - query: Query = query {}, - backpressureStrategy: BackpressureStrategy = LATEST -) = - RxQuery.observable(query).toFlowable(backpressureStrategy) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt index 0bc898e7e..10ab0408a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageState.kt @@ -49,7 +49,8 @@ abstract class PageState { is LibkiwixBookmarkItem -> it.copy(isSelected = !it.isSelected) as T is HistoryItem -> it.copy(isSelected = !it.isSelected) as T is NoteListItem -> it.copy(isSelected = !it.isSelected) as T - else -> it + // For test cases only. + else -> it.apply { isSelected = !isSelected } } } else { it diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt index 5171316cc..1548cd977 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModel.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import org.jetbrains.annotations.VisibleForTesting import org.kiwix.kiwixmobile.core.base.SideEffect import org.kiwix.kiwixmobile.core.dao.PageDao import org.kiwix.kiwixmobile.core.page.adapter.Page @@ -66,6 +67,9 @@ abstract class PageViewModel>( val actions = MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) private val coroutineJobs = mutableListOf() + @VisibleForTesting + fun getMutableStateForTestCases() = _state + init { coroutineJobs.apply { add(observeActions()) diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModelTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModelTest.kt index 26ffa04f8..56a1cbd2b 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModelTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/page/bookmark/viewmodel/BookmarkViewModelTest.kt @@ -32,6 +32,7 @@ import org.junit.jupiter.api.extension.ExtendWith import org.kiwix.kiwixmobile.core.dao.LibkiwixBookmarks import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.bookmark.viewmodel.effects.ShowDeleteBookmarksDialog +import org.kiwix.kiwixmobile.core.page.bookmark.viewmodel.effects.UpdateAllBookmarksPreference import org.kiwix.kiwixmobile.core.page.bookmarkState import org.kiwix.kiwixmobile.core.page.libkiwixBookmarkItem import org.kiwix.kiwixmobile.core.page.viewmodel.Action @@ -41,6 +42,7 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower +import org.kiwix.kiwixmobile.core.utils.files.testFlow import org.kiwix.sharedFunctions.InstantExecutorExtension import java.util.UUID @@ -102,12 +104,23 @@ internal class BookmarkViewModelTest { @Test fun `offerUpdateToShowAllToggle offers UpdateAllBookmarksPreference`() = runTest { - // viewModel.effects.test().also { - // viewModel.offerUpdateToShowAllToggle( - // Action.UserClickedShowAllToggle(false), - // bookmarkState() - // ) - // }.assertValues(UpdateAllBookmarksPreference(sharedPreferenceUtil, false)) + testFlow( + flow = viewModel.effects, + triggerAction = { + viewModel.offerUpdateToShowAllToggle( + Action.UserClickedShowAllToggle(false), + bookmarkState() + ) + }, + assert = { + assertThat(awaitItem()).isEqualTo( + UpdateAllBookmarksPreference( + sharedPreferenceUtil, + false + ) + ) + } + ) } @Test diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModelTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModelTest.kt index 3971d944d..983ce2de7 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModelTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/page/history/viewmodel/HistoryViewModelTest.kt @@ -3,9 +3,6 @@ package org.kiwix.kiwixmobile.core.page.history.viewmodel import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk -import io.reactivex.plugins.RxJavaPlugins -import io.reactivex.schedulers.Schedulers -import io.reactivex.schedulers.TestScheduler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow @@ -17,6 +14,7 @@ import org.junit.jupiter.api.extension.ExtendWith import org.kiwix.kiwixmobile.core.dao.HistoryRoomDao import org.kiwix.kiwixmobile.core.page.adapter.Page import org.kiwix.kiwixmobile.core.page.history.viewmodel.effects.ShowDeleteHistoryDialog +import org.kiwix.kiwixmobile.core.page.history.viewmodel.effects.UpdateAllHistoryPreference import org.kiwix.kiwixmobile.core.page.historyItem import org.kiwix.kiwixmobile.core.page.historyState import org.kiwix.kiwixmobile.core.page.viewmodel.Action.Filter @@ -26,8 +24,8 @@ import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil import org.kiwix.kiwixmobile.core.utils.dialog.AlertDialogShower +import org.kiwix.kiwixmobile.core.utils.files.testFlow import org.kiwix.sharedFunctions.InstantExecutorExtension -import org.kiwix.sharedFunctions.setScheduler @ExtendWith(InstantExecutorExtension::class) internal class HistoryViewModelTest { @@ -38,14 +36,8 @@ internal class HistoryViewModelTest { private val viewModelScope = CoroutineScope(Dispatchers.IO) private lateinit var viewModel: HistoryViewModel - private val testScheduler = TestScheduler() private val zimReaderSource: ZimReaderSource = mockk() - init { - setScheduler(testScheduler) - RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } - } - private val itemsFromDb: MutableSharedFlow> = MutableSharedFlow>(0) @@ -88,13 +80,21 @@ internal class HistoryViewModelTest { } @Test - fun `offerUpdateToShowAllToggle offers UpdateAllHistoryPreference`() { - // viewModel.effects.test().also { - // viewModel.offerUpdateToShowAllToggle( - // UserClickedShowAllToggle(false), - // historyState() - // ) - // }.assertValues(UpdateAllHistoryPreference(sharedPreferenceUtil, false)) + fun `offerUpdateToShowAllToggle offers UpdateAllHistoryPreference`() = runTest { + testFlow( + flow = viewModel.effects, + triggerAction = { + viewModel.offerUpdateToShowAllToggle( + UserClickedShowAllToggle(false), + historyState() + ) + }, + assert = { + assertThat(awaitItem()).isEqualTo( + UpdateAllHistoryPreference(sharedPreferenceUtil, false) + ) + } + ) } @Test diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt index c73c1d516..3e92a3654 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/page/viewmodel/PageViewModelTest.kt @@ -21,25 +21,39 @@ package org.kiwix.kiwixmobile.core.page.viewmodel import io.mockk.clearAllMocks import io.mockk.every import io.mockk.mockk -import io.reactivex.plugins.RxJavaPlugins -import io.reactivex.schedulers.Schedulers -import io.reactivex.schedulers.TestScheduler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.kiwix.kiwixmobile.core.dao.PageDao +import org.kiwix.kiwixmobile.core.page.PageImpl import org.kiwix.kiwixmobile.core.page.adapter.Page +import org.kiwix.kiwixmobile.core.page.pageState +import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UpdatePages +import org.kiwix.kiwixmobile.core.page.viewmodel.Action.Filter +import org.kiwix.kiwixmobile.core.page.viewmodel.Action.Exit +import org.kiwix.kiwixmobile.core.page.viewmodel.Action.ExitActionModeMenu +import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedShowAllToggle +import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedDeleteButton +import org.kiwix.kiwixmobile.core.page.viewmodel.Action.UserClickedDeleteSelectedPages +import org.kiwix.kiwixmobile.core.page.viewmodel.Action.OnItemClick +import org.kiwix.kiwixmobile.core.page.viewmodel.Action.OnItemLongClick +import org.kiwix.kiwixmobile.core.page.viewmodel.effects.OpenPage import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer +import org.kiwix.kiwixmobile.core.reader.ZimReaderSource +import org.kiwix.kiwixmobile.core.search.viewmodel.effects.PopFragmentBackstack import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil +import org.kiwix.kiwixmobile.core.utils.files.testFlow import org.kiwix.sharedFunctions.InstantExecutorExtension -import org.kiwix.sharedFunctions.setScheduler @OptIn(ExperimentalCoroutinesApi::class) @ExtendWith(InstantExecutorExtension::class) @@ -49,15 +63,9 @@ internal class PageViewModelTest { private val sharedPreferenceUtil: SharedPreferenceUtil = mockk() private lateinit var viewModel: TestablePageViewModel - private val testScheduler = TestScheduler() private val itemsFromDb: MutableSharedFlow> = MutableSharedFlow>(0) - init { - setScheduler(testScheduler) - RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() } - } - @BeforeEach fun init() { Dispatchers.setMain(UnconfinedTestDispatcher()) @@ -75,95 +83,173 @@ internal class PageViewModelTest { } @Test - fun `initial state is Initialising`() { - // viewModel.state.test().assertValue(pageState()) + fun `initial state is Initialising`() = runTest { + testFlow( + flow = viewModel.state, + triggerAction = {}, + assert = { assertThat(awaitItem()).isEqualTo(pageState()) } + ) } @Test - fun `Exit calls PopFragmentBackstack`() { - // viewModel.effects.test().also { viewModel.actions.offer(Exit) } - // .assertValue(PopFragmentBackstack) - // viewModel.state.test().assertValue(pageState()) + fun `Exit calls PopFragmentBackstack`() = runTest { + testFlow( + flow = viewModel.effects, + triggerAction = { viewModel.actions.tryEmit(Exit) }, + assert = { assertThat(awaitItem()).isEqualTo(PopFragmentBackstack) } + ) + testFlow( + flow = viewModel.state, + triggerAction = {}, + assert = { assertThat(awaitItem()).isEqualTo(pageState()) } + ) } @Test - fun `ExitActionModeMenu calls deslectAllPages`() { - // viewModel.actions.offer(ExitActionModeMenu) - // viewModel.state.test().assertValue(TestablePageState(searchTerm = "deselectAllPagesCalled")) + fun `ExitActionModeMenu calls deslectAllPages`() = runTest { + testFlow( + flow = viewModel.state, + triggerAction = { viewModel.actions.tryEmit(ExitActionModeMenu) }, + assert = { + assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = "")) + assertThat(awaitItem()) + .isEqualTo(TestablePageState(searchTerm = "deselectAllPagesCalled")) + } + ) } @Test - fun `UserClickedShowAllToggle calls offerUpdateToShowAllToggle`() { - // val action = UserClickedShowAllToggle(true) - // viewModel.actions.offer(action) - // viewModel.state.test() - // .assertValue(TestablePageState(searchTerm = "offerUpdateToShowAllToggleCalled")) + fun `UserClickedShowAllToggle calls offerUpdateToShowAllToggle`() = runTest { + testFlow( + flow = viewModel.state, + triggerAction = { + viewModel.actions.tryEmit(UserClickedShowAllToggle(true)) + }, + assert = { + assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = "")) + assertThat(awaitItem()) + .isEqualTo(TestablePageState(searchTerm = "offerUpdateToShowAllToggleCalled")) + } + ) } @Test - fun `UserClickedDeleteButton calls createDeletePageDialogEffect`() { - // viewModel.actions.offer(UserClickedDeleteButton) - // assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) + fun `UserClickedDeleteButton calls createDeletePageDialogEffect`() = runTest { + viewModel.actions.tryEmit(UserClickedDeleteButton) + advanceUntilIdle() + assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) } @Test - fun `UserClickedDeleteSelectedPages calls createDeletePageDialogEffect`() { - // viewModel.actions.offer(UserClickedDeleteSelectedPages) - // assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) + fun `UserClickedDeleteSelectedPages calls createDeletePageDialogEffect`() = runTest { + viewModel.actions.tryEmit(UserClickedDeleteSelectedPages) + advanceUntilIdle() + assertThat(viewModel.createDeletePageDialogEffectCalled).isEqualTo(true) } @Test - internal fun `OnItemClick selects item if one is selected`() { - // val zimReaderSource: ZimReaderSource = mockk() - // val page = PageImpl(isSelected = true, zimReaderSource = zimReaderSource) - // viewModel.state.postValue(TestablePageState(listOf(page))) - // viewModel.actions.offer(OnItemClick(page)) - // viewModel.state.test() - // .assertValue(TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))) + internal fun `OnItemClick selects item if one is selected`() = runTest { + val zimReaderSource: ZimReaderSource = mockk() + testFlow( + viewModel.state, + triggerAction = { + val page = PageImpl(isSelected = true, zimReaderSource = zimReaderSource) + viewModel.getMutableStateForTestCases().value = TestablePageState(listOf(page)) + viewModel.actions.tryEmit(OnItemClick(page)) + }, + assert = { + assertThat(awaitItem()).isEqualTo(TestablePageState()) + assertThat(awaitItem()) + .isEqualTo( + TestablePageState( + listOf(PageImpl(zimReaderSource = zimReaderSource)) + ) + ) + } + ) } @Test - internal fun `OnItemClick offers OpenPage if none is selected`() { - // val zimReaderSource: ZimReaderSource = mockk() - // viewModel.state.postValue( - // TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource))) - // ) - // viewModel.effects.test() - // .also { viewModel.actions.offer(OnItemClick(PageImpl(zimReaderSource = zimReaderSource))) } - // .assertValue(OpenPage(PageImpl(zimReaderSource = zimReaderSource), zimReaderContainer)) - // viewModel.state.test() - // .assertValue(TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource)))) + internal fun `OnItemClick offers OpenPage if none is selected`() = runTest { + val zimReaderSource: ZimReaderSource = mockk() + testFlow( + viewModel.effects, + triggerAction = { + viewModel.getMutableStateForTestCases().value = + TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource))) + viewModel.actions.tryEmit(OnItemClick(PageImpl(zimReaderSource = zimReaderSource))) + }, + assert = { + assertThat(awaitItem()).isEqualTo( + OpenPage( + PageImpl(zimReaderSource = zimReaderSource), + zimReaderContainer + ) + ) + } + ) + testFlow( + viewModel.state, + triggerAction = { + viewModel.getMutableStateForTestCases().value = + TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource))) + viewModel.actions.tryEmit(OnItemClick(PageImpl(zimReaderSource = zimReaderSource))) + }, + assert = { + assertThat(awaitItem()).isEqualTo( + TestablePageState(listOf(PageImpl(zimReaderSource = zimReaderSource))) + ) + } + ) } @Test - internal fun `OnItemLongClick selects item if none is selected`() { - // val zimReaderSource: ZimReaderSource = mockk() - // val page = PageImpl(zimReaderSource = zimReaderSource) - // viewModel.state.postValue(TestablePageState(listOf(page))) - // viewModel.actions.offer(OnItemLongClick(page)) - // viewModel.state.test().assertValue( - // TestablePageState( - // listOf( - // PageImpl( - // isSelected = true, - // zimReaderSource = zimReaderSource - // ) - // ) - // ) - // ) + internal fun `OnItemLongClick selects item if none is selected`() = runTest { + val zimReaderSource: ZimReaderSource = mockk() + val page = PageImpl(zimReaderSource = zimReaderSource) + testFlow( + viewModel.state, + triggerAction = { + viewModel.getMutableStateForTestCases().value = TestablePageState(listOf(page)) + viewModel.actions.tryEmit(OnItemLongClick(page)) + }, + assert = { + assertThat(awaitItem()).isEqualTo(TestablePageState()) + assertThat(awaitItem()).isEqualTo( + TestablePageState( + listOf( + PageImpl( + isSelected = true, + zimReaderSource = zimReaderSource + ) + ) + ) + ) + } + ) } @Test - fun `Filter calls updatePagesBasedOnFilter`() { - // viewModel.actions.offer(Filter("Called")) - // viewModel.state.test() - // .assertValue(TestablePageState(searchTerm = "updatePagesBasedOnFilterCalled")) + fun `Filter calls updatePagesBasedOnFilter`() = runTest { + testFlow( + viewModel.state, + triggerAction = { viewModel.actions.tryEmit(Filter("Called")) }, + assert = { + assertThat(awaitItem()).isEqualTo(TestablePageState()) + assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = "updatePagesBasedOnFilterCalled")) + } + ) } @Test - fun `UpdatePages calls updatePages`() { - // viewModel.actions.offer(UpdatePages(emptyList())) - // viewModel.state.test() - // .assertValue(TestablePageState(searchTerm = "updatePagesCalled")) + fun `UpdatePages calls updatePages`() = runTest { + testFlow( + viewModel.state, + triggerAction = { viewModel.actions.tryEmit(UpdatePages(emptyList())) }, + assert = { + assertThat(awaitItem()).isEqualTo(TestablePageState()) + assertThat(awaitItem()).isEqualTo(TestablePageState(searchTerm = "updatePagesCalled")) + } + ) } } diff --git a/core/src/test/java/org/kiwix/kiwixmobile/core/utils/files/FileSearchTest.kt b/core/src/test/java/org/kiwix/kiwixmobile/core/utils/files/FileSearchTest.kt index 46c2323a6..4d94016eb 100644 --- a/core/src/test/java/org/kiwix/kiwixmobile/core/utils/files/FileSearchTest.kt +++ b/core/src/test/java/org/kiwix/kiwixmobile/core/utils/files/FileSearchTest.kt @@ -31,7 +31,6 @@ import io.mockk.clearMocks import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic -import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope @@ -41,8 +40,6 @@ import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test -import org.kiwix.sharedFunctions.resetSchedulers -import org.kiwix.sharedFunctions.setScheduler import java.io.File class FileSearchTest { @@ -54,10 +51,6 @@ class FileSearchTest { private val storageDevice: StorageDevice = mockk() private val scanningProgressListener: ScanningProgressListener = mockk() - init { - setScheduler(Schedulers.trampoline()) - } - @BeforeEach fun init() { clearMocks(context, externalStorageDirectory, contentResolver, storageDevice) @@ -78,7 +71,6 @@ class FileSearchTest { @AfterAll fun teardown() { deleteTempDirectory() - resetSchedulers() } @Nested