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

Verified Commit 6eb01428 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury
Browse files

test: ensure test coverage for error handling in search results

parent 53b03bc7
Loading
Loading
Loading
Loading
Loading
+79 −0
Original line number Diff line number Diff line
@@ -238,6 +238,31 @@ class SearchResultsContentTest {
        ).assertIsDisplayed()
    }

    @Test
    fun appendError_showsFooterRetryWithResults() {
        val pagingData = PagingData.from(
            listOf(sampleApp("Loaded App")),
            sourceLoadStates = loadStates(
                refresh = LoadState.NotLoading(endOfPaginationReached = false),
                append = LoadState.Error(RuntimeException("append boom"))
            )
        )

        renderSearchResults(
            tabs = listOf(SearchTabType.OPEN_SOURCE),
            selectedTab = SearchTabType.OPEN_SOURCE,
            fossPagingData = pagingData,
        )

        composeRule.onNodeWithText("Loaded App").assertIsDisplayed()
        composeRule.onNodeWithText(
            composeRule.activity.getString(R.string.search_error)
        ).assertIsDisplayed()
        composeRule.onNodeWithText(
            composeRule.activity.getString(R.string.retry)
        ).assertIsDisplayed()
    }

    @Test
    fun emptyResults_showsPlaceholder() {
        val pagingData = PagingData.empty<Application>(
@@ -256,6 +281,60 @@ class SearchResultsContentTest {
        composeRule.onNodeWithText(noAppsText).assertIsDisplayed()
    }

    @Test
    fun emptyResults_resetOnNewQuery_showsRefreshLoading() {
        val noAppsText = composeRule.activity.getString(R.string.no_apps_found)
        val emptyPagingData = PagingData.empty<Application>(
            sourceLoadStates = loadStates(
                refresh = LoadState.NotLoading(endOfPaginationReached = true)
            )
        )
        val loadingPagingData = PagingData.empty<Application>(
            sourceLoadStates = loadStates(
                refresh = LoadState.Loading
            )
        )
        lateinit var updateQueryState: () -> Unit

        composeRule.setContent {
            val searchVersion = remember { mutableStateOf(0) }
            val pagingData = remember { mutableStateOf(emptyPagingData) }
            updateQueryState = {
                searchVersion.value = 1
                pagingData.value = loadingPagingData
            }
            val fossItems = remember(pagingData.value) {
                flowOf(pagingData.value)
            }.collectAsLazyPagingItems()

            AppTheme(darkTheme = false) {
                Surface(color = MaterialTheme.colorScheme.background) {
                    SearchResultsContent(
                        tabs = listOf(SearchTabType.OPEN_SOURCE),
                        selectedTab = SearchTabType.OPEN_SOURCE,
                        fossItems = fossItems,
                        pwaItems = null,
                        playStoreItems = null,
                        searchVersion = searchVersion.value,
                        getScrollPosition = { null },
                        onScrollPositionChange = { _, _, _ -> },
                        onTabSelect = {},
                        installButtonStateProvider = { defaultInstallButtonState() },
                    )
                }
            }
        }

        composeRule.onNodeWithText(noAppsText).assertIsDisplayed()
        composeRule.runOnIdle {
            updateQueryState()
        }
        composeRule.waitForIdle()
        composeRule.onNodeWithTag(SearchResultsContentTestTags.REFRESH_LOADER)
            .assertIsDisplayed()
        composeRule.onAllNodesWithText(noAppsText).assertCountEquals(0)
    }

    @Test
    fun appendLoading_showsBottomSpinner() {
        val pagingData = PagingData.from(
+74 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 */

package foundation.e.apps.ui.compose.components.search

import androidx.activity.ComponentActivity
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.test.ext.junit.runners.AndroidJUnit4
import foundation.e.apps.R
import foundation.e.apps.ui.compose.theme.AppTheme
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class SearchErrorStateTest {
    @get:Rule
    val composeRule = createAndroidComposeRule<ComponentActivity>()

    @Test
    fun fullScreenError_displaysMessageAndRetry() {
        composeRule.setContent {
            AppTheme(darkTheme = false) {
                Surface(color = MaterialTheme.colorScheme.background) {
                    SearchErrorState(onRetry = {}, fullScreen = true)
                }
            }
        }

        composeRule.onNodeWithText(
            composeRule.activity.getString(R.string.search_error)
        ).assertIsDisplayed()
        composeRule.onNodeWithText(
            composeRule.activity.getString(R.string.retry)
        ).assertIsDisplayed()
    }

    @Test
    fun footerError_displaysMessageAndRetry() {
        composeRule.setContent {
            AppTheme(darkTheme = false) {
                Surface(color = MaterialTheme.colorScheme.background) {
                    SearchErrorState(onRetry = {}, fullScreen = false)
                }
            }
        }

        composeRule.onNodeWithText(
            composeRule.activity.getString(R.string.search_error)
        ).assertIsDisplayed()
        composeRule.onNodeWithText(
            composeRule.activity.getString(R.string.retry)
        ).assertIsDisplayed()
    }
}
+51 −3
Original line number Diff line number Diff line
@@ -354,7 +354,12 @@ class SearchViewModelV2Test {
        openSourceSelected = false
        pwaSelected = false
        buildViewModel()
        every { playStorePagingRepository.playStoreSearch(any(), any()) } returns flowOf(PagingData.empty())
        every {
            playStorePagingRepository.playStoreSearch(
                any(),
                any()
            )
        } returns flowOf(PagingData.empty())

        viewModel.onSearchSubmitted("  android ")
        viewModel.playStorePagingFlow.first()
@@ -449,6 +454,37 @@ class SearchViewModelV2Test {
        assertEquals(firstVersion + 1, viewModel.uiState.value.searchVersion)
    }

    @Test
    fun `blank search submit keeps search version`() = runTest {
        viewModel.onSearchSubmitted("first")
        val firstVersion = viewModel.uiState.value.searchVersion

        viewModel.onSearchSubmitted("   ")

        assertEquals(firstVersion, viewModel.uiState.value.searchVersion)
    }

    @Test
    fun `search submit emits new requests for repeated query`() = runTest {
        playStoreSelected = true
        openSourceSelected = false
        pwaSelected = false
        buildViewModel()
        every {
            playStorePagingRepository.playStoreSearch(
                any(),
                any()
            )
        } returns flowOf(PagingData.empty())

        viewModel.onSearchSubmitted("apps")
        viewModel.playStorePagingFlow.first()
        viewModel.onSearchSubmitted("apps")
        viewModel.playStorePagingFlow.first()

        verify(exactly = 2) { playStorePagingRepository.playStoreSearch("apps", 20) }
    }

    @Test
    fun `search submit clears scroll positions`() = runTest {
        viewModel.updateScrollPosition(SearchTabType.COMMON_APPS, 6, 4)
@@ -467,7 +503,13 @@ class SearchViewModelV2Test {
            package_name = "com.example.app",
            status = Status.DOWNLOADING,
        )
        every { searchPagingRepository.cleanApkSearch(any()) } returns flowOf(PagingData.from(listOf(app)))
        every { searchPagingRepository.cleanApkSearch(any()) } returns flowOf(
            PagingData.from(
                listOf(
                    app
                )
            )
        )
        coEvery { installStatusReconciler.reconcile(any(), any(), any()) } returns
                InstallStatusReconciler.Result(app, 42)

@@ -510,7 +552,13 @@ class SearchViewModelV2Test {
            package_name = "",
            status = Status.UPDATABLE,
        )
        every { searchPagingRepository.cleanApkSearch(any()) } returns flowOf(PagingData.from(listOf(app)))
        every { searchPagingRepository.cleanApkSearch(any()) } returns flowOf(
            PagingData.from(
                listOf(
                    app
                )
            )
        )
        coEvery { installStatusReconciler.reconcile(any(), any(), any()) } returns
                InstallStatusReconciler.Result(app, null)