Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to wait for next @Composable function in jetpack compose test?

Suppose I have 3 @Composable functions: Start, Loading, Result.

In the test, I call the Start function, click the Begin button on it, and the Loading function is called.

The Loading function displays the loading procedure, takes some time, and then calls the Result function.

The Result function renders a field with the text OK.

How to wait in the test for the Result or few seconds function to be drawn and check that the text is rendered OK?

composeTestRule
    .onNodeWithText("Begin")
    .performClick()

// Here you have to wait ...

composeTestRule
    .onNodeWithText("OK")
    .assertIsDisplayed() 


like image 825
Pitry Avatar asked Dec 03 '25 09:12

Pitry


2 Answers

Edit: there are new waitUntil functions:

fun waitUntilAtLeastOneExists(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilDoesNotExist(matcher: SemanticsMatcher, timeout: Long = 1000L)

fun waitUntilExactlyOneExists(matcher: SemanticsMatcher,  timeout: Long = 1000L)

fun waitUntilNodeCount(matcher: SemanticsMatcher, count: Int, timeout: Long = 1000L)

You can use the waitUntil function, as suggested in the comments:

composeTestRule.waitUntil {
    composeTestRule
        .onAllNodesWithText("OK")
        .fetchSemanticsNodes().size == 1
}

There's a request to improve this API but in the meantime you can get the helpers from this blog post and use it like so:

composeTestRule.waitUntilExists(hasText("OK"))
like image 155
Jose Alcérreca Avatar answered Dec 05 '25 07:12

Jose Alcérreca


So the options are:

  1. It is possible to write to the global variable which function was last called. The disadvantage is that you will need to register in each function.
  2. Subscribe to the state of the screen through the viewmodel and track when it comes. The disadvantage is that you will need to pull the viewmodel into the test and know the code. The plus is that the test is quickly executed and does not get stuck, as is the case with a timer.
  3. I made this choice. I wrote a function for starting an asynchronous timer, that is, the application is running, the test waits, and after the timer ends, it continues checking in the test. The disadvantage is that you set a timer with a margin of time to complete the operation and the test takes a long time to idle. The advantage is that you don't have to dig into the source code.

Implemented the function like this.

fun asyncTimer (delay: Long = 1000) {
    AsyncTimer.start (delay)
    composeTestRule.waitUntil (
        condition = {AsyncTimer.expired},
        timeoutMillis = delay + 1000
    )
}

object AsyncTimer {
    var expired = false
    fun start(delay: Long = 1000){
        expired = false
        Timer().schedule(delay) {
            expired = true
        }
    }
}

Then I created a base class for the test and starting to write a new test, I inherit and I have the necessary ready-made functionality for tests.

like image 36
Pitry Avatar answered Dec 05 '25 08:12

Pitry



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!