Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Verified Commit 1e44b1ad authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

test: cover install state sync flows

Add unit/instrumentation coverage for install button mapping, progress reconciliation, and search action rendering so expected behaviors are documented.

Refine install-state
helpers and PWA URL lookup to satisfy static analysis without changing behavior.
parent b3927bca
Loading
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -226,7 +226,7 @@ class SearchResultListItemTest {
                            showPrivacyScore = true,
                            isPrivacyLoading = true,
                            primaryAction = PrimaryActionUiState(
                                label = "Download",
                                label = "",
                                enabled = true,
                                isInProgress = true,
                                isFilledStyle = true,
@@ -243,7 +243,6 @@ class SearchResultListItemTest {

        composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIMARY_PROGRESS)
            .assertIsDisplayed()
        composeRule.onAllNodesWithText("Download").assertCountEquals(0)
        composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIVACY_PROGRESS)
            .assertIsDisplayed()
        composeRule.onAllNodesWithText("07/10").assertCountEquals(0)
+117 −1
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.flow.flowOf
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
@@ -47,6 +48,9 @@ import foundation.e.apps.data.application.data.Application
import foundation.e.apps.data.application.data.Ratings
import foundation.e.apps.data.enums.Source
import foundation.e.apps.data.enums.Status
import foundation.e.apps.ui.compose.state.ButtonLabel
import foundation.e.apps.ui.compose.state.InstallButtonAction
import foundation.e.apps.ui.compose.state.InstallButtonState
import foundation.e.apps.ui.compose.theme.AppTheme
import foundation.e.apps.ui.search.v2.SearchTabType
import java.util.Locale
@@ -104,6 +108,7 @@ class SearchResultsContentTest {
                        selectedTab = selectedTab,
                        fossItems = fossItems,
                        pwaItems = pwaItems,
                        playStoreItems = null,
                        searchVersion = 0,
                        getScrollPosition = { null },
                        onScrollPositionChange = { _, _, _ -> },
@@ -111,6 +116,7 @@ class SearchResultsContentTest {
                            selectedTab = tab
                            selectedTabs.add(tab)
                        },
                        installButtonStateProvider = { defaultInstallButtonState() },
                    )
                }
            }
@@ -164,7 +170,14 @@ class SearchResultsContentTest {
                        status = Status.UPDATABLE,
                    ),
                )
            )
            ),
            installButtonStateProvider = { app ->
                when (app.status) {
                    Status.INSTALLED -> InstallButtonState(label = ButtonLabel(resId = R.string.open))
                    Status.UPDATABLE -> InstallButtonState(label = ButtonLabel(resId = R.string.install))
                    else -> defaultInstallButtonState()
                }
            }
        )

        composeRule.onNodeWithText("com.example.rated").assertIsDisplayed()
