Skip to main content

Command Palette

Search for a command to run...

Chapter 31 : ImageSearchScreen Composable Testing (Final Code)

Published
3 min readView as Markdown
package androidavatar.learning.artlearnersapp.presentation.ui

import androidx.compose.ui.test.*
import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule
import org.junit.Test

class ImageSearchScreenTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    /**
     * Test 1 – Verify basic UI elements (TextField + Search icon)
     */
    @Test
    fun initialScreen_displaysSearchFieldAndButton() {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState()
            )
        }

        composeTestRule.onNodeWithText("Search Image").assertIsDisplayed()
        composeTestRule.onNodeWithContentDescription("Search").assertIsDisplayed()
    }

    /**
     * Test 2 – Loading indicator is visible when state.isLoading = true
     */
    @Test
    fun whenLoading_showsCircularProgressIndicator() {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(isLoading = true)
            )
        }

        // Add test tag to your CircularProgressIndicator in the composable if not already:
        // CircularProgressIndicator(modifier = Modifier.testTag("Progress"))
        composeTestRule.onNodeWithTag("Progress").assertExists()
    }

    /**
     * Test 3 – Error message and "Go Back" button appear on error
     */
    @Test
    fun whenErrorMessagePresent_displaysErrorAndButton() {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(errorMessage = "Network Error")
            )
        }

        composeTestRule.onNodeWithText("Network Error").assertIsDisplayed()
        composeTestRule.onNodeWithText("Go Back").assertIsDisplayed()
    }

    /**
     * Test 4 – Search callback triggers when valid query entered (≥3 chars)
     */
    @Test
    fun enteringValidQuery_triggersOnSearchCallback() {
        var queryTriggered = false
        var receivedQuery: String? = null

        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {
                    queryTriggered = true
                    receivedQuery = it
                },
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState()
            )
        }

        composeTestRule.onNodeWithText("Search Image").performTextInput("mountain")
        composeTestRule.onNodeWithContentDescription("Search").performClick()

        assert(queryTriggered)
        assert(receivedQuery == "mountain")
    }

    /**
     * Test 5 – Invalid short query (<3 chars) should not trigger callback
     */
    @Test
    fun enteringShortQuery_doesNotTriggerSearch() {
        var searchTriggered = false

        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = { searchTriggered = true },
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState()
            )
        }

        composeTestRule.onNodeWithText("Search Image").performTextInput("ab")
        composeTestRule.onNodeWithContentDescription("Search").performClick()

        assert(!searchTriggered)
    }

    /**
     * Test 6 – Clicking an image triggers onImageClick with correct URL
     */
    @Test
    fun clickingImage_triggersImageClickCallback() {
        val fakeUrls = listOf("url1", "url2")
        var clickedUrl: String? = null

        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = { clickedUrl = it },
                onBack = {},
                state = SearchImageUiState(searchResults = fakeUrls)
            )
        }

        composeTestRule.onAllNodes(hasContentDescription("Search result image"))
            .onFirst()
            .performClick()

        assert(clickedUrl == "url1")
    }

    /**
     * Test 7 – Verify loading state hides images and error UI
     */
    @Test
    fun loadingState_hidesImagesAndErrorMessage() {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(isLoading = true)
            )
        }

        composeTestRule.onNodeWithTag("Progress").assertExists()
        composeTestRule.onNodeWithText("Go Back").assertDoesNotExist()
        composeTestRule.onAllNodes(hasContentDescription("Search result image"))
            .assertCountEquals(0)
    }

    /**
     * Test 8 – Verify results are displayed correctly when available
     */
    @Test
    fun whenResultsAvailable_displaysGridOfImages() {
        val fakeUrls = listOf("url1", "url2", "url3")

        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(searchResults = fakeUrls)
            )
        }

        composeTestRule.onAllNodes(hasContentDescription("Search result image"))
            .assertCountEquals(3)
    }

    /**
     * Test 9 – Verify error screen hides loading and image grid
     */
    @Test
    fun whenError_displaysOnlyErrorUI() {
        composeTestRule.setContent {
            ImageSearchScreen(
                onSearch = {},
                onImageClick = {},
                onBack = {},
                state = SearchImageUiState(errorMessage = "Something went wrong")
            )
        }

        composeTestRule.onNodeWithText("Something went wrong").assertIsDisplayed()
        composeTestRule.onNodeWithTag("Progress").assertDoesNotExist()
        composeTestRule.onAllNodes(hasContentDescription("Search result image"))
            .assertCountEquals(0)
    }
}

More from this blog

Architecting Android: The Right way

31 posts

Exploring Clean Architecture in Android with Kotlin and Jetpack Compose — simplifying design patterns, testing, and app structure for curious developers.