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

Commit 31ebd4d2 authored by Fahim M. Choudhury's avatar Fahim M. Choudhury Committed by Fahim M. Choudhury
Browse files

test: add tests for SearchViewModelV2 covering preference-based tab selection

parent f05af72d
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -152,7 +152,7 @@ class SearchViewModelV2 @Inject constructor(
        onSearchSubmitted(suggestion)
    }

    fun onClearQuery() {
    fun onQueryCleared() {
        suggestionJob?.cancel()
        _uiState.update { current ->
            if (current.hasSubmittedSearch && current.availableTabs.isNotEmpty()) {
@@ -179,7 +179,7 @@ class SearchViewModelV2 @Inject constructor(
    fun onSearchSubmitted(submitted: String) {
        val trimmedQuery = submitted.trim()
        if (trimmedQuery.isEmpty()) {
            onClearQuery()
            onQueryCleared()
            return
        }

+235 −30
Original line number Diff line number Diff line
@@ -18,13 +18,24 @@

package foundation.e.apps.ui.search.v2

import android.content.SharedPreferences
import foundation.e.apps.data.Constants.PREFERENCE_SHOW_FOSS
import foundation.e.apps.data.Constants.PREFERENCE_SHOW_GPLAY
import foundation.e.apps.data.Stores
import foundation.e.apps.data.cleanapk.repositories.CleanApkAppsRepository
import foundation.e.apps.data.cleanapk.repositories.CleanApkPwaRepository
import foundation.e.apps.data.playstore.PlayStoreRepository
import foundation.e.apps.data.preference.AppLoungePreference
import foundation.e.apps.data.search.FakeSuggestionSource
import foundation.e.apps.data.search.SuggestionSource
import foundation.e.apps.util.MainCoroutineRule
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -38,93 +49,287 @@ class SearchViewModelV2Test {
    @get:Rule
    val mainCoroutineRule = MainCoroutineRule()

    private lateinit var suggestionSource: SuggestionSource
    private lateinit var suggestionSource: FakeSuggestionSource
    private lateinit var preference: AppLoungePreference
    private lateinit var stores: Stores
    private var playStoreSelected = true
    private var openSourceSelected = true
    private var pwaSelected = false
    private var preferenceListener: SharedPreferences.OnSharedPreferenceChangeListener? = null
    private lateinit var viewModel: SearchViewModelV2

    @Before
    fun setUp() {
        suggestionSource = FakeSuggestionSource()
        viewModel = SearchViewModelV2(suggestionSource)
        preference = mockk(relaxed = true)

        stores = buildStores()

        every { preference.isPlayStoreSelected() } answers { playStoreSelected }
        every { preference.isOpenSourceSelected() } answers { openSourceSelected }
        every { preference.isPWASelected() } answers { pwaSelected }
        every { preference.registerStorePreferenceListener(any()) } answers {
            preferenceListener = arg(0)
            Unit
        }
        every { preference.unregisterStorePreferenceListener(any()) } answers {
            if (preferenceListener == arg<SharedPreferences.OnSharedPreferenceChangeListener>(0)) {
                preferenceListener = null
            }
            Unit
        }

        buildViewModel()
    }

    private fun buildStores(): Stores {
        val playStoreRepository = mockk<PlayStoreRepository>(relaxed = true)
        val cleanApkAppsRepository = mockk<CleanApkAppsRepository>(relaxed = true)
        val cleanApkPwaRepository = mockk<CleanApkPwaRepository>(relaxed = true)

        return Stores(
            playStoreRepository,
            cleanApkAppsRepository,
            cleanApkPwaRepository,
            preference
        )
    }

    @Test
    fun `non-blank query loads suggestions after debounce`() = runTest {
        viewModel.onQueryChanged("tel")
    fun `play store disabled hides suggestions when typing`() = runTest {
        playStoreSelected = false

        viewModel.onQueryChanged("apps")
        advanceDebounce()

        val state = viewModel.uiState.value
        assertEquals(listOf("Telegram", "Telegram FOSS", "Telegram X"), state.suggestions.take(3))
        assertTrue(state.isSuggestionVisible)
        assertEquals("tel", state.query)
        assertTrue(state.suggestions.isEmpty())
        assertFalse(state.isSuggestionVisible)
        assertEquals("apps", state.query)
    }

    @Test
    fun `blank query clears suggestions and hides immediately`() = runTest {
    fun `matching query shows suggestions when play store enabled`() = runTest {
        playStoreSelected = true

        viewModel.onQueryChanged("tel")
        advanceDebounce()

        val state = viewModel.uiState.value
        assertFalse(state.suggestions.isEmpty())
        assertTrue(state.isSuggestionVisible)
        assertEquals("tel", state.query)
    }

    @Test
    fun `blank query before submit clears tabs and results`() = runTest {
        viewModel.onQueryChanged("   ")

        val state = viewModel.uiState.value
        assertTrue(state.suggestions.isEmpty())
        assertTrue(state.resultsByTab.isEmpty())
        assertEquals(visibleTabs(), state.availableTabs)
        assertEquals(visibleTabs().firstOrNull(), state.selectedTab)
        assertFalse(state.hasSubmittedSearch)
    }

    @Test
    fun `blank query after submit hides suggestions but keeps results`() = runTest {
        viewModel.onSearchSubmitted("query")
        val resultsBefore = viewModel.uiState.value.resultsByTab
        val tabsBefore = viewModel.uiState.value.availableTabs

        viewModel.onQueryChanged("   ")

        val state = viewModel.uiState.value
        assertEquals(resultsBefore, state.resultsByTab)
        assertEquals(tabsBefore, state.availableTabs)
        assertTrue(state.hasSubmittedSearch)
        assertFalse(state.isSuggestionVisible)
        assertEquals("", state.query)
    }

    @Test
    fun `empty suggestions keep dropdown hidden`() = runTest {
        viewModel.onQueryChanged("zzz")
    fun `clear query after submit retains tabs and results`() = runTest {
        viewModel.onSearchSubmitted("query")
        val resultsBefore = viewModel.uiState.value.resultsByTab
        val tabsBefore = viewModel.uiState.value.availableTabs

        advanceDebounce()
        viewModel.onQueryCleared()

        val state = viewModel.uiState.value
        assertEquals(resultsBefore, state.resultsByTab)
        assertEquals(tabsBefore, state.availableTabs)
        assertTrue(state.hasSubmittedSearch)
        assertEquals("", state.query)
        assertFalse(state.isSuggestionVisible)
    }

    @Test
    fun `search submit trims query and builds per tab results`() = runTest {
        playStoreSelected = true
        openSourceSelected = true
        pwaSelected = true
        buildViewModel()

        viewModel.onSearchSubmitted("  spaced query  ")

        val state = viewModel.uiState.value
        assertEquals("spaced query", state.query)
        assertEquals(visibleTabs(), state.availableTabs)
        assertTrue(state.resultsByTab.keys.containsAll(visibleTabs()))
        assertTrue(state.resultsByTab[SearchTabType.WEB_APPS]!!.all { it.contains("spaced query") })
        assertTrue(state.resultsByTab.values.all { it.size == 6 })
        assertTrue(state.hasSubmittedSearch)
        assertTrue(state.suggestions.isEmpty())
        assertFalse(state.isSuggestionVisible)
        assertEquals("zzz", state.query)
    }

    @Test
    fun `clear query resets state`() = runTest {
        viewModel.onQueryChanged("sig")
        advanceDebounce()
    fun `search submit with no visible tabs yields no results`() = runTest {
        playStoreSelected = false
        openSourceSelected = false
        pwaSelected = false
        buildViewModel()

        viewModel.onSearchSubmitted("anything")

        viewModel.onQueryCleared()
        val state = viewModel.uiState.value
        assertTrue(state.availableTabs.isEmpty())
        assertTrue(state.resultsByTab.isEmpty())
        assertNull(state.selectedTab)
        assertFalse(state.hasSubmittedSearch)
    }

    @Test
    fun `search submit with blank query clears state`() = runTest {
        playStoreSelected = true
        buildViewModel()

        viewModel.onSearchSubmitted("   ")

        val state = viewModel.uiState.value
        assertTrue(state.resultsByTab.isEmpty())
        assertFalse(state.hasSubmittedSearch)
        assertEquals(visibleTabs(), state.availableTabs)
        assertEquals(visibleTabs().firstOrNull(), state.selectedTab)
        assertEquals("", state.query)
        assertTrue(state.suggestions.isEmpty())
        assertFalse(state.isSuggestionVisible)
    }

    @Test
    fun `selecting suggestion commits value and hides dropdown`() = runTest {
    fun `store change after submit rebuilds tabs and results`() = runTest {
        playStoreSelected = true
        openSourceSelected = false
        pwaSelected = false
        buildViewModel()
        viewModel.onSearchSubmitted("apps")

        playStoreSelected = false
        openSourceSelected = true
        notifyPreferenceChange(PREFERENCE_SHOW_FOSS)

        val state = viewModel.uiState.value
        assertEquals(listOf(SearchTabType.OPEN_SOURCE), state.availableTabs)
        assertEquals(SearchTabType.OPEN_SOURCE, state.selectedTab)
        assertTrue(state.resultsByTab.keys == setOf(SearchTabType.OPEN_SOURCE))
        assertTrue(state.resultsByTab[SearchTabType.OPEN_SOURCE]!!.all { it.contains("apps") })
        assertTrue(state.hasSubmittedSearch)
    }

    @Test
    fun `store change hides suggestions when play store turns off`() = runTest {
        playStoreSelected = true
        buildViewModel()
        viewModel.onQueryChanged("tel")
        advanceDebounce()
        assertTrue(viewModel.uiState.value.isSuggestionVisible)

        viewModel.onSuggestionSelected("Telegram X")
        playStoreSelected = false
        notifyPreferenceChange(PREFERENCE_SHOW_GPLAY)

        val state = viewModel.uiState.value
        assertEquals("Telegram X", state.query)
        assertTrue(state.suggestions.isEmpty())
        assertFalse(state.isSuggestionVisible)
    }

    @Test
    fun `submitting search keeps query and clears suggestions`() = runTest {
        viewModel.onQueryChanged("sig")
        advanceDebounce()
    fun `store change removing all tabs clears submitted state`() = runTest {
        playStoreSelected = true
        openSourceSelected = false
        pwaSelected = false
        buildViewModel()
        viewModel.onSearchSubmitted("apps")

        viewModel.onSearchSubmitted("Signal")
        playStoreSelected = false
        openSourceSelected = false
        pwaSelected = false
        notifyPreferenceChange(PREFERENCE_SHOW_GPLAY)

        val state = viewModel.uiState.value
        assertTrue(state.availableTabs.isEmpty())
        assertTrue(state.resultsByTab.isEmpty())
        assertNull(state.selectedTab)
        assertFalse(state.hasSubmittedSearch)
    }

    @Test
    fun `tab selection ignores unavailable tabs`() = runTest {
        playStoreSelected = true
        openSourceSelected = true
        pwaSelected = false
        buildViewModel()

        viewModel.onTabSelected(SearchTabType.WEB_APPS)
        assertEquals(SearchTabType.COMMON_APPS, viewModel.uiState.value.selectedTab)

        viewModel.onTabSelected(SearchTabType.OPEN_SOURCE)
        assertEquals(SearchTabType.OPEN_SOURCE, viewModel.uiState.value.selectedTab)
    }

    @Test
    fun `on suggestion selected delegates to search submission`() = runTest {
        playStoreSelected = true
        buildViewModel()

        viewModel.onSuggestionSelected("Signal ")

        val state = viewModel.uiState.value
        assertEquals("Signal", state.query)
        assertTrue(state.suggestions.isEmpty())
        assertFalse(state.isSuggestionVisible)
        assertTrue(state.hasSubmittedSearch)
    }

    @Test
    fun `on cleared unregisters preference listener`() {
        playStoreSelected = true
        buildViewModel()
        assertNotNull(preferenceListener)

        invokeOnCleared()

        assertNull(preferenceListener)
    }

    private fun advanceDebounce() {
        mainCoroutineRule.testDispatcher.scheduler.advanceTimeBy(DEBOUNCE_MS)
        mainCoroutineRule.testDispatcher.scheduler.runCurrent()
    }

    private fun visibleTabs(): List<SearchTabType> = buildList {
        if (playStoreSelected) add(SearchTabType.COMMON_APPS)
        if (openSourceSelected) add(SearchTabType.OPEN_SOURCE)
        if (pwaSelected) add(SearchTabType.WEB_APPS)
    }

    private fun notifyPreferenceChange(key: String) {
        preferenceListener?.onSharedPreferenceChanged(null, key)
    }

    private fun buildViewModel() {
        viewModel = SearchViewModelV2(suggestionSource, preference, stores)
    }

    private fun invokeOnCleared() {
        val method = SearchViewModelV2::class.java.getDeclaredMethod("onCleared")
        method.isAccessible = true
        method.invoke(viewModel)
    }
}