@@ -250,18 +263,114 @@ class SearchResultsContentTest {
            .assertIsDisplayed()
    }

    @Test
    fun primaryAction_prefers_literal_label_text() {
        renderSearchResults(
            tabs = listOf(SearchTabType.OPEN_SOURCE),
            selectedTab = SearchTabType.OPEN_SOURCE,
            fossPagingData = pagingData(listOf(sampleApp("Paid App"))),
            installButtonStateProvider = {
                InstallButtonState(label = ButtonLabel(text = "$2.99"))
            }
        )

        composeRule.onNodeWithText("$2.99").assertIsDisplayed()
    }

    @Test
    fun primaryAction_uses_progress_percent_when_no_label_text() {
        renderSearchResults(
            tabs = listOf(SearchTabType.OPEN_SOURCE),
            selectedTab = SearchTabType.OPEN_SOURCE,
            fossPagingData = pagingData(listOf(sampleApp("Downloading App"))),
            installButtonStateProvider = {
                InstallButtonState(
                    progressPercentText = "45%",
                    actionIntent = InstallButtonAction.CancelDownload,
                )
            }
        )

        composeRule.onNodeWithText("45%").assertIsDisplayed()
    }

    @Test
    fun primaryAction_uses_resource_label_when_no_text_or_percent() {
        val openLabel = composeRule.activity.getString(R.string.open)

        renderSearchResults(
            tabs = listOf(SearchTabType.OPEN_SOURCE),
            selectedTab = SearchTabType.OPEN_SOURCE,
            fossPagingData = pagingData(listOf(sampleApp("Open App"))),
            installButtonStateProvider = {
                InstallButtonState(label = ButtonLabel(resId = R.string.open))
            }
        )

        composeRule.onNodeWithText(openLabel).assertIsDisplayed()
    }

    @Test
    fun primaryAction_shows_spinner_when_in_progress_and_blank() {
        renderSearchResults(
            tabs = listOf(SearchTabType.OPEN_SOURCE),
            selectedTab = SearchTabType.OPEN_SOURCE,
            fossPagingData = pagingData(listOf(sampleApp("Installing App"))),
            installButtonStateProvider = {
                InstallButtonState(
                    label = ButtonLabel(text = ""),
                    showProgressBar = true,
                )
            }
        )

        composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIMARY_PROGRESS)
            .assertIsDisplayed()
    }

    @Test
    fun primaryAction_click_forwards_action_intent() {
        var capturedAction: InstallButtonAction? = null

        renderSearchResults(
            tabs = listOf(SearchTabType.OPEN_SOURCE),
            selectedTab = SearchTabType.OPEN_SOURCE,
            fossPagingData = pagingData(listOf(sampleApp("Update App"))),
            installButtonStateProvider = {
                InstallButtonState(
                    label = ButtonLabel(text = "Update"),
                    actionIntent = InstallButtonAction.UpdateSelfConfirm,
                )
            },
            onPrimaryActionClick = { _, action -> capturedAction = action }
        )

        composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIMARY_BUTTON)
            .performClick()

        composeRule.runOnIdle {
            assertEquals(InstallButtonAction.UpdateSelfConfirm, capturedAction)
        }
    }

    private fun renderSearchResults(
        tabs: List<SearchTabType>,
        selectedTab: SearchTabType,
        fossPagingData: PagingData<Application>,
        pwaPagingData: PagingData<Application>? = null,
        playStorePagingData: PagingData<Application>? = null,
        searchVersion: Int = 0,
        installButtonStateProvider: (Application) -> InstallButtonState = { defaultInstallButtonState() },
        onPrimaryActionClick: (Application, InstallButtonAction) -> Unit = { _, _ -> },
    ) {
        composeRule.setContent {
            val fossItems = remember { flowOf(fossPagingData) }.collectAsLazyPagingItems()
            val pwaItems = pwaPagingData?.let {
                remember(it) { flowOf(it) }.collectAsLazyPagingItems()
            }
            val playStoreItems = playStorePagingData?.let {
                remember(it) { flowOf(it) }.collectAsLazyPagingItems()
            }

            AppTheme(darkTheme = false) {
                Surface(color = MaterialTheme.colorScheme.background) {
@@ -270,10 +379,13 @@ class SearchResultsContentTest {
                        selectedTab = selectedTab,
                        fossItems = fossItems,
                        pwaItems = pwaItems,
                        playStoreItems = playStoreItems,
                        searchVersion = searchVersion,
                        getScrollPosition = { null },
                        onScrollPositionChange = { _, _, _ -> },
                        onTabSelect = {},
                        onPrimaryActionClick = onPrimaryActionClick,
                        installButtonStateProvider = installButtonStateProvider,
                    )
                }
            }
@@ -294,4 +406,8 @@ class SearchResultsContentTest {
            refresh = LoadState.NotLoading(endOfPaginationReached = false)
        )
    )

    private fun defaultInstallButtonState() = InstallButtonState(
        label = ButtonLabel(resId = R.string.open),
    )
}
+15 −42
Original line number Diff line number Diff line
@@ -143,50 +143,23 @@ class AppManagerWrapper @Inject constructor(
        progress: DownloadProgress
    ): Int {
        val downloadIds = appInstall.downloadIdMap.keys
        if (downloadIds.isEmpty()) {
            // Download request exists but ids not yet populated; show 0% instead of dropping percent.
            return 0
        }

        var percent = 0
        if (downloadIds.isNotEmpty()) {
            val totalSizeBytes = progress.totalSizeBytes
                .filterKeys { downloadIds.contains(it) }
                .values
                .sum()
        if (totalSizeBytes <= 0) {
            return 0
        }

            if (totalSizeBytes > 0) {
                val downloadedSoFar = progress.bytesDownloadedSoFar
                    .filterKeys { downloadIds.contains(it) }
                    .values
                    .sum()

        val percent = ((downloadedSoFar / totalSizeBytes.toDouble()) * 100)
                percent = ((downloadedSoFar / totalSizeBytes.toDouble()) * PERCENTAGE_MULTIPLIER)
                    .toInt()
            .coerceIn(0, 100)
        return percent
                    .coerceIn(0, PERCENTAGE_MULTIPLIER)
            }

    private suspend fun isProgressValidForApp(
        application: Application,
        downloadProgress: DownloadProgress
    ): Boolean {
        val download = getDownloadList().singleOrNull {
            it.id == application._id && it.packageName == application.package_name
        } ?: return false

        /*
         * We cannot rely on a single downloadId because DownloadProgress aggregates
         * multiple ids and downloadId is overwritten while iterating.
         * Validation instead checks whether any of the app's download ids are present
         * in the progress maps, which makes progress computation resilient to
         * concurrent multi-part downloads.
         */
        val appDownloadIds = download.downloadIdMap.keys
        return appDownloadIds.any { id ->
            downloadProgress.totalSizeBytes.containsKey(id) ||
                    downloadProgress.bytesDownloadedSoFar.containsKey(id)
        }
        return percent
    }

    fun handleRatingFormat(rating: Double): String? {
+11 −10
Original line number Diff line number Diff line
@@ -119,8 +119,7 @@ class PwaManager @Inject constructor(
     * Used for periodic status polling in Compose search to mirror legacy status detection.
     */
    fun getInstalledPwaUrls(): Set<String> {
        val installed = mutableSetOf<String>()
        context.contentResolver.query(
        return context.contentResolver.query(
            PWA_PLAYER.toUri(),
            arrayOf("url"),
            null,
@@ -128,16 +127,18 @@ class PwaManager @Inject constructor(
            null
        )?.use { cursor ->
            val urlIndex = cursor.getColumnIndex("url")
            if (urlIndex != -1) {
            if (urlIndex == -1) {
                return@use emptySet()
            }
            val installed = mutableSetOf<String>()
            while (cursor.moveToNext()) {
                val url = cursor.getString(urlIndex)
                if (!url.isNullOrBlank()) {
                    installed.add(url)
                }
            }
            }
        }
        return installed
            installed
        } ?: emptySet()
    }

    suspend fun installPWAApp(appInstall: AppInstall) {
+0 −4
Original line number Diff line number Diff line
@@ -284,8 +284,6 @@ private fun PrimaryActionArea(
    val buttonContent: @Composable () -> Unit = {
        val showSpinner = uiState.isInProgress && uiState.label.isBlank()
        if (showSpinner) {
            val indicatorColor =
                if (uiState.isFilledStyle) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onPrimary
            CircularProgressIndicator(
                modifier = Modifier
                    .size(16.dp)
@@ -294,8 +292,6 @@ private fun PrimaryActionArea(
                color = labelTextColor,
            )
        } else {
            val textColor =
                if (uiState.isFilledStyle) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface
            Text(
                text = uiState.label,
                maxLines = 1,
Loading