如何告诉 composeTestRule 等待 navhost 转换?

Rob*_*rdi 5 android-espresso android-jetpack-navigation android-jetpack-compose

我正在尝试为完全用 Compose 编写的 Android 应用程序编写集成测试,该应用程序具有单个 Activity 并使用 Compose 导航来更改屏幕内容。

我设法正确交互并测试导航图显示的第一个屏幕,但是,一旦我导航到新目的地,测试就会失败,因为它不等待 NavHost 加载新内容。

@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()
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我确信这是一个计时问题,因为如果我在Thread.sleep(500)之前添加onNodeWithTag(SecondScreen.USERNAME_TEXT_FIELD).assertIsDisplayed(),测试就会成功。但我想Thread.sleep()在我的代码中避免使用 s 。

有没有更好的方法来告诉composeTestRuleNavHost 在执行之前等待 NavHost 加载新内容assertIsDisplayed()

PS 我知道单独测试可组合项会更好,但我真的想使用 Espresso 模拟应用程序上的用户输入,而不仅仅是测试可组合项行为。

Rob*_*rdi 8

正如这篇信息丰富的博客文章中所建议的,waitUntil可用于等待显示具有正确标签的节点:

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

或者,添加一些糖后:

@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)

Run Code Online (Sandbox Code Playgroud)