'How to tell the composeTestRule to wait for the navhost transition?

I'm trying to write an integration test for an Android application entirely written in Compose that has a single Activity and uses the Compose Navigation to change the screen content.

I managed to properly interact and test the first screen that is shown by the navigation graph but, as soon as I navigate to a new destination, the test fails because it does not wait for the NavHost to load the new content.

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
    @get:Rule
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test
    fun appStartsWithoutCrashing() {
        composeTestRule.apply {
            // Check Switch
            onNodeWithTag(FirstScreen.CONSENT_SWITCH)
                .assertIsDisplayed()
                .assertIsOff()
                .performClick()
                .assertIsOn()

            // Click accept button
            onNodeWithTag(FirstScreen.ACCEPT_BUTTON)
                .assertIsDisplayed()
                .performClick()

            // Check we are inside the second screen
            onNodeWithTag(SecondScreen.USERNAME_TEXT_FIELD)
                .assertIsDisplayed()
        }
    }
}

I'm sure that is a timing issue because if I add a Thread.sleep(500) before the onNodeWithTag(SecondScreen.USERNAME_TEXT_FIELD).assertIsDisplayed(), the test is successful. But I would like to avoid Thread.sleep()s in my code.

Is there a better way to tell the composeTestRule to wait for the NavHost to load the new content before executing the assertIsDisplayed()?

PS I know that would be better to test the Composables in isolation, but I really want to simulate the user input on the App using Espresso and not only test the Composable behavior.



Solution 1:[1]

As suggested in this very informative blog article, waitUntil can be used to wait until the node with the right tag is shown:

            // Waiting for the new destination to be shown
            waitUntil {
                composeTestRule
                    .onAllNodesWithTag(LogInTestTags.USERNAME_TEXT_FIELD)
                    .fetchSemanticsNodes().size == 1
            }

Or, after adding some sugar:

@RunWith(AndroidJUnit4::class)
class MainActivityTest {
    @get:Rule
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Test
    fun appStartsWithoutCrashing() {
        composeTestRule.apply {
            // Check Switch
            onNodeWithTag(FirstScreen.CONSENT_SWITCH)
                .assertIsDisplayed()
                .assertIsOff()
                .performClick()
                .assertIsOn()

            // Click accept button
            onNodeWithTag(FirstScreen.ACCEPT_BUTTON)
                .assertIsDisplayed()
                .performClick()

            // Waiting for the new destination to be shown
            waitUntilExists(hasTestTag(SecondScreen.USERNAME_TEXT_FIELD))

            // Check we are inside the second screen
            onNodeWithTag(SecondScreen.USERNAME_TEXT_FIELD)
                .assertIsDisplayed()
        }
    }
}

private const val WAIT_UNTIL_TIMEOUT = 1_000L

fun ComposeContentTestRule.waitUntilNodeCount(
    matcher: SemanticsMatcher,
    count: Int,
    timeoutMillis: Long = WAIT_UNTIL_TIMEOUT
) {
    waitUntil(timeoutMillis) {
        onAllNodes(matcher).fetchSemanticsNodes().size == count
    }
}

fun ComposeContentTestRule.waitUntilExists(
    matcher: SemanticsMatcher,
    timeoutMillis: Long = WAIT_UNTIL_TIMEOUT
) = waitUntilNodeCount(matcher, 1, timeoutMillis)

fun ComposeContentTestRule.waitUntilDoesNotExist(
    matcher: SemanticsMatcher,
    timeoutMillis: Long = WAIT_UNTIL_TIMEOUT
) = waitUntilNodeCount(matcher, 0, timeoutMillis)

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Roberto Leinardi