Loading app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultListItemTest.kt +2 −52 Original line number Diff line number Diff line Loading @@ -54,7 +54,6 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } Loading @@ -70,7 +69,6 @@ class SearchResultListItemTest { fun metadataAndClicks_areWiredCorrectly() { var itemClicks = 0 var primaryClicks = 0 var privacyClicks = 0 composeRule.setContent { AppTheme(darkTheme = false) { Loading @@ -81,11 +79,6 @@ class SearchResultListItemTest { author = "Signal LLC", ratingText = "4.5", showRating = true, sourceTag = "Play Store", showSourceTag = true, privacyScore = "06/10", showPrivacyScore = true, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "Install", enabled = true, Loading @@ -96,7 +89,6 @@ class SearchResultListItemTest { onItemClick = { itemClicks += 1 }, onPrimaryActionClick = { primaryClicks += 1 }, onShowMoreClick = {}, onPrivacyClick = { privacyClicks += 1 }, ) } } Loading @@ -105,9 +97,7 @@ class SearchResultListItemTest { composeRule.onNodeWithText("Signal").assertIsDisplayed() composeRule.onNodeWithText("Signal LLC").assertIsDisplayed() composeRule.onNodeWithText("4.5").assertIsDisplayed() composeRule.onNodeWithText("Play Store").assertIsDisplayed() composeRule.onNodeWithText("Install").assertIsDisplayed() composeRule.onNodeWithText("06/10").assertIsDisplayed() composeRule.onAllNodesWithTag(SearchResultListItemTestTags.SHOW_MORE) .assertCountEquals(0) Loading @@ -115,18 +105,15 @@ class SearchResultListItemTest { .performClick() composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIMARY_BUTTON) .performClick() composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIVACY_BADGE) .performClick() composeRule.runOnIdle { assertEquals(1, itemClicks) assertEquals(1, primaryClicks) assertEquals(1, privacyClicks) } } @Test fun hidesRatingAndSourceTag_whenDisabled() { fun hidesRating_whenDisabled() { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { Loading @@ -136,11 +123,6 @@ class SearchResultListItemTest { author = "Anonymous", ratingText = "4.9", showRating = false, sourceTag = "Play Store", showSourceTag = false, privacyScore = "", showPrivacyScore = false, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "Install", enabled = true, Loading @@ -151,14 +133,12 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } } composeRule.onAllNodesWithText("4.9").assertCountEquals(0) composeRule.onAllNodesWithText("Play Store").assertCountEquals(0) } @Test Loading @@ -175,11 +155,6 @@ class SearchResultListItemTest { author = "Author", ratingText = "", showRating = false, sourceTag = "", showSourceTag = false, privacyScore = "", showPrivacyScore = false, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "Install", enabled = true, Loading @@ -191,7 +166,6 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = { showMoreClicks += 1 }, onPrivacyClick = {}, ) } } Loading @@ -210,7 +184,7 @@ class SearchResultListItemTest { } @Test fun inProgressPrimaryAction_andPrivacyLoading_showSpinners() { fun inProgressPrimaryAction_showsSpinner() { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { Loading @@ -220,11 +194,6 @@ class SearchResultListItemTest { author = "Author", ratingText = "", showRating = false, sourceTag = "", showSourceTag = false, privacyScore = "07/10", showPrivacyScore = true, isPrivacyLoading = true, primaryAction = PrimaryActionUiState( label = "", enabled = true, Loading @@ -235,7 +204,6 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } Loading @@ -243,9 +211,6 @@ class SearchResultListItemTest { composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIMARY_PROGRESS) .assertIsDisplayed() composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIVACY_PROGRESS) .assertIsDisplayed() composeRule.onAllNodesWithText("07/10").assertCountEquals(0) } private fun sampleApp(name: String) = Application(name = name) Loading @@ -254,11 +219,6 @@ class SearchResultListItemTest { author = "", ratingText = "", showRating = false, sourceTag = "", showSourceTag = false, privacyScore = "", showPrivacyScore = false, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "", enabled = false, Loading @@ -274,21 +234,11 @@ class SearchResultListItemTest { author: String, ratingText: String, showRating: Boolean, sourceTag: String, showSourceTag: Boolean, privacyScore: String, showPrivacyScore: Boolean, isPrivacyLoading: Boolean, primaryAction: PrimaryActionUiState, ): SearchResultListItemState = SearchResultListItemState( author = author, ratingText = ratingText, showRating = showRating, sourceTag = sourceTag, showSourceTag = showSourceTag, privacyScore = privacyScore, showPrivacyScore = showPrivacyScore, isPrivacyLoading = isPrivacyLoading, primaryAction = primaryAction, iconUrl = null, placeholderResId = null, Loading app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt +2 −47 Original line number Diff line number Diff line Loading @@ -204,51 +204,6 @@ class SearchResultsContentTest { composeRule.onAllNodesWithText("Open App").assertCountEquals(0) } @Test fun refreshError_showsRetry() { val pagingData = PagingData.empty<Application>( sourceLoadStates = loadStates(refresh = LoadState.Error(RuntimeException("boom"))) ) renderSearchResults( tabs = listOf(SearchTabType.OPEN_SOURCE), selectedTab = SearchTabType.OPEN_SOURCE, fossPagingData = pagingData, ) composeRule.onNodeWithText( composeRule.activity.getString(R.string.search_error) ).assertIsDisplayed() composeRule.onNodeWithText( composeRule.activity.getString(R.string.retry) ).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>( Loading @@ -256,7 +211,7 @@ class SearchResultsContentTest { refresh = LoadState.NotLoading(endOfPaginationReached = true) ) ) val noAppsText = composeRule.activity.getString(R.string.no_apps_found) val noAppsText = composeRule.activity.getString(R.string.search_empty_results_body) renderSearchResults( tabs = listOf(SearchTabType.OPEN_SOURCE), Loading @@ -269,7 +224,7 @@ class SearchResultsContentTest { @Test fun emptyResults_resetOnNewQuery_showsRefreshLoading() { val noAppsText = composeRule.activity.getString(R.string.no_apps_found) val noAppsText = composeRule.activity.getString(R.string.search_empty_results_body) val emptyPagingData = PagingData.empty<Application>( sourceLoadStates = loadStates( refresh = LoadState.NotLoading(endOfPaginationReached = true) Loading app/src/androidTest/java/foundation/e/apps/ui/compose/components/search/SearchErrorStateTest.kt +4 −10 Original line number Diff line number Diff line Loading @@ -41,16 +41,13 @@ class SearchErrorStateTest { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { SearchErrorState(onRetry = {}, fullScreen = true) SearchErrorState(errorTitleStringRes = R.string.search_error_title_opensource, fullScreen = true) } } } composeRule.onNodeWithText( composeRule.activity.getString(R.string.search_error) ).assertIsDisplayed() composeRule.onNodeWithText( composeRule.activity.getString(R.string.retry) composeRule.activity.getString(R.string.search_error_message) ).assertIsDisplayed() } Loading @@ -59,16 +56,13 @@ class SearchErrorStateTest { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { SearchErrorState(onRetry = {}, fullScreen = false) SearchErrorState(errorTitleStringRes = R.string.search_error_title_opensource, fullScreen = false) } } } composeRule.onNodeWithText( composeRule.activity.getString(R.string.search_error) ).assertIsDisplayed() composeRule.onNodeWithText( composeRule.activity.getString(R.string.retry) composeRule.activity.getString(R.string.search_error_message) ).assertIsDisplayed() } } app/src/androidTest/java/foundation/e/apps/ui/compose/screens/SearchTopBarTest.kt +5 −97 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import foundation.e.apps.R import foundation.e.apps.ui.search.v2.SearchUiState import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test Loading @@ -53,16 +54,13 @@ class SearchTopBarTest { val composeRule = createAndroidComposeRule<ComponentActivity>() @Test fun typingQuery_expandsSearchBar_andUpdatesQueryState() { fun typingQuery_updatesQueryState() { val recorder = SearchTopBarRecorder() val hintText = composeRule.activity.getString(R.string.search_hint) composeRule.setContent { SearchTopBarTestContent( initialQuery = "", suggestions = emptyList(), initialExpanded = false, showSuggestions = false, recorder = recorder, ) } Loading @@ -78,21 +76,17 @@ class SearchTopBarTest { .performTextInput("camera") composeRule.runOnIdle { assertTrue(recorder.expandedChanges.contains(true)) assertTrue(recorder.queryChanges.contains("camera")) assertEquals("camera", recorder.queryChanges.lastOrNull()) } } @Test fun submitQuery_collapsesSearchBar_andClearsFocus() { fun submitQuery_clearsFocus_andCallsSubmit() { val recorder = SearchTopBarRecorder() composeRule.setContent { SearchTopBarTestContent( initialQuery = "vpn", suggestions = emptyList(), initialExpanded = true, showSuggestions = false, recorder = recorder, ) } Loading @@ -103,7 +97,6 @@ class SearchTopBarTest { inputField.performImeAction() composeRule.runOnIdle { assertTrue(recorder.expandedChanges.contains(false)) assertTrue(recorder.searchSubmissions.contains("vpn")) } Loading @@ -112,15 +105,12 @@ class SearchTopBarTest { } @Test fun clearButton_clearsQuery_keepsExpanded_andFocusesInput() { fun clearButton_clearsQuery_andFocusesInput() { val recorder = SearchTopBarRecorder() composeRule.setContent { SearchTopBarTestContent( initialQuery = "maps", suggestions = emptyList(), initialExpanded = true, showSuggestions = false, recorder = recorder, ) } Loading @@ -134,7 +124,6 @@ class SearchTopBarTest { composeRule.runOnIdle { assertTrue(recorder.clearTapped) assertTrue(recorder.expandedChanges.contains(true)) } composeRule.onNodeWithTag(SearchTopBarTestTags.INPUT_FIELD) Loading @@ -148,9 +137,6 @@ class SearchTopBarTest { composeRule.setContent { SearchTopBarTestContent( initialQuery = "news", suggestions = emptyList(), initialExpanded = true, showSuggestions = false, recorder = recorder, ) } Loading @@ -169,75 +155,11 @@ class SearchTopBarTest { composeRule.onNodeWithTag(SearchTopBarTestTags.INPUT_FIELD) .assertIsNotFocused() } @Test fun suggestions_callbackFires_whenSuggestionSelected() { // Material3 SearchBar renders dropdown in a popup window inaccessible to standard // Compose test APIs. This test verifies the callback logic is wired correctly // by simulating a suggestion selection through the callback. val recorder = SearchTopBarRecorder() val suggestions = listOf("camera", "camera apps", "camera pro") var simulateSuggestionClick: ((String) -> Unit)? = null composeRule.setContent { var query by remember { mutableStateOf("cam") } var expanded by remember { mutableStateOf(true) } val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current // Capture the suggestion select callback for manual invocation simulateSuggestionClick = { suggestion -> expanded = false focusManager.clearFocus() recorder.suggestionSelections.add(suggestion) recorder.expandedChanges.add(false) } SearchTopBar( uiState = SearchUiState( query = query, suggestions = suggestions, ), expanded = expanded, showSuggestions = expanded, focusRequester = focusRequester, focusManager = focusManager, onQueryChange = { query = it }, onClearQuery = { query = "" }, onSearchSubmit = {}, onSuggestionSelect = { suggestion -> expanded = false focusManager.clearFocus() recorder.suggestionSelections.add(suggestion) recorder.expandedChanges.add(false) }, onExpandedChange = { expanded = it }, onBack = {}, modifier = Modifier.fillMaxSize(), ) } // Verify the SearchBar is displayed and expanded composeRule.onNodeWithTag(SearchTopBarTestTags.SEARCH_BAR) .assertIsDisplayed() // Simulate suggestion selection via callback composeRule.runOnIdle { simulateSuggestionClick?.invoke("camera apps") } composeRule.runOnIdle { assertTrue(recorder.expandedChanges.contains(false)) assertTrue(recorder.suggestionSelections.contains("camera apps")) } } } private class SearchTopBarRecorder { val queryChanges = mutableListOf<String>() val searchSubmissions = mutableListOf<String>() val suggestionSelections = mutableListOf<String>() val expandedChanges = mutableListOf<Boolean>() var clearTapped = false var backTapped = false } Loading @@ -245,23 +167,16 @@ private class SearchTopBarRecorder { @Composable private fun SearchTopBarTestContent( initialQuery: String, suggestions: List<String>, initialExpanded: Boolean, showSuggestions: Boolean, recorder: SearchTopBarRecorder, ) { var query by remember { mutableStateOf(initialQuery) } var expanded by remember { mutableStateOf(initialExpanded) } val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current SearchTopBar( uiState = SearchUiState( query = query, suggestions = suggestions, ), expanded = expanded, showSuggestions = showSuggestions && expanded, focusRequester = focusRequester, focusManager = focusManager, onQueryChange = { updatedQuery -> Loading @@ -275,13 +190,6 @@ private fun SearchTopBarTestContent( onSearchSubmit = { submittedQuery -> recorder.searchSubmissions.add(submittedQuery) }, onSuggestionSelect = { suggestion -> recorder.suggestionSelections.add(suggestion) }, onExpandedChange = { isExpanded -> expanded = isExpanded recorder.expandedChanges.add(isExpanded) }, onBack = { recorder.backTapped = true }, Loading app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +9 −9 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ package foundation.e.apps.ui.application import android.annotation.SuppressLint import android.content.Intent import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Bundle import android.text.Html Loading Loading @@ -85,6 +84,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale import javax.inject.Inject import foundation.e.elib.R as eR @AndroidEntryPoint class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { Loading Loading @@ -868,8 +868,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun MaterialButton.disableInstallButton(buttonStringID: Int) { isEnabled = false text = context.getString(buttonStringID) strokeColor = ContextCompat.getColorStateList(context, R.color.light_grey) setTextColor(context.getColor(R.color.light_grey)) strokeColor = ContextCompat.getColorStateList(context, eR.color.e_disabled_color) setTextColor(context.getColor(eR.color.e_disabled_color)) backgroundTintList = ContextCompat.getColorStateList(context, android.R.color.transparent) } Loading @@ -877,8 +877,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun MaterialButton.enableInstallButton(buttonStringID: Int) { isEnabled = true text = context.getString(buttonStringID) strokeColor = ContextCompat.getColorStateList(context, R.color.colorAccent) setTextColor(context.getColor(R.color.colorAccent)) strokeColor = ContextCompat.getColorStateList(context, eR.color.e_accent) setTextColor(context.getColor(eR.color.e_accent)) backgroundTintList = ContextCompat.getColorStateList(context, android.R.color.transparent) } Loading Loading @@ -914,9 +914,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } else { getString(R.string.update) } setTextColor(Color.WHITE) setTextColor(ContextCompat.getColor(view.context, eR.color.e_background)) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) ContextCompat.getColorStateList(view.context, eR.color.e_accent) setOnClickListener { if (mainActivityViewModel.checkUnsupportedApplication(application, activity)) { return@setOnClickListener Loading @@ -941,9 +941,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { appSize.visibility = View.VISIBLE installButton.apply { enableInstallButton(R.string.open) setTextColor(Color.WHITE) setTextColor(ContextCompat.getColor(view.context, eR.color.e_background)) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) ContextCompat.getColorStateList(view.context, eR.color.e_accent) setOnClickListener { if (application.is_pwa) { pwaManager.launchPwa(application) Loading Loading
app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultListItemTest.kt +2 −52 Original line number Diff line number Diff line Loading @@ -54,7 +54,6 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } Loading @@ -70,7 +69,6 @@ class SearchResultListItemTest { fun metadataAndClicks_areWiredCorrectly() { var itemClicks = 0 var primaryClicks = 0 var privacyClicks = 0 composeRule.setContent { AppTheme(darkTheme = false) { Loading @@ -81,11 +79,6 @@ class SearchResultListItemTest { author = "Signal LLC", ratingText = "4.5", showRating = true, sourceTag = "Play Store", showSourceTag = true, privacyScore = "06/10", showPrivacyScore = true, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "Install", enabled = true, Loading @@ -96,7 +89,6 @@ class SearchResultListItemTest { onItemClick = { itemClicks += 1 }, onPrimaryActionClick = { primaryClicks += 1 }, onShowMoreClick = {}, onPrivacyClick = { privacyClicks += 1 }, ) } } Loading @@ -105,9 +97,7 @@ class SearchResultListItemTest { composeRule.onNodeWithText("Signal").assertIsDisplayed() composeRule.onNodeWithText("Signal LLC").assertIsDisplayed() composeRule.onNodeWithText("4.5").assertIsDisplayed() composeRule.onNodeWithText("Play Store").assertIsDisplayed() composeRule.onNodeWithText("Install").assertIsDisplayed() composeRule.onNodeWithText("06/10").assertIsDisplayed() composeRule.onAllNodesWithTag(SearchResultListItemTestTags.SHOW_MORE) .assertCountEquals(0) Loading @@ -115,18 +105,15 @@ class SearchResultListItemTest { .performClick() composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIMARY_BUTTON) .performClick() composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIVACY_BADGE) .performClick() composeRule.runOnIdle { assertEquals(1, itemClicks) assertEquals(1, primaryClicks) assertEquals(1, privacyClicks) } } @Test fun hidesRatingAndSourceTag_whenDisabled() { fun hidesRating_whenDisabled() { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { Loading @@ -136,11 +123,6 @@ class SearchResultListItemTest { author = "Anonymous", ratingText = "4.9", showRating = false, sourceTag = "Play Store", showSourceTag = false, privacyScore = "", showPrivacyScore = false, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "Install", enabled = true, Loading @@ -151,14 +133,12 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } } composeRule.onAllNodesWithText("4.9").assertCountEquals(0) composeRule.onAllNodesWithText("Play Store").assertCountEquals(0) } @Test Loading @@ -175,11 +155,6 @@ class SearchResultListItemTest { author = "Author", ratingText = "", showRating = false, sourceTag = "", showSourceTag = false, privacyScore = "", showPrivacyScore = false, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "Install", enabled = true, Loading @@ -191,7 +166,6 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = { showMoreClicks += 1 }, onPrivacyClick = {}, ) } } Loading @@ -210,7 +184,7 @@ class SearchResultListItemTest { } @Test fun inProgressPrimaryAction_andPrivacyLoading_showSpinners() { fun inProgressPrimaryAction_showsSpinner() { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { Loading @@ -220,11 +194,6 @@ class SearchResultListItemTest { author = "Author", ratingText = "", showRating = false, sourceTag = "", showSourceTag = false, privacyScore = "07/10", showPrivacyScore = true, isPrivacyLoading = true, primaryAction = PrimaryActionUiState( label = "", enabled = true, Loading @@ -235,7 +204,6 @@ class SearchResultListItemTest { onItemClick = {}, onPrimaryActionClick = {}, onShowMoreClick = {}, onPrivacyClick = {}, ) } } Loading @@ -243,9 +211,6 @@ class SearchResultListItemTest { composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIMARY_PROGRESS) .assertIsDisplayed() composeRule.onNodeWithTag(SearchResultListItemTestTags.PRIVACY_PROGRESS) .assertIsDisplayed() composeRule.onAllNodesWithText("07/10").assertCountEquals(0) } private fun sampleApp(name: String) = Application(name = name) Loading @@ -254,11 +219,6 @@ class SearchResultListItemTest { author = "", ratingText = "", showRating = false, sourceTag = "", showSourceTag = false, privacyScore = "", showPrivacyScore = false, isPrivacyLoading = false, primaryAction = PrimaryActionUiState( label = "", enabled = false, Loading @@ -274,21 +234,11 @@ class SearchResultListItemTest { author: String, ratingText: String, showRating: Boolean, sourceTag: String, showSourceTag: Boolean, privacyScore: String, showPrivacyScore: Boolean, isPrivacyLoading: Boolean, primaryAction: PrimaryActionUiState, ): SearchResultListItemState = SearchResultListItemState( author = author, ratingText = ratingText, showRating = showRating, sourceTag = sourceTag, showSourceTag = showSourceTag, privacyScore = privacyScore, showPrivacyScore = showPrivacyScore, isPrivacyLoading = isPrivacyLoading, primaryAction = primaryAction, iconUrl = null, placeholderResId = null, Loading
app/src/androidTest/java/foundation/e/apps/ui/compose/components/SearchResultsContentTest.kt +2 −47 Original line number Diff line number Diff line Loading @@ -204,51 +204,6 @@ class SearchResultsContentTest { composeRule.onAllNodesWithText("Open App").assertCountEquals(0) } @Test fun refreshError_showsRetry() { val pagingData = PagingData.empty<Application>( sourceLoadStates = loadStates(refresh = LoadState.Error(RuntimeException("boom"))) ) renderSearchResults( tabs = listOf(SearchTabType.OPEN_SOURCE), selectedTab = SearchTabType.OPEN_SOURCE, fossPagingData = pagingData, ) composeRule.onNodeWithText( composeRule.activity.getString(R.string.search_error) ).assertIsDisplayed() composeRule.onNodeWithText( composeRule.activity.getString(R.string.retry) ).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>( Loading @@ -256,7 +211,7 @@ class SearchResultsContentTest { refresh = LoadState.NotLoading(endOfPaginationReached = true) ) ) val noAppsText = composeRule.activity.getString(R.string.no_apps_found) val noAppsText = composeRule.activity.getString(R.string.search_empty_results_body) renderSearchResults( tabs = listOf(SearchTabType.OPEN_SOURCE), Loading @@ -269,7 +224,7 @@ class SearchResultsContentTest { @Test fun emptyResults_resetOnNewQuery_showsRefreshLoading() { val noAppsText = composeRule.activity.getString(R.string.no_apps_found) val noAppsText = composeRule.activity.getString(R.string.search_empty_results_body) val emptyPagingData = PagingData.empty<Application>( sourceLoadStates = loadStates( refresh = LoadState.NotLoading(endOfPaginationReached = true) Loading
app/src/androidTest/java/foundation/e/apps/ui/compose/components/search/SearchErrorStateTest.kt +4 −10 Original line number Diff line number Diff line Loading @@ -41,16 +41,13 @@ class SearchErrorStateTest { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { SearchErrorState(onRetry = {}, fullScreen = true) SearchErrorState(errorTitleStringRes = R.string.search_error_title_opensource, fullScreen = true) } } } composeRule.onNodeWithText( composeRule.activity.getString(R.string.search_error) ).assertIsDisplayed() composeRule.onNodeWithText( composeRule.activity.getString(R.string.retry) composeRule.activity.getString(R.string.search_error_message) ).assertIsDisplayed() } Loading @@ -59,16 +56,13 @@ class SearchErrorStateTest { composeRule.setContent { AppTheme(darkTheme = false) { Surface(color = MaterialTheme.colorScheme.background) { SearchErrorState(onRetry = {}, fullScreen = false) SearchErrorState(errorTitleStringRes = R.string.search_error_title_opensource, fullScreen = false) } } } composeRule.onNodeWithText( composeRule.activity.getString(R.string.search_error) ).assertIsDisplayed() composeRule.onNodeWithText( composeRule.activity.getString(R.string.retry) composeRule.activity.getString(R.string.search_error_message) ).assertIsDisplayed() } }
app/src/androidTest/java/foundation/e/apps/ui/compose/screens/SearchTopBarTest.kt +5 −97 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ import androidx.compose.ui.test.performTextInput import androidx.test.ext.junit.runners.AndroidJUnit4 import foundation.e.apps.R import foundation.e.apps.ui.search.v2.SearchUiState import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Rule import org.junit.Test Loading @@ -53,16 +54,13 @@ class SearchTopBarTest { val composeRule = createAndroidComposeRule<ComponentActivity>() @Test fun typingQuery_expandsSearchBar_andUpdatesQueryState() { fun typingQuery_updatesQueryState() { val recorder = SearchTopBarRecorder() val hintText = composeRule.activity.getString(R.string.search_hint) composeRule.setContent { SearchTopBarTestContent( initialQuery = "", suggestions = emptyList(), initialExpanded = false, showSuggestions = false, recorder = recorder, ) } Loading @@ -78,21 +76,17 @@ class SearchTopBarTest { .performTextInput("camera") composeRule.runOnIdle { assertTrue(recorder.expandedChanges.contains(true)) assertTrue(recorder.queryChanges.contains("camera")) assertEquals("camera", recorder.queryChanges.lastOrNull()) } } @Test fun submitQuery_collapsesSearchBar_andClearsFocus() { fun submitQuery_clearsFocus_andCallsSubmit() { val recorder = SearchTopBarRecorder() composeRule.setContent { SearchTopBarTestContent( initialQuery = "vpn", suggestions = emptyList(), initialExpanded = true, showSuggestions = false, recorder = recorder, ) } Loading @@ -103,7 +97,6 @@ class SearchTopBarTest { inputField.performImeAction() composeRule.runOnIdle { assertTrue(recorder.expandedChanges.contains(false)) assertTrue(recorder.searchSubmissions.contains("vpn")) } Loading @@ -112,15 +105,12 @@ class SearchTopBarTest { } @Test fun clearButton_clearsQuery_keepsExpanded_andFocusesInput() { fun clearButton_clearsQuery_andFocusesInput() { val recorder = SearchTopBarRecorder() composeRule.setContent { SearchTopBarTestContent( initialQuery = "maps", suggestions = emptyList(), initialExpanded = true, showSuggestions = false, recorder = recorder, ) } Loading @@ -134,7 +124,6 @@ class SearchTopBarTest { composeRule.runOnIdle { assertTrue(recorder.clearTapped) assertTrue(recorder.expandedChanges.contains(true)) } composeRule.onNodeWithTag(SearchTopBarTestTags.INPUT_FIELD) Loading @@ -148,9 +137,6 @@ class SearchTopBarTest { composeRule.setContent { SearchTopBarTestContent( initialQuery = "news", suggestions = emptyList(), initialExpanded = true, showSuggestions = false, recorder = recorder, ) } Loading @@ -169,75 +155,11 @@ class SearchTopBarTest { composeRule.onNodeWithTag(SearchTopBarTestTags.INPUT_FIELD) .assertIsNotFocused() } @Test fun suggestions_callbackFires_whenSuggestionSelected() { // Material3 SearchBar renders dropdown in a popup window inaccessible to standard // Compose test APIs. This test verifies the callback logic is wired correctly // by simulating a suggestion selection through the callback. val recorder = SearchTopBarRecorder() val suggestions = listOf("camera", "camera apps", "camera pro") var simulateSuggestionClick: ((String) -> Unit)? = null composeRule.setContent { var query by remember { mutableStateOf("cam") } var expanded by remember { mutableStateOf(true) } val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current // Capture the suggestion select callback for manual invocation simulateSuggestionClick = { suggestion -> expanded = false focusManager.clearFocus() recorder.suggestionSelections.add(suggestion) recorder.expandedChanges.add(false) } SearchTopBar( uiState = SearchUiState( query = query, suggestions = suggestions, ), expanded = expanded, showSuggestions = expanded, focusRequester = focusRequester, focusManager = focusManager, onQueryChange = { query = it }, onClearQuery = { query = "" }, onSearchSubmit = {}, onSuggestionSelect = { suggestion -> expanded = false focusManager.clearFocus() recorder.suggestionSelections.add(suggestion) recorder.expandedChanges.add(false) }, onExpandedChange = { expanded = it }, onBack = {}, modifier = Modifier.fillMaxSize(), ) } // Verify the SearchBar is displayed and expanded composeRule.onNodeWithTag(SearchTopBarTestTags.SEARCH_BAR) .assertIsDisplayed() // Simulate suggestion selection via callback composeRule.runOnIdle { simulateSuggestionClick?.invoke("camera apps") } composeRule.runOnIdle { assertTrue(recorder.expandedChanges.contains(false)) assertTrue(recorder.suggestionSelections.contains("camera apps")) } } } private class SearchTopBarRecorder { val queryChanges = mutableListOf<String>() val searchSubmissions = mutableListOf<String>() val suggestionSelections = mutableListOf<String>() val expandedChanges = mutableListOf<Boolean>() var clearTapped = false var backTapped = false } Loading @@ -245,23 +167,16 @@ private class SearchTopBarRecorder { @Composable private fun SearchTopBarTestContent( initialQuery: String, suggestions: List<String>, initialExpanded: Boolean, showSuggestions: Boolean, recorder: SearchTopBarRecorder, ) { var query by remember { mutableStateOf(initialQuery) } var expanded by remember { mutableStateOf(initialExpanded) } val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current SearchTopBar( uiState = SearchUiState( query = query, suggestions = suggestions, ), expanded = expanded, showSuggestions = showSuggestions && expanded, focusRequester = focusRequester, focusManager = focusManager, onQueryChange = { updatedQuery -> Loading @@ -275,13 +190,6 @@ private fun SearchTopBarTestContent( onSearchSubmit = { submittedQuery -> recorder.searchSubmissions.add(submittedQuery) }, onSuggestionSelect = { suggestion -> recorder.suggestionSelections.add(suggestion) }, onExpandedChange = { isExpanded -> expanded = isExpanded recorder.expandedChanges.add(isExpanded) }, onBack = { recorder.backTapped = true }, Loading
app/src/main/java/foundation/e/apps/ui/application/ApplicationFragment.kt +9 −9 Original line number Diff line number Diff line Loading @@ -20,7 +20,6 @@ package foundation.e.apps.ui.application import android.annotation.SuppressLint import android.content.Intent import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Bundle import android.text.Html Loading Loading @@ -85,6 +84,7 @@ import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale import javax.inject.Inject import foundation.e.elib.R as eR @AndroidEntryPoint class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { Loading Loading @@ -868,8 +868,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun MaterialButton.disableInstallButton(buttonStringID: Int) { isEnabled = false text = context.getString(buttonStringID) strokeColor = ContextCompat.getColorStateList(context, R.color.light_grey) setTextColor(context.getColor(R.color.light_grey)) strokeColor = ContextCompat.getColorStateList(context, eR.color.e_disabled_color) setTextColor(context.getColor(eR.color.e_disabled_color)) backgroundTintList = ContextCompat.getColorStateList(context, android.R.color.transparent) } Loading @@ -877,8 +877,8 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { private fun MaterialButton.enableInstallButton(buttonStringID: Int) { isEnabled = true text = context.getString(buttonStringID) strokeColor = ContextCompat.getColorStateList(context, R.color.colorAccent) setTextColor(context.getColor(R.color.colorAccent)) strokeColor = ContextCompat.getColorStateList(context, eR.color.e_accent) setTextColor(context.getColor(eR.color.e_accent)) backgroundTintList = ContextCompat.getColorStateList(context, android.R.color.transparent) } Loading Loading @@ -914,9 +914,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { } else { getString(R.string.update) } setTextColor(Color.WHITE) setTextColor(ContextCompat.getColor(view.context, eR.color.e_background)) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) ContextCompat.getColorStateList(view.context, eR.color.e_accent) setOnClickListener { if (mainActivityViewModel.checkUnsupportedApplication(application, activity)) { return@setOnClickListener Loading @@ -941,9 +941,9 @@ class ApplicationFragment : TimeoutFragment(R.layout.fragment_application) { appSize.visibility = View.VISIBLE installButton.apply { enableInstallButton(R.string.open) setTextColor(Color.WHITE) setTextColor(ContextCompat.getColor(view.context, eR.color.e_background)) backgroundTintList = ContextCompat.getColorStateList(view.context, R.color.colorAccent) ContextCompat.getColorStateList(view.context, eR.color.e_accent) setOnClickListener { if (application.is_pwa) { pwaManager.launchPwa(application) Loading