mirror of
https://github.com/kiwix/kiwix-android.git
synced 2025-09-09 07:16:04 -04:00
Introduced the turbine library for efficiently testing the coroutine flows.
* Removed the `TestObserver` since it was sometimes stucks on the execution. * Refactored the test cases according to turbine library.
This commit is contained in:
parent
2f9439391a
commit
4af14d8c98
@ -130,6 +130,7 @@ androidComponents {
|
|||||||
dependencies {
|
dependencies {
|
||||||
androidTestImplementation(Libs.leakcanary_android_instrumentation)
|
androidTestImplementation(Libs.leakcanary_android_instrumentation)
|
||||||
testImplementation(Libs.kotlinx_coroutines_test)
|
testImplementation(Libs.kotlinx_coroutines_test)
|
||||||
|
testImplementation(Libs.TURBINE_FLOW_TEST)
|
||||||
}
|
}
|
||||||
tasks.register("generateVersionCodeAndName") {
|
tasks.register("generateVersionCodeAndName") {
|
||||||
val file = File("VERSION_INFO")
|
val file = File("VERSION_INFO")
|
||||||
|
@ -1,102 +0,0 @@
|
|||||||
/*
|
|
||||||
* Kiwix Android
|
|
||||||
* Copyright (c) 2025 Kiwix <android.kiwix.org>
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.kiwix.kiwixmobile
|
|
||||||
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.test.TestScope
|
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
|
||||||
|
|
||||||
fun <T> Flow<T>.test(scope: TestScope, itemCountsToEmitInFlow: Int = 1): TestObserver<T> =
|
|
||||||
TestObserver(scope, this, itemCountsToEmitInFlow).also { it.startCollecting() }
|
|
||||||
|
|
||||||
class TestObserver<T>(
|
|
||||||
private val scope: TestScope,
|
|
||||||
private val flow: Flow<T>,
|
|
||||||
private val itemCountsToEmitInFlow: Int
|
|
||||||
) {
|
|
||||||
private val values = mutableListOf<T>()
|
|
||||||
private val completionChannel = Channel<Unit>()
|
|
||||||
private var job: Job? = null
|
|
||||||
|
|
||||||
fun startCollecting() {
|
|
||||||
job = scope.launch {
|
|
||||||
flow.collect {
|
|
||||||
values.add(it)
|
|
||||||
completionChannel.send(Unit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the list of values collected from the flow.
|
|
||||||
*
|
|
||||||
* If [shouldAwaitCompletion] is true, this method will suspend until the flow
|
|
||||||
* signals completion, ensuring all values have been collected before returning.
|
|
||||||
*
|
|
||||||
* @param shouldAwaitCompletion Whether to wait for the flow to finish collecting before returning the results.
|
|
||||||
* @return A mutable list of values emitted by the flow.
|
|
||||||
*/
|
|
||||||
suspend fun getValues(shouldAwaitCompletion: Boolean = true): MutableList<T> {
|
|
||||||
if (shouldAwaitCompletion) {
|
|
||||||
awaitCompletion()
|
|
||||||
}
|
|
||||||
return values
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun awaitCompletion() {
|
|
||||||
repeat(itemCountsToEmitInFlow) {
|
|
||||||
completionChannel.receive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun assertValues(listValues: MutableList<T>): TestObserver<T> {
|
|
||||||
awaitCompletion()
|
|
||||||
assertThat(listValues).containsExactlyElementsOf(values)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun containsExactlyInAnyOrder(
|
|
||||||
listValues: MutableList<T>,
|
|
||||||
vararg values: T
|
|
||||||
): TestObserver<T> {
|
|
||||||
awaitCompletion()
|
|
||||||
assertThat(listValues).containsExactlyInAnyOrder(*values)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun assertLastValue(value: T): TestObserver<T> {
|
|
||||||
awaitCompletion()
|
|
||||||
assertThat(values.last()).isEqualTo(value)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun finish() {
|
|
||||||
job?.cancelAndJoin()
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun assertLastValue(value: (T) -> Boolean): TestObserver<T> {
|
|
||||||
awaitCompletion()
|
|
||||||
assertThat(values.last()).satisfies({ value(it) })
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
}
|
|
@ -23,6 +23,8 @@ import android.net.ConnectivityManager
|
|||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
import android.net.NetworkCapabilities.TRANSPORT_WIFI
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import app.cash.turbine.TurbineTestContext
|
||||||
|
import app.cash.turbine.test
|
||||||
import com.jraska.livedata.test
|
import com.jraska.livedata.test
|
||||||
import io.mockk.clearAllMocks
|
import io.mockk.clearAllMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
@ -34,9 +36,13 @@ import io.reactivex.processors.PublishProcessor
|
|||||||
import io.reactivex.schedulers.TestScheduler
|
import io.reactivex.schedulers.TestScheduler
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Disabled
|
import org.junit.jupiter.api.Disabled
|
||||||
@ -65,7 +71,6 @@ import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListIte
|
|||||||
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
|
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
|
||||||
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.MULTI
|
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.MULTI
|
||||||
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.NORMAL
|
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.SelectionMode.NORMAL
|
||||||
import org.kiwix.kiwixmobile.test
|
|
||||||
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState
|
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState
|
||||||
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile
|
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CanWrite4GbFile
|
||||||
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile
|
import org.kiwix.kiwixmobile.zimManager.Fat32Checker.FileSystemState.CannotWrite4GbFile
|
||||||
@ -213,7 +218,7 @@ class ZimManageViewModelTest {
|
|||||||
inner class Books {
|
inner class Books {
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@Test
|
@Test
|
||||||
fun `emissions from dat source are observed`() {
|
fun `emissions from data source are observed`() {
|
||||||
val expectedList = listOf(bookOnDisk())
|
val expectedList = listOf(bookOnDisk())
|
||||||
booksOnDiskListItems.onNext(expectedList)
|
booksOnDiskListItems.onNext(expectedList)
|
||||||
testScheduler.triggerActions()
|
testScheduler.triggerActions()
|
||||||
@ -486,10 +491,11 @@ class ZimManageViewModelTest {
|
|||||||
),
|
),
|
||||||
NORMAL
|
NORMAL
|
||||||
)
|
)
|
||||||
viewModel.sideEffects.test(this)
|
testFlow(
|
||||||
.also { viewModel.fileSelectActions.emit(RequestMultiSelection(bookToSelect)) }
|
flow = viewModel.sideEffects,
|
||||||
.assertValues(mutableListOf(StartMultiSelection(viewModel.fileSelectActions)))
|
triggerAction = { viewModel.fileSelectActions.emit(RequestMultiSelection(bookToSelect)) },
|
||||||
.finish()
|
assert = { assertThat(awaitItem()).isEqualTo(StartMultiSelection(viewModel.fileSelectActions)) }
|
||||||
|
)
|
||||||
viewModel.fileSelectListStates.test()
|
viewModel.fileSelectListStates.test()
|
||||||
.assertValue(
|
.assertValue(
|
||||||
FileSelectListState(
|
FileSelectListState(
|
||||||
@ -504,10 +510,18 @@ class ZimManageViewModelTest {
|
|||||||
val selectedBook = bookOnDisk().apply { isSelected = true }
|
val selectedBook = bookOnDisk().apply { isSelected = true }
|
||||||
viewModel.fileSelectListStates.value =
|
viewModel.fileSelectListStates.value =
|
||||||
FileSelectListState(listOf(selectedBook, bookOnDisk()), NORMAL)
|
FileSelectListState(listOf(selectedBook, bookOnDisk()), NORMAL)
|
||||||
viewModel.sideEffects.test(this)
|
testFlow(
|
||||||
.also { viewModel.fileSelectActions.emit(RequestDeleteMultiSelection) }
|
flow = viewModel.sideEffects,
|
||||||
.assertValues(mutableListOf(DeleteFiles(listOf(selectedBook), alertDialogShower)))
|
triggerAction = { viewModel.fileSelectActions.emit(RequestDeleteMultiSelection) },
|
||||||
.finish()
|
assert = {
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
DeleteFiles(
|
||||||
|
listOf(selectedBook),
|
||||||
|
alertDialogShower
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -515,10 +529,11 @@ class ZimManageViewModelTest {
|
|||||||
val selectedBook = bookOnDisk().apply { isSelected = true }
|
val selectedBook = bookOnDisk().apply { isSelected = true }
|
||||||
viewModel.fileSelectListStates.value =
|
viewModel.fileSelectListStates.value =
|
||||||
FileSelectListState(listOf(selectedBook, bookOnDisk()), NORMAL)
|
FileSelectListState(listOf(selectedBook, bookOnDisk()), NORMAL)
|
||||||
viewModel.sideEffects.test(this)
|
testFlow(
|
||||||
.also { viewModel.fileSelectActions.emit(RequestShareMultiSelection) }
|
flow = viewModel.sideEffects,
|
||||||
.assertValues(mutableListOf(ShareFiles(listOf(selectedBook))))
|
triggerAction = { viewModel.fileSelectActions.emit(RequestShareMultiSelection) },
|
||||||
.finish()
|
assert = { assertThat(awaitItem()).isEqualTo(ShareFiles(listOf(selectedBook))) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -526,10 +541,11 @@ class ZimManageViewModelTest {
|
|||||||
val selectedBook = bookOnDisk().apply { isSelected = true }
|
val selectedBook = bookOnDisk().apply { isSelected = true }
|
||||||
viewModel.fileSelectListStates.value =
|
viewModel.fileSelectListStates.value =
|
||||||
FileSelectListState(listOf(selectedBook, bookOnDisk()), NORMAL)
|
FileSelectListState(listOf(selectedBook, bookOnDisk()), NORMAL)
|
||||||
viewModel.sideEffects.test(this)
|
testFlow(
|
||||||
.also { viewModel.fileSelectActions.emit(MultiModeFinished) }
|
flow = viewModel.sideEffects,
|
||||||
.assertValues(mutableListOf(None))
|
triggerAction = { viewModel.fileSelectActions.emit(MultiModeFinished) },
|
||||||
.finish()
|
assert = { assertThat(awaitItem()).isEqualTo(None) }
|
||||||
|
)
|
||||||
viewModel.fileSelectListStates.test().assertValue(
|
viewModel.fileSelectListStates.test().assertValue(
|
||||||
FileSelectListState(
|
FileSelectListState(
|
||||||
listOf(
|
listOf(
|
||||||
@ -545,10 +561,11 @@ class ZimManageViewModelTest {
|
|||||||
val selectedBook = bookOnDisk(0L).apply { isSelected = true }
|
val selectedBook = bookOnDisk(0L).apply { isSelected = true }
|
||||||
viewModel.fileSelectListStates.value =
|
viewModel.fileSelectListStates.value =
|
||||||
FileSelectListState(listOf(selectedBook, bookOnDisk(1L)), NORMAL)
|
FileSelectListState(listOf(selectedBook, bookOnDisk(1L)), NORMAL)
|
||||||
viewModel.sideEffects.test(this)
|
testFlow(
|
||||||
.also { viewModel.fileSelectActions.emit(RequestSelect(selectedBook)) }
|
flow = viewModel.sideEffects,
|
||||||
.assertValues(mutableListOf(None))
|
triggerAction = { viewModel.fileSelectActions.emit(RequestSelect(selectedBook)) },
|
||||||
.finish()
|
assert = { assertThat(awaitItem()).isEqualTo(None) }
|
||||||
|
)
|
||||||
viewModel.fileSelectListStates.test().assertValue(
|
viewModel.fileSelectListStates.test().assertValue(
|
||||||
FileSelectListState(
|
FileSelectListState(
|
||||||
listOf(
|
listOf(
|
||||||
@ -561,10 +578,26 @@ class ZimManageViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `RestartActionMode offers StartMultiSelection`() = runTest {
|
fun `RestartActionMode offers StartMultiSelection`() = runTest {
|
||||||
viewModel.sideEffects.test(this)
|
testFlow(
|
||||||
.also { viewModel.fileSelectActions.emit(RestartActionMode) }
|
flow = viewModel.sideEffects,
|
||||||
.assertValues(mutableListOf(StartMultiSelection(viewModel.fileSelectActions)))
|
triggerAction = { viewModel.fileSelectActions.emit(RestartActionMode) },
|
||||||
.finish()
|
assert = { assertThat(awaitItem()).isEqualTo(StartMultiSelection(viewModel.fileSelectActions)) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T> TestScope.testFlow(
|
||||||
|
flow: Flow<T>,
|
||||||
|
triggerAction: suspend () -> Unit,
|
||||||
|
assert: suspend TurbineTestContext<T>.() -> Unit
|
||||||
|
) {
|
||||||
|
val job = launch {
|
||||||
|
flow.test {
|
||||||
|
triggerAction()
|
||||||
|
assert()
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
job.join()
|
||||||
|
}
|
||||||
|
@ -29,6 +29,11 @@ object Libs {
|
|||||||
const val kotlinx_coroutines_test: String = "org.jetbrains.kotlinx:kotlinx-coroutines-test:" +
|
const val kotlinx_coroutines_test: String = "org.jetbrains.kotlinx:kotlinx-coroutines-test:" +
|
||||||
Versions.org_jetbrains_kotlinx_kotlinx_coroutines
|
Versions.org_jetbrains_kotlinx_kotlinx_coroutines
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://github.com/cashapp/turbine
|
||||||
|
*/
|
||||||
|
const val TURBINE_FLOW_TEST: String = "app.cash.turbine:turbine:" + Versions.TURBINE_FLOW_TEST
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://developer.android.com/testing
|
* https://developer.android.com/testing
|
||||||
*/
|
*/
|
||||||
|
@ -119,6 +119,8 @@ object Versions {
|
|||||||
const val COMPOSE_VERSION = "1.7.8"
|
const val COMPOSE_VERSION = "1.7.8"
|
||||||
|
|
||||||
const val COMPOSE_MATERIAL3 = "1.3.1"
|
const val COMPOSE_MATERIAL3 = "1.3.1"
|
||||||
|
|
||||||
|
const val TURBINE_FLOW_TEST = "1.2.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,4 +63,5 @@ dependencies {
|
|||||||
testImplementation(Libs.kotlinx_coroutines_test)
|
testImplementation(Libs.kotlinx_coroutines_test)
|
||||||
implementation(Libs.kotlinx_coroutines_android)
|
implementation(Libs.kotlinx_coroutines_android)
|
||||||
implementation(Libs.zxing)
|
implementation(Libs.zxing)
|
||||||
|
testImplementation(Libs.TURBINE_FLOW_TEST)
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,8 @@ import io.reactivex.schedulers.Schedulers
|
|||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.TestScope
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@ -37,10 +37,11 @@ import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
|
|||||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
import org.kiwix.kiwixmobile.core.reader.ZimFileReader
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Factory
|
import org.kiwix.kiwixmobile.core.reader.ZimFileReader.Factory
|
||||||
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
|
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.test
|
|
||||||
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
|
||||||
import org.kiwix.kiwixmobile.core.utils.files.FileSearch
|
import org.kiwix.kiwixmobile.core.utils.files.FileSearch
|
||||||
import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener
|
import org.kiwix.kiwixmobile.core.utils.files.ScanningProgressListener
|
||||||
|
import org.kiwix.kiwixmobile.core.utils.files.testFlow
|
||||||
|
import org.kiwix.kiwixmobile.core.zim_manager.fileselect_view.BooksOnDiskListItem.BookOnDisk
|
||||||
import org.kiwix.sharedFunctions.book
|
import org.kiwix.sharedFunctions.book
|
||||||
import org.kiwix.sharedFunctions.bookOnDisk
|
import org.kiwix.sharedFunctions.bookOnDisk
|
||||||
import org.kiwix.sharedFunctions.resetSchedulers
|
import org.kiwix.sharedFunctions.resetSchedulers
|
||||||
@ -86,7 +87,11 @@ class StorageObserverTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `books from disk are filtered by current downloads`() = runTest {
|
fun `books from disk are filtered by current downloads`() = runTest {
|
||||||
withFiltering()
|
withFiltering()
|
||||||
booksOnFileSystem(this).assertValues(mutableListOf(mutableListOf())).finish()
|
testFlow(
|
||||||
|
flow = booksOnFileSystem(),
|
||||||
|
triggerAction = {},
|
||||||
|
assert = { assertThat(awaitItem()).isEqualTo(listOf<BookOnDisk>()) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
@ -100,23 +105,25 @@ class StorageObserverTest {
|
|||||||
withNoFiltering()
|
withNoFiltering()
|
||||||
every { zimFileReader.toBook() } returns expectedBook
|
every { zimFileReader.toBook() } returns expectedBook
|
||||||
every { zimFileReader.zimReaderSource } returns zimReaderSource
|
every { zimFileReader.zimReaderSource } returns zimReaderSource
|
||||||
val testObserver = booksOnFileSystem(this)
|
testFlow(
|
||||||
testObserver.assertValues(
|
flow = booksOnFileSystem(),
|
||||||
mutableListOf(
|
triggerAction = {},
|
||||||
mutableListOf(
|
assert = {
|
||||||
|
assertThat(awaitItem()).isEqualTo(
|
||||||
|
listOf<BookOnDisk>(
|
||||||
bookOnDisk(
|
bookOnDisk(
|
||||||
book = expectedBook,
|
book = expectedBook,
|
||||||
zimReaderSource = zimReaderSource
|
zimReaderSource = zimReaderSource
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).finish()
|
}
|
||||||
|
)
|
||||||
verify { zimFileReader.dispose() }
|
verify { zimFileReader.dispose() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun booksOnFileSystem(testScope: TestScope) =
|
private fun booksOnFileSystem() =
|
||||||
storageObserver.getBooksOnFileSystem(scanningProgressListener)
|
storageObserver.getBooksOnFileSystem(scanningProgressListener)
|
||||||
.test(testScope)
|
|
||||||
.also {
|
.also {
|
||||||
downloads.value = listOf(downloadModel)
|
downloads.value = listOf(downloadModel)
|
||||||
files.value = listOf(file)
|
files.value = listOf(file)
|
||||||
|
@ -23,6 +23,8 @@ import android.content.Context
|
|||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.provider.MediaStore.MediaColumns
|
import android.provider.MediaStore.MediaColumns
|
||||||
|
import app.cash.turbine.TurbineTestContext
|
||||||
|
import app.cash.turbine.test
|
||||||
import eu.mhutti1.utils.storage.StorageDevice
|
import eu.mhutti1.utils.storage.StorageDevice
|
||||||
import eu.mhutti1.utils.storage.StorageDeviceUtils
|
import eu.mhutti1.utils.storage.StorageDeviceUtils
|
||||||
import io.mockk.clearMocks
|
import io.mockk.clearMocks
|
||||||
@ -30,12 +32,15 @@ import io.mockk.every
|
|||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.AfterAll
|
import org.junit.jupiter.api.AfterAll
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.kiwix.kiwixmobile.core.search.viewmodel.test
|
|
||||||
import org.kiwix.sharedFunctions.resetSchedulers
|
import org.kiwix.sharedFunctions.resetSchedulers
|
||||||
import org.kiwix.sharedFunctions.setScheduler
|
import org.kiwix.sharedFunctions.setScheduler
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -81,10 +86,11 @@ class FileSearchTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `scan of directory that doesn't exist returns nothing`() = runTest {
|
fun `scan of directory that doesn't exist returns nothing`() = runTest {
|
||||||
every { contentResolver.query(any(), any(), any(), any(), any()) } returns null
|
every { contentResolver.query(any(), any(), any(), any(), any()) } returns null
|
||||||
fileSearch.scan(scanningProgressListener)
|
testFlow(
|
||||||
.test(this)
|
flow = fileSearch.scan(scanningProgressListener),
|
||||||
.assertValues(mutableListOf(mutableListOf()))
|
triggerAction = {},
|
||||||
.finish()
|
assert = { assertThat(awaitItem()).isEqualTo(emptyList<File>()) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -94,13 +100,11 @@ class FileSearchTest {
|
|||||||
File.createTempFile("willNotFind", ".txt")
|
File.createTempFile("willNotFind", ".txt")
|
||||||
every { contentResolver.query(any(), any(), any(), any(), any()) } returns null
|
every { contentResolver.query(any(), any(), any(), any(), any()) } returns null
|
||||||
every { storageDevice.name } returns zimFile.parent
|
every { storageDevice.name } returns zimFile.parent
|
||||||
val testObserver = fileSearch.scan(scanningProgressListener)
|
testFlow(
|
||||||
.test(this)
|
flow = fileSearch.scan(scanningProgressListener),
|
||||||
val observedValues = testObserver.getValues().first()
|
triggerAction = {},
|
||||||
testObserver.containsExactlyInAnyOrder(
|
assert = { assertThat(awaitItem()).containsExactlyInAnyOrder(zimFile, zimaaFile) }
|
||||||
mutableListOf(observedValues),
|
)
|
||||||
listOf(zimFile, zimaaFile)
|
|
||||||
).finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -116,13 +120,11 @@ class FileSearchTest {
|
|||||||
)
|
)
|
||||||
every { contentResolver.query(any(), any(), any(), any(), any()) } returns null
|
every { contentResolver.query(any(), any(), any(), any(), any()) } returns null
|
||||||
every { storageDevice.name } returns zimFile.parentFile.parent
|
every { storageDevice.name } returns zimFile.parentFile.parent
|
||||||
val testObserver = fileSearch.scan(scanningProgressListener)
|
testFlow(
|
||||||
.test(this)
|
flow = fileSearch.scan(scanningProgressListener),
|
||||||
val observedValue = testObserver.getValues()[0]
|
triggerAction = {},
|
||||||
testObserver.containsExactlyInAnyOrder(
|
assert = { assertThat(awaitItem()[0]).isEqualTo(zimFile) }
|
||||||
mutableListOf(observedValue),
|
)
|
||||||
listOf(zimFile)
|
|
||||||
).finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,10 +134,11 @@ class FileSearchTest {
|
|||||||
fun `scan media store, if files are readable they are returned`() = runTest {
|
fun `scan media store, if files are readable they are returned`() = runTest {
|
||||||
val fileToFind = File.createTempFile("fileToFind", ".zim")
|
val fileToFind = File.createTempFile("fileToFind", ".zim")
|
||||||
expectFromMediaStore(fileToFind)
|
expectFromMediaStore(fileToFind)
|
||||||
fileSearch.scan(scanningProgressListener)
|
testFlow(
|
||||||
.test(this)
|
flow = fileSearch.scan(scanningProgressListener),
|
||||||
.assertValues(mutableListOf(listOf(fileToFind)))
|
triggerAction = {},
|
||||||
.finish()
|
assert = { assertThat(awaitItem()).isEqualTo(listOf(fileToFind)) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -143,10 +146,11 @@ class FileSearchTest {
|
|||||||
val unreadableFile = File.createTempFile("fileToFind", ".zim")
|
val unreadableFile = File.createTempFile("fileToFind", ".zim")
|
||||||
expectFromMediaStore(unreadableFile)
|
expectFromMediaStore(unreadableFile)
|
||||||
unreadableFile.delete()
|
unreadableFile.delete()
|
||||||
fileSearch.scan(scanningProgressListener)
|
testFlow(
|
||||||
.test(this)
|
flow = fileSearch.scan(scanningProgressListener),
|
||||||
.assertValues(mutableListOf(mutableListOf()))
|
triggerAction = {},
|
||||||
.finish()
|
assert = { assertThat(awaitItem()).isEqualTo(listOf<File>()) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expectFromMediaStore(fileToFind: File) {
|
private fun expectFromMediaStore(fileToFind: File) {
|
||||||
@ -168,7 +172,26 @@ class FileSearchTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteTempDirectory() {
|
private fun deleteTempDirectory() {
|
||||||
|
try {
|
||||||
File.createTempFile("temp", ".txt")
|
File.createTempFile("temp", ".txt")
|
||||||
.parentFile.deleteRecursively()
|
.parentFile.deleteRecursively()
|
||||||
|
} catch (ignore: Exception) {
|
||||||
|
ignore.printStackTrace()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun <T> TestScope.testFlow(
|
||||||
|
flow: Flow<T>,
|
||||||
|
triggerAction: suspend () -> Unit,
|
||||||
|
assert: suspend TurbineTestContext<T>.() -> Unit
|
||||||
|
) {
|
||||||
|
val job = launch {
|
||||||
|
flow.test {
|
||||||
|
triggerAction()
|
||||||
|
assert()
|
||||||
|
cancelAndIgnoreRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
job.join()
|
||||||
|
}
|
||||||
|
@ -62,6 +62,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation(Libs.kotlinx_coroutines_test)
|
testImplementation(Libs.kotlinx_coroutines_test)
|
||||||
|
testImplementation(Libs.TURBINE_FLOW_TEST)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ProductFlavor.createDownloadTask(file: File): TaskProvider<Task> {
|
fun ProductFlavor.createDownloadTask(file: File): TaskProvider<Task> {
|
||||||
|
@ -18,23 +18,19 @@
|
|||||||
|
|
||||||
package org.kiwix.kiwixmobile.custom.download
|
package org.kiwix.kiwixmobile.custom.download
|
||||||
|
|
||||||
|
import app.cash.turbine.TurbineTestContext
|
||||||
|
import app.cash.turbine.test
|
||||||
import com.tonyodev.fetch2.Error.NONE
|
import com.tonyodev.fetch2.Error.NONE
|
||||||
import io.mockk.clearAllMocks
|
import io.mockk.clearAllMocks
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.test.TestScope
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.advanceUntilIdle
|
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.jupiter.api.BeforeEach
|
import org.junit.jupiter.api.BeforeEach
|
||||||
import org.junit.jupiter.api.Disabled
|
|
||||||
import org.junit.jupiter.api.Nested
|
import org.junit.jupiter.api.Nested
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import org.junit.jupiter.api.extension.ExtendWith
|
import org.junit.jupiter.api.extension.ExtendWith
|
||||||
@ -79,16 +75,20 @@ internal class CustomDownloadViewModelTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `effects emits SetPreferred on Subscribe`() = runTest {
|
internal fun `effects emits SetPreferred on Subscribe`() = runTest {
|
||||||
customDownloadViewModel.effects.test(this)
|
testFlow(
|
||||||
.assertValues(mutableListOf(setPreferredStorageWithMostSpace))
|
flow = customDownloadViewModel.effects,
|
||||||
.finish()
|
triggerAction = {},
|
||||||
|
assert = { assertThat(awaitItem()).isEqualTo(setPreferredStorageWithMostSpace) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `initial State is DownloadRequired`() = runTest {
|
internal fun `initial State is DownloadRequired`() = runTest {
|
||||||
customDownloadViewModel.state.test(this)
|
testFlow(
|
||||||
.assertValues(mutableListOf(State.DownloadRequired))
|
flow = customDownloadViewModel.state,
|
||||||
.finish()
|
triggerAction = {},
|
||||||
|
assert = { assertThat(awaitItem()).isEqualTo(State.DownloadRequired) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
@ -97,26 +97,26 @@ internal class CustomDownloadViewModelTest {
|
|||||||
internal fun `Emission with data moves state from Required to InProgress`() = runTest {
|
internal fun `Emission with data moves state from Required to InProgress`() = runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
1,
|
|
||||||
DownloadRequired,
|
DownloadRequired,
|
||||||
DatabaseEmission(listOf(downloadItem())),
|
DatabaseEmission(listOf(downloadItem())),
|
||||||
State.DownloadInProgress(listOf(downloadItem()))
|
State.DownloadInProgress(listOf(downloadItem())),
|
||||||
|
2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `Emission without data moves state from Required to Required`() = runTest {
|
internal fun `Emission without data moves state from Required to Required`() = runTest {
|
||||||
assertStateTransition(this, 1, DownloadRequired, DatabaseEmission(listOf()), DownloadRequired)
|
assertStateTransition(this, DownloadRequired, DatabaseEmission(listOf()), DownloadRequired)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Disabled
|
@Test
|
||||||
internal fun `Emission with data moves state from Failed to InProgress`() = runTest {
|
internal fun `Emission with data moves state from Failed to InProgress`() = runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
1,
|
|
||||||
DownloadFailed(DownloadState.Pending),
|
DownloadFailed(DownloadState.Pending),
|
||||||
DatabaseEmission(listOf(downloadItem())),
|
DatabaseEmission(listOf(downloadItem())),
|
||||||
State.DownloadInProgress(listOf(downloadItem()))
|
State.DownloadInProgress(listOf(downloadItem())),
|
||||||
|
2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +124,6 @@ internal class CustomDownloadViewModelTest {
|
|||||||
internal fun `Emission without data moves state from Failed to Failed`() = runTest {
|
internal fun `Emission without data moves state from Failed to Failed`() = runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
1,
|
|
||||||
DownloadFailed(DownloadState.Pending),
|
DownloadFailed(DownloadState.Pending),
|
||||||
DatabaseEmission(listOf()),
|
DatabaseEmission(listOf()),
|
||||||
DownloadFailed(DownloadState.Pending)
|
DownloadFailed(DownloadState.Pending)
|
||||||
@ -135,10 +134,10 @@ internal class CustomDownloadViewModelTest {
|
|||||||
internal fun `Emission with data+failure moves state from InProgress to Failed`() = runTest {
|
internal fun `Emission with data+failure moves state from InProgress to Failed`() = runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
1,
|
|
||||||
DownloadInProgress(listOf()),
|
DownloadInProgress(listOf()),
|
||||||
DatabaseEmission(listOf(downloadItem(state = Failed(NONE, null)))),
|
DatabaseEmission(listOf(downloadItem(state = Failed(NONE, null)))),
|
||||||
DownloadFailed(Failed(NONE, null))
|
DownloadFailed(Failed(NONE, null)),
|
||||||
|
2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,117 +145,98 @@ internal class CustomDownloadViewModelTest {
|
|||||||
internal fun `Emission with data moves state from InProgress to InProgress`() = runTest {
|
internal fun `Emission with data moves state from InProgress to InProgress`() = runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
1,
|
|
||||||
DownloadInProgress(listOf(downloadItem(downloadId = 1L))),
|
DownloadInProgress(listOf(downloadItem(downloadId = 1L))),
|
||||||
DatabaseEmission(listOf(downloadItem(downloadId = 2L))),
|
DatabaseEmission(listOf(downloadItem(downloadId = 2L))),
|
||||||
DownloadInProgress(listOf(downloadItem(downloadId = 2L)))
|
DownloadInProgress(listOf(downloadItem(downloadId = 2L))),
|
||||||
|
2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Disabled("TODO fix in upcoming issue when properly migrated to coroutines")
|
@Test
|
||||||
internal fun `Emission without data moves state from InProgress to Complete`() = runTest {
|
internal fun `Emission without data moves state from InProgress to Complete`() = runTest {
|
||||||
val sideEffects = customDownloadViewModel.effects.test(this)
|
testFlow(
|
||||||
|
flow = customDownloadViewModel.effects,
|
||||||
|
triggerAction = {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
1,
|
|
||||||
DownloadInProgress(listOf()),
|
DownloadInProgress(listOf()),
|
||||||
DatabaseEmission(listOf()),
|
DatabaseEmission(listOf()),
|
||||||
DownloadComplete
|
DownloadComplete,
|
||||||
|
2
|
||||||
)
|
)
|
||||||
sideEffects.assertValues(
|
},
|
||||||
mutableListOf(
|
assert = {
|
||||||
setPreferredStorageWithMostSpace,
|
assertThat(awaitItem()).isEqualTo(setPreferredStorageWithMostSpace)
|
||||||
navigateToCustomReader
|
assertThat(awaitItem()).isEqualTo(navigateToCustomReader)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
).finish()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
internal fun `Any emission does not change state from Complete`() = runTest {
|
internal fun `Any emission does not change state from Complete`() = runTest {
|
||||||
assertStateTransition(
|
assertStateTransition(
|
||||||
this,
|
this,
|
||||||
1,
|
|
||||||
DownloadComplete,
|
DownloadComplete,
|
||||||
DatabaseEmission(listOf(downloadItem())),
|
DatabaseEmission(listOf(downloadItem())),
|
||||||
DownloadComplete
|
DownloadComplete
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
private suspend fun assertStateTransition(
|
||||||
private fun assertStateTransition(
|
|
||||||
testScope: TestScope,
|
testScope: TestScope,
|
||||||
flowCount: Int,
|
|
||||||
initialState: State,
|
initialState: State,
|
||||||
action: DatabaseEmission,
|
action: DatabaseEmission,
|
||||||
endState: State
|
endState: State,
|
||||||
|
awaitItemCount: Int = 1
|
||||||
) {
|
) {
|
||||||
customDownloadViewModel.getStateForTesting().value = initialState
|
customDownloadViewModel.getStateForTesting().value = initialState
|
||||||
testScope.launch {
|
testScope.testFlow(
|
||||||
customDownloadViewModel.actions.emit(action)
|
flow = customDownloadViewModel.state,
|
||||||
testScope.advanceUntilIdle()
|
triggerAction = { customDownloadViewModel.actions.emit(action) },
|
||||||
customDownloadViewModel.state.test(testScope, flowCount)
|
assert = {
|
||||||
.assertLastValues(mutableListOf(endState)).finish()
|
val items = (1..awaitItemCount).map { awaitItem() }
|
||||||
|
assertThat(items.last()).isEqualTo(endState)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Disabled("TODO fix in upcoming issue when properly migrated to coroutines")
|
@Test
|
||||||
internal fun `clicking Retry triggers DownloadCustom`() = runTest {
|
internal fun `clicking Retry triggers DownloadCustom`() = runTest {
|
||||||
val sideEffects = customDownloadViewModel.effects.test(this)
|
testFlow(
|
||||||
customDownloadViewModel.actions.emit(ClickedRetry)
|
flow = customDownloadViewModel.effects,
|
||||||
sideEffects.assertValues(mutableListOf(setPreferredStorageWithMostSpace, downloadCustom))
|
triggerAction = { customDownloadViewModel.actions.emit(ClickedRetry) },
|
||||||
.finish()
|
assert = {
|
||||||
|
assertThat(awaitItem()).isEqualTo(setPreferredStorageWithMostSpace)
|
||||||
|
assertThat(awaitItem()).isEqualTo(downloadCustom)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Disabled("TODO fix in upcoming issue when properly migrated to coroutines")
|
@Test
|
||||||
internal fun `clicking Download triggers DownloadCustom`() = runTest {
|
internal fun `clicking Download triggers DownloadCustom`() = runTest {
|
||||||
val sideEffects = customDownloadViewModel.effects.test(this)
|
testFlow(
|
||||||
customDownloadViewModel.actions.emit(ClickedDownload)
|
flow = customDownloadViewModel.effects,
|
||||||
sideEffects.assertValues(mutableListOf(setPreferredStorageWithMostSpace, downloadCustom))
|
triggerAction = { customDownloadViewModel.actions.emit(ClickedDownload) },
|
||||||
.finish()
|
assert = {
|
||||||
|
assertThat(awaitItem()).isEqualTo(setPreferredStorageWithMostSpace)
|
||||||
|
assertThat(awaitItem()).isEqualTo(downloadCustom)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T> Flow<T>.test(scope: TestScope, itemCountsToEmitInFlow: Int = 1): TestObserver<T> =
|
suspend fun <T> TestScope.testFlow(
|
||||||
TestObserver(scope, this, itemCountsToEmitInFlow).also { it.startCollecting() }
|
flow: Flow<T>,
|
||||||
|
triggerAction: suspend () -> Unit,
|
||||||
class TestObserver<T>(
|
assert: suspend TurbineTestContext<T>.() -> Unit
|
||||||
private val scope: TestScope,
|
|
||||||
private val flow: Flow<T>,
|
|
||||||
private val itemCountsToEmitInFlow: Int
|
|
||||||
) {
|
) {
|
||||||
private val values = mutableListOf<T>()
|
val job = launch {
|
||||||
private val completionChannel = Channel<Unit>()
|
flow.test {
|
||||||
private var job: Job? = null
|
triggerAction()
|
||||||
|
assert()
|
||||||
fun startCollecting() {
|
cancelAndIgnoreRemainingEvents()
|
||||||
job = scope.launch {
|
|
||||||
flow.collect {
|
|
||||||
values.add(it)
|
|
||||||
completionChannel.send(Unit)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
job.join()
|
||||||
|
|
||||||
private suspend fun awaitCompletion() {
|
|
||||||
repeat(itemCountsToEmitInFlow) {
|
|
||||||
completionChannel.receive()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun assertValues(listValues: MutableList<T>): TestObserver<T> {
|
|
||||||
awaitCompletion()
|
|
||||||
assertThat(listValues).containsExactlyElementsOf(values)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun assertLastValues(listValues: MutableList<T>): TestObserver<T> {
|
|
||||||
awaitCompletion()
|
|
||||||
assertThat(listValues).containsExactlyElementsOf(mutableListOf(values.last()))
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun finish() {
|
|
||||||
job?.cancelAndJoin()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user