'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 |