Pitry
Pitry

Reputation: 331

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


Upvotes: 17

Views: 11811

Answers (5)

Myralyn Garcia
Myralyn Garcia

Reputation: 21

After your performClick() add WaitforIdle()

Upvotes: 0

Maksim Dmitriev
Maksim Dmitriev

Reputation: 6209

I modified per_jansson's reply.


fun ComposeContentTestRule.waitUntilTimeout(
    timeoutMillis: Long
) {
    AsyncTimer.start(timeoutMillis)
    this.waitUntil(
        condition = { AsyncTimer.expired },
        timeoutMillis = timeoutMillis + 1000
    )
}

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

class TimerTaskImpl(private val runnable: Runnable) : TimerTask() {

    override fun run() {
        runnable.run()
    }
}

TimerTask is an abstract class, and when I copied and pasted per_jansson's code, it didn't work for me

Upvotes: 1

Jose Alcérreca
Jose Alcérreca

Reputation: 2048

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

Upvotes: 22

per_jansson
per_jansson

Reputation: 2189

Based on Pitry's answer I created this extension function:

fun ComposeContentTestRule.waitUntilTimeout(
    timeoutMillis: Long
) {
    AsyncTimer.start(timeoutMillis)
    this.waitUntil(
        condition = { AsyncTimer.expired },
        timeoutMillis = timeoutMillis + 1000
    )
}

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

Usage in compose test

composeTestRule.waitUntilTimeout(2000L)

Upvotes: 4

Pitry
Pitry

Reputation: 331

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.

Upvotes: 6

Related Questions