如何告诉 composeTestRule 等待 navhost 转换?

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

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

我设法正确交互并测试了导航图显示的第一个屏幕,但是,当我导航到新目的地时,测试失败了,因为它没有等待 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()
        }
    }
}

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

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

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

this very informative blog article中所建议,waitUntil可用于等待显示具有正确标签的节点:

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

或者,添加一些糖后:

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