Add unit tests for DetailsPresenter

This commit is contained in:
Torsten Grote
2026-05-11 17:51:46 -03:00
parent 9d602ee642
commit 63ca340f97
4 changed files with 693 additions and 9 deletions

View File

@@ -34,8 +34,6 @@ import org.fdroid.utils.sha256
private const val TAG = "DetailsPresenter"
// TODO write tests for this function
// see: https://github.com/cashapp/molecule?tab=readme-ov-file#testing
@Composable
fun DetailsPresenter(
db: FDroidDatabase,

View File

@@ -142,6 +142,13 @@ val categoryItems =
CategoryItem("doesn't exist", "Foo bar"),
)
private val description =
"NewPipe does not use any Google framework libraries, or the YouTube API. " +
"It only parses the website in order to gain the information it needs. " +
"Therefore this app can be used on devices without Google Services installed. " +
"Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.\n\n" +
LoremIpsum(128).values.joinToString(" ")
val testApp =
LoadedAppDetailsItem(
app =
@@ -150,6 +157,9 @@ val testApp =
packageName = "org.schabi.newpipe",
added = 1441756800000,
lastUpdated = 1747214796000,
name = mapOf("en-US" to "New Pipe"),
summary = mapOf("en-US" to "Lightweight YouTube frontend"),
description = mapOf("en-US" to description),
webSite = "https://newpipe.net",
changelog = "https://github.com/TeamNewPipe/NewPipe/releases",
license = "GPL-3.0-or-later",
@@ -178,12 +188,7 @@ val testApp =
appPrefs = AppPrefs("org.schabi.newpipe"),
name = "New Pipe",
summary = "Lightweight YouTube frontend",
description =
"NewPipe does not use any Google framework libraries, or the YouTube API. " +
"It only parses the website in order to gain the information it needs. " +
"Therefore this app can be used on devices without Google Services installed. " +
"Also, you don't need a YouTube account to use NewPipe, and it's FLOSS.\n\n" +
LoremIpsum(128).values.joinToString(" "),
description = description,
categories = categoryItems.subList(0, 5),
antiFeatures =
listOf(

View File

@@ -0,0 +1,680 @@
package org.fdroid.ui.details
import android.content.Intent
import android.content.pm.PackageInfo
import android.content.pm.Signature
import androidx.core.content.pm.PackageInfoCompat
import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode
import androidx.lifecycle.MutableLiveData
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.ReceiveTurbine
import app.cash.turbine.test
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertIs
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertTrue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest
import org.fdroid.CompatibilityChecker
import org.fdroid.UpdateChecker
import org.fdroid.database.App
import org.fdroid.database.AppDao
import org.fdroid.database.AppIssue
import org.fdroid.database.AppManifest
import org.fdroid.database.AppPrefs
import org.fdroid.database.AppPrefsDao
import org.fdroid.database.AppVersion
import org.fdroid.database.FDroidDatabase
import org.fdroid.database.NotAvailable
import org.fdroid.database.Repository
import org.fdroid.database.VersionDao
import org.fdroid.download.NetworkState
import org.fdroid.index.IndexFormatVersion
import org.fdroid.index.RELEASE_CHANNEL_BETA
import org.fdroid.index.RepoManager
import org.fdroid.index.v2.FileV1
import org.fdroid.index.v2.SignerV2
import org.fdroid.index.v2.UsesSdkV2
import org.fdroid.install.AppInstallManager
import org.fdroid.install.InstallState
import org.fdroid.repo.RepoPreLoader
import org.fdroid.settings.SettingsManager
import org.fdroid.ui.apps.AppWithIssueItem
import org.fdroid.ui.utils.testApp
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@Config(sdk = [34]) // needed for oldTargetSdk assertion
@RunWith(RobolectricTestRunner::class)
internal class DetailsPresenterTest {
private val packageName = testApp.app.packageName
private val repoId = testApp.app.repoId
private val db: FDroidDatabase = mockk()
private val appDao: AppDao = mockk()
private val versionDao: VersionDao = mockk()
private val appPrefsDao: AppPrefsDao = mockk()
private val repoManager: RepoManager = mockk()
private val repoPreLoader: RepoPreLoader = mockk()
private val settingsManager: SettingsManager = mockk()
private val appInstallManager: AppInstallManager = mockk()
private val viewModel: AppDetailsViewModel = mockk(relaxed = true)
private val compatibilityChecker: CompatibilityChecker = mockk()
private val updateChecker = UpdateChecker(compatibilityChecker)
private val repository =
Repository(
repoId = repoId,
address = "https://example.org/fdroid/repo",
timestamp = 123L,
formatVersion = IndexFormatVersion.TWO,
certificate = "abcd",
version = 1L,
weight = 100,
lastUpdated = 456L,
)
private val app: App = mockk()
private val version: AppVersion = mockk()
private val versionCode = 42L
init {
every { db.getAppDao() } returns appDao
every { db.getVersionDao() } returns versionDao
every { db.getAppPrefsDao() } returns appPrefsDao
every { repoManager.getRepository(repoId) } returns repository
every { repoPreLoader.defaultRepoAddresses } returns emptySet()
every { settingsManager.proxyConfig } returns null
every { appInstallManager.getAppFlow(packageName) } returns flowOf(InstallState.Unknown)
every { appDao.getApp(repoId, packageName) } returns app
every { app.metadata } returns testApp.app
every { app.repoId } returns repoId
every { app.authorName } returns null
every { app.packageName } returns packageName
every { app.getDescription(any()) } returns testApp.description
every { app.getIcon(any()) } returns null
every { app.getFeatureGraphic(any()) } returns null
every { app.getPhoneScreenshots(any()) } returns emptyList()
every { versionDao.getAppVersions(repoId, packageName) } returns
MutableLiveData(listOf(version))
every { version.versionCode } returns versionCode
every { version.versionName } returns "1.0"
every { version.file } returns FileV1(name = "test.apk", sha256 = "abcd", size = 123L)
every { version.added } returns 300L
every { version.size } returns 123L
every { version.signer } returns null
every { version.releaseChannels } returns emptyList()
every { version.packageManifest } returns AppManifest(versionName = "1.0", versionCode = 42L)
every { version.hasKnownVulnerability } returns false
every { version.isCompatible } returns true
every { version.getWhatsNew(any()) } returns "Bug fixes"
every { version.antiFeatureKeys } returns emptyList()
every { appDao.getRepositoryIdsForApp(packageName) } returns listOf(repoId)
every { appPrefsDao.getAppPrefs(packageName) } returns MutableLiveData(AppPrefs(packageName))
every { compatibilityChecker.isCompatible(any()) } returns true
}
private val appInfoFlow = MutableStateFlow(AppInfo(packageName = packageName))
private val currentRepoIdFlow = MutableStateFlow<Long?>(repoId)
private val showAntiFeaturesOnboardingFlow = MutableStateFlow(false)
private val appsWithIssuesFlow = MutableStateFlow<List<AppWithIssueItem>?>(emptyList())
private val networkStateFlow = MutableStateFlow(NetworkState(isOnline = true, isMetered = false))
val presenterFlow =
moleculeFlow(RecompositionMode.Immediate) {
DetailsPresenter(
db = db,
dispatcher = Dispatchers.Unconfined,
repoManager = repoManager,
repoPreLoader = repoPreLoader,
updateChecker = updateChecker,
settingsManager = settingsManager,
appInstallManager = appInstallManager,
viewModel = viewModel,
packageInfoFlow = appInfoFlow,
currentRepoIdFlow = currentRepoIdFlow,
showAntiFeaturesOnboardingFlow = showAntiFeaturesOnboardingFlow,
appsWithIssuesFlow = appsWithIssuesFlow,
networkStateFlow = networkStateFlow,
)
}
// Not found cases
@Test
fun emitsNotFoundWhenAppIsNull() = runTest {
// App does not exist in the database for this repo
every { appDao.getApp(repoId, packageName) } returns null
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<NotFoundAppDetailsItem>(item)
cancelAndPrintRemainingEvents()
}
}
@Test
fun emitsNotFoundWhenRepoIsNull() = runTest {
// Repo has been removed (unlikely here)
every { repoManager.getRepository(repoId) } returns null
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<NotFoundAppDetailsItem>(item)
cancelAndPrintRemainingEvents()
}
}
// App that can be installed
@Test
fun emitsInstallableLoadedItem() = runTest {
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertEquals(MainButtonState.INSTALL, item.mainButtonState)
assertNotNull(item.versions)
assertEquals(packageName, item.app.packageName)
assertEquals(testApp.name, item.name)
assertEquals(testApp.summary, item.summary)
assertEquals(getHtmlDescription(testApp.description), item.description)
assertEquals(repoId, item.preferredRepoId)
assertEquals(InstallState.Unknown, item.installState)
assertEquals(MainButtonState.INSTALL, item.mainButtonState)
assertEquals(version, item.suggestedVersion)
assertFalse(item.showAntiFeaturesOnboarding)
val versionItem = item.versions.single()
assertNotNull(versionItem)
assertEquals(version, versionItem.version)
assertTrue(versionItem.isSuggested)
assertTrue(versionItem.isCompatible)
assertTrue(versionItem.isSignerCompatible)
assertFalse(versionItem.isInstalled)
assertTrue(versionItem.showInstallButton)
assertEquals(NetworkState(isOnline = true, isMetered = false), item.networkState)
assertNull(item.installedVersionCode)
assertNull(item.installedVersion)
assertNull(item.installedSigner)
assertEquals("Bug fixes", item.whatsNew)
assertTrue(item.showDonate)
assertTrue(item.showAuthorContact)
assertEquals(testApp.liberapayUri, item.liberapayUri)
assertEquals(testApp.openCollectiveUri, item.openCollectiveUri)
assertEquals(testApp.bitcoinUri, item.bitcoinUri)
assertEquals(testApp.litecoinUri, item.litecoinUri)
assertFalse(item.showWarnings)
assertFalse(item.ignoresCurrentUpdate)
assertFalse(item.ignoresAllUpdates)
assertFalse(item.allowsBetaVersions)
assertFalse(item.oldTargetSdk)
assertEquals(emptyList(), item.categories)
cancelAndPrintRemainingEvents()
}
}
// MainButtonState
@Test
fun showsLoadingStateWhenVersionsNotYetAvailable() = runTest {
setupInstalledApp(versionCode)
// Return a LiveData with no initial value so versions stay null in the presenter
every { versionDao.getAppVersions(repoId, packageName) } returns MutableLiveData()
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertEquals(MainButtonState.LOADING, item.mainButtonState)
assertEquals(null, item.versions)
assertEquals(versionCode, item.installedVersionCode)
cancelAndPrintRemainingEvents()
}
}
@Test
fun showsLoadingStateWhenNotInstalledAndVersionsNotYetAvailable() = runTest {
// similar as above, but with no installed version, so different code path
every { versionDao.getAppVersions(repoId, packageName) } returns MutableLiveData()
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertEquals(MainButtonState.LOADING, item.mainButtonState)
assertNull(item.versions)
assertNull(item.installedVersionCode)
cancelAndPrintRemainingEvents()
}
}
@Test
fun showsProgressStateWhenInstallationIsInProgress() = runTest {
val progressState =
InstallState.Starting(name = testApp.name, versionName = "1.0", lastUpdated = 300L)
every { appInstallManager.getAppFlow(packageName) } returns flowOf(progressState)
presenterFlow.test {
// after first load state is still unknown
val item1 = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item1)
assertEquals(InstallState.Unknown, item1.installState)
// then second emission reflects proper progress state
val item2 = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item2)
assertEquals(MainButtonState.PROGRESS, item2.mainButtonState)
assertEquals(progressState, item2.installState)
cancelAndPrintRemainingEvents()
}
}
@Test
fun emitsUpdateButtonWhenInstalledAndUpdateAvailable() = runTest {
setupInstalledApp(versionCode = versionCode - 1)
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertEquals(MainButtonState.UPDATE, item.mainButtonState)
assertNotNull(item.versions)
assertEquals(packageName, item.app.packageName)
assertNotNull(item.installedSigner)
cancelAndPrintRemainingEvents()
}
}
@Test
fun emitsOpenButtonWhenInstalledWithNoUpdate() = runTest {
setupInstalledApp(versionCode = versionCode, isInstalled = true)
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertEquals(MainButtonState.NONE, item.mainButtonState)
assertTrue(item.showOpenButton)
assertNotNull(item.versions)
assertEquals(version, item.installedVersion)
assertEquals(versionCode, item.installedVersionCode)
assertEquals("0.1", item.installedVersionName)
val versionItem = item.versions.single()
assertNotNull(versionItem)
assertTrue(versionItem.isInstalled)
cancelAndPrintRemainingEvents()
}
}
@Test
fun emitsNoneButtonWhenNoCompatibleVersions() = runTest {
// all versions are not compatible with this device
every { version.isCompatible } returns false
every { compatibilityChecker.isCompatible(any()) } returns false
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertEquals(MainButtonState.NONE, item.mainButtonState)
assertTrue(item.isIncompatible)
assertTrue(item.showWarnings)
assertTrue(item.versions?.all { !it.isCompatible } ?: false)
assertFalse(item.showOpenButton)
cancelAndPrintRemainingEvents()
}
}
// Signer compatibility
@Test
fun nullSignerVersionFoundWhenMultipleVersionsShareVersionCode() = runTest {
// The presenter finds the installed version by signer when multiple versions share the same
// version code. Versions without a signer are explicitly allowed by F-Droid, so a
// null signer version must still be recognized as the installed one.
every { version.signer } returns null
val version2: AppVersion = mockk()
every { version2.versionCode } returns versionCode // same code as default version
every { version2.signer } returns SignerV2(listOf("a_different_signer_hash"))
every { version2.isCompatible } returns true
every { version2.antiFeatureKeys } returns emptyList()
every { versionDao.getAppVersions(repoId, packageName) } returns
MutableLiveData(listOf(version, version2))
// Install at the same version code so both versions appear in installedVersions
setupInstalledApp(versionCode = versionCode, isInstalled = true)
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
// version (null signer, first in list) is matched, version2 (mismatched signer) is skipped
assertEquals(version, item.installedVersion)
cancelAndPrintRemainingEvents()
}
}
@Test
fun signerMismatchHidesInstallButtonForVersion() = runTest {
// When the installed app's signer does not match a version's signer, the version must
// not show an installation button even if its version code is higher than the installed one.
every { version.signer } returns SignerV2(listOf("a_different_signer_hash"))
setupInstalledApp(versionCode = versionCode - 1)
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
val versionItem = item.versions?.single()
assertNotNull(versionItem)
assertFalse(versionItem.isSignerCompatible)
assertFalse(versionItem.showInstallButton) // hidden because of signer mismatch
// Since the signer doesn't match allowedSigners, no update is suggested either
assertNull(item.suggestedVersion)
assertEquals(MainButtonState.NONE, item.mainButtonState)
cancelAndPrintRemainingEvents()
}
}
// Misc
@Test
fun showsAntiFeaturesOnboardingWhenFlagIsSet() = runTest {
showAntiFeaturesOnboardingFlow.value = true
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertTrue(item.showAntiFeaturesOnboarding)
cancelAndPrintRemainingEvents()
}
}
@Test
fun showsWarningWhenIssueIsPresent() = runTest {
val issue: AppIssue = NotAvailable
appsWithIssuesFlow.value =
listOf(
AppWithIssueItem(
packageName = packageName,
name = testApp.name,
installedVersionName = "1.0",
installedVersionCode = versionCode,
issue = issue,
lastUpdated = 200L,
)
)
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertEquals(issue, item.issue)
assertTrue(item.showWarnings)
assertFalse(item.isIncompatible)
cancelAndPrintRemainingEvents()
}
}
@Test
fun showsWarningWhenTargetSdkIsTooOld() = runTest {
// targetSdk 28 on SDK 34 means isAutoUpdateSupported() = false, so oldTargetSdk = true
every { version.packageManifest } returns
AppManifest(
versionName = "1.0",
versionCode = versionCode,
usesSdk = UsesSdkV2(minSdkVersion = 21, targetSdkVersion = 28),
)
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertTrue(item.oldTargetSdk)
assertTrue(item.showWarnings)
assertFalse(item.isIncompatible)
assertNull(item.issue)
cancelAndPrintRemainingEvents()
}
}
@Test
fun hidesAuthorContactWhenNoEmailNorWebSite() = runTest {
every { app.metadata } returns testApp.app.copy(authorEmail = null, authorWebSite = null)
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertFalse(item.showAuthorContact)
cancelAndPrintRemainingEvents()
}
}
@Test
fun showsAuthorHasMoreThanOneApp() = runTest {
val authorName = "Test Dev"
every { app.authorName } returns authorName
every { appDao.hasAuthorMoreThanOneApp(authorName) } returns MutableLiveData(true)
presenterFlow.test {
// First non-null item has the initial produceState value of false
val item1 = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item1)
assertFalse(item1.authorHasMoreThanOneApp)
// Second emission reflects the actual DB result
val item2 = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item2)
assertTrue(item2.authorHasMoreThanOneApp)
cancelAndPrintRemainingEvents()
}
}
// Repository visibility
@Test
fun hidesRepositoriesWhenAppIsInDefaultRepoOnly() = runTest {
// The app's repo address is listed as a default address -> repo chooser should stay hidden
every { repoPreLoader.defaultRepoAddresses } returns setOf(repository.address)
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertTrue(item.repositories.isEmpty())
cancelAndPrintRemainingEvents()
}
}
@Test
fun showsRepositoriesWhenAppIsInNonDefaultRepo() = runTest {
// The repo address is NOT a default address -> repo chooser must be shown
every { repoPreLoader.defaultRepoAddresses } returns emptySet()
presenterFlow.test {
// Initially repositories are empty because of async load
val item1 = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item1)
assertEquals(emptyList(), item1.repositories)
// After the async load the single non-default repo becomes visible
val item2 = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item2)
assertEquals(repository, item2.repositories.single())
cancelAndPrintRemainingEvents()
}
}
@Test
fun showsMultipleRepositoriesWhenAppInMultipleRepos() = runTest {
// set up a second repo this app is in
val repoId2 = 2L
val repo2 =
Repository(
repoId = repoId2,
address = "https://example.com/second/repo",
timestamp = 200L,
formatVersion = IndexFormatVersion.TWO,
certificate = "abcde",
version = 2L,
weight = 50,
lastUpdated = 500L,
)
every { appDao.getRepositoryIdsForApp(packageName) } returns listOf(repoId, repoId2)
every { repoManager.getRepository(repoId2) } returns repo2
presenterFlow.test {
// repos are loaded async, so the list is empty by default (don't show repo chooser)
val item1 = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item1)
assertEquals(emptyList(), item1.repositories)
// now the app is in two repos
val item2 = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item2)
assertEquals(2, item2.repositories.size)
cancelAndPrintRemainingEvents()
}
}
// App preferences
@Test
fun usesPreferredRepoIdFromAppPrefs() = runTest {
// When the user has chosen a preferred repo, preferredRepoId must reflect that choice
// rather than defaulting to the app's own repoId.
val customPreferredRepoId = 99L
every { appPrefsDao.getAppPrefs(packageName) } returns
MutableLiveData(AppPrefs(packageName, preferredRepoId = customPreferredRepoId))
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertEquals(customPreferredRepoId, item.preferredRepoId)
cancelAndPrintRemainingEvents()
}
}
@Test
fun ignoresAllUpdatesFromAppPrefs() = runTest {
// When the user ignores all updates, ignoresAllUpdates must be true and there should be
// no suggested version, resulting in a NONE button.
every { appPrefsDao.getAppPrefs(packageName) } returns
MutableLiveData(AppPrefs(packageName).toggleIgnoreAllUpdates())
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertTrue(item.ignoresAllUpdates)
assertNull(item.suggestedVersion)
assertEquals(MainButtonState.NONE, item.mainButtonState)
cancelAndPrintRemainingEvents()
}
}
@Test
fun ignoresCurrentUpdateFromAppPrefs() = runTest {
setupInstalledApp(versionCode = versionCode - 1)
every { appPrefsDao.getAppPrefs(packageName) } returns
MutableLiveData(AppPrefs(packageName, ignoreVersionCodeUpdate = versionCode))
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
// possibleUpdate exists, but ignores, so suggestedVersion is null
assertNull(item.suggestedVersion)
assertEquals(MainButtonState.NONE, item.mainButtonState)
assertNotNull(item.actions.ignoreThisUpdate) // there is a version to un-ignore
assertTrue(item.ignoresCurrentUpdate)
assertFalse(item.ignoresAllUpdates)
cancelAndPrintRemainingEvents()
}
}
@Test
fun allowsBetaVersionsFromAppPrefs() = runTest {
// When the user opts into beta versions, allowsBetaVersions must be reflected in the item.
every { appPrefsDao.getAppPrefs(packageName) } returns
MutableLiveData(AppPrefs(packageName).toggleReleaseChannel(RELEASE_CHANNEL_BETA))
presenterFlow.test {
val item = awaitNonNullItem()
assertIs<LoadedAppDetailsItem>(item)
assertTrue(item.allowsBetaVersions)
cancelAndPrintRemainingEvents()
}
}
private fun setupInstalledApp(versionCode: Long, isInstalled: Boolean = false) {
val signature = mockk<Signature>()
every { signature.toByteArray() } returns byteArrayOf(0xAB.toByte(), 0xCD.toByte())
val packageInfo =
spyk(PackageInfo()).also {
it.packageName = packageName
it.versionName = "0.1"
@Suppress("DEPRECATION")
it.signatures = arrayOf(signature)
}
mockkStatic(PackageInfoCompat::getLongVersionCode)
every { getLongVersionCode(packageInfo) } returns versionCode
appInfoFlow.value =
AppInfo(
packageName = packageName,
packageInfo = packageInfo,
launchIntent = if (isInstalled) Intent() else null,
)
}
private suspend fun ReceiveTurbine<AppDetailsItem?>.awaitNonNullItem(): AppDetailsItem {
var item: AppDetailsItem? = null
var count = 0
while (item == null) {
item = awaitItem()
count++
}
println("Received non-null item after $count emissions")
return item
}
private suspend fun ReceiveTurbine<AppDetailsItem?>.cancelAndPrintRemainingEvents() {
val lastItems = cancelAndConsumeRemainingEvents()
if (!lastItems.isEmpty()) println("Received additional items after cancellation")
lastItems.forEach { item -> println(" $item") }
}
}

View File

@@ -5,6 +5,7 @@ import android.content.pm.ApplicationInfo
import android.content.pm.InstallSourceInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.Signature
import android.os.Build
import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode
import io.mockk.every
@@ -493,7 +494,7 @@ internal class DbAppCheckerTest {
}
every { appInfo.loadLabel(packageManager) } returns appName
val sig = mockk<android.content.pm.Signature>()
val sig = mockk<Signature>()
every { sig.toByteArray() } returns signerBytes
val packageInfo =