Compose Unstyled 2.0 is out! Check the official announcement blog ->
Interface

ComposeUiTest

A test environment that allows you to test and control composables, either in isolation or in applications.

Source set: Common
@ExperimentalTestApi
interface ComposeUiTest : SemanticsNodeInteractionsProvider

A test environment that allows you to test and control composables, either in isolation or in applications. Most of the functionality in this interface provides some form of test synchronization: the test will block until the app or composable is idle, to ensure the tests are deterministic.

For example, if you would perform a click on the center of the screen while a button is animating from left to right over the screen, without synchronization the test would sometimes click when the button is in the middle of the screen (button is clicked), and sometimes when the button is past the middle of the screen (button is not clicked). With synchronization, the app would not be idle until the animation is over, so the test will always click when the button is past the middle of the screen (and not click it). If you actually do want to click the button when it's in the middle of the animation, you can do so by controlling the clock. You'll have to disable automatic advancing, and manually advance the clock by the time necessary to position the button in the middle of the screen.

To test a composable in isolation, use setContent to set the composable in a host. On Android, the host is an Activity. When using runComposeUiTest or any of its platform specific variants, the host will be started for you automatically, unless otherwise specified. To test an application, use the platform specific variant of runComposeUiTest that launches the app.

An instance of ComposeUiTest can be obtained through runComposeUiTest or any of its platform specific variants, the argument to which will have it as the receiver scope.

Properties

density

Source set: Common
val density: Density

Current device screen's density. Note that it is technically possible for a Compose hierarchy to define a different density for a certain subtree. Try to use LayoutInfo.density where possible, which can be obtained from SemanticsNode.layoutInfo.

mainClock

Source set: Common
val mainClock: MainTestClock

Clock that drives frames and recompositions in compose tests.

Functions

runOnUiThread

fun <T> runOnUiThread(action: () -> T): T

Runs the given action on the UI thread.

This method blocks until the action is complete.

runOnIdle

fun <T> runOnIdle(action: () -> T): T

Executes the given action in the same way as runOnUiThread but waits until the app is idle before executing the action. This is the recommended way of doing your assertions on shared variables.

This method blocks until the action is complete.

runWithoutImplicitWait

fun <T> runWithoutImplicitWait(block: () -> T): T

Executes the given block with implicit synchronization suppressed. block should contain read-only assertions, and any actions that mutate state should be performed outside of this block.

To ensure stability of the UI tree while running assertions in this block, make sure to call this on the UI thread, such as with runOnUiThread. If you run this block off the UI thread, state might change in the background and be reflected in the UI while the block is executing. This exposes your test to race conditions, flakiness, and may cause you to read stale or inconsistent state.

Standard node queries (like onNodeWithTag or fetchSemanticsNode) normally trigger a waitForIdle() under the hood. In animation tests that manually step through frames in a loop, these implicit waits impose a severe performance penalty.

This API acts as a performance optimization for motion tests that assert UI state across multiple frames. It is primarily designed for use when mainClock.autoAdvance is set to false and the UI is known to be in a stable state at the specific frame being tested (for example, by calling waitForIdle() before this block).

waitForIdle

fun waitForIdle()

Waits for the UI to become idle. Quiescence is reached when there are no more pending changes (e.g. pending recompositions or a pending draw call) and all IdlingResources are idle.

If auto advancement is enabled on the mainClock, this method will advance the clock to process any pending composition, invalidation and animation. If auto advancement is not enabled, the clock will not be advanced which means that the Compose UI appears to be frozen. This is ideal for testing animations in a deterministic way. This method will always wait for all IdlingResources to become idle.

Note that some processes are driven by the host operating system and will therefore still execute when auto advancement is disabled. For example, Android's measure, layout and draw passes can still happen if required by the View system.

awaitIdle

suspend fun awaitIdle()

Suspends until the UI is idle. Quiescence is reached when there are no more pending changes (e.g. pending recompositions or a pending draw call) and all IdlingResources are idle.

If auto advancement is enabled on the mainClock, this method will advance the clock to process any pending composition, invalidation and animation. If auto advancement is not enabled, the clock will not be advanced which means that the Compose UI appears to be frozen. This is ideal for testing animations in a deterministic way. This method will always wait for all IdlingResources to become idle.

Note that some processes are driven by the host operating system and will therefore still execute when auto advancement is disabled. For example, Android's measure, layout and draw passes can still happen if required by the View system.

waitUntil

fun waitUntil(
        conditionDescription: String? = null,
        timeoutMillis: Long = 1_000,
        condition: () -> Boolean,
    )

Blocks until the given condition is satisfied.

If auto advancement is enabled on the mainClock, this method will actively advance the clock to process any pending composition, invalidation and animation. If auto advancement is not enabled, the clock will not be advanced actively which means that the Compose UI appears to be frozen. It is still valid to use this method in this way, if the condition will be satisfied by something not driven by our clock.

Compared to MainTestClock.advanceTimeUntil, waitUntil sleeps after every iteration to yield to other processes. This gives waitUntil a better integration with the host, but it is less preferred from a performance viewpoint. Therefore, we recommend that you try using MainTestClock.advanceTimeUntil before resorting to waitUntil.

Parameters

conditionDescription An optional human-readable description of condition that will be included in the timeout exception if thrown.
timeoutMillis The time after which this method throws an exception if the given condition is not satisfied. This observes wall clock time, not test clock time.
condition Condition that must be satisfied in order for this method to successfully finish.

setContent

fun setContent(composable: @Composable () -> Unit)

Sets the given composable as the content to be tested. This should be called exactly once per test.

hasPendingWork

fun hasPendingWork(): Boolean

Returns whether the Compose UI has any pending work.

This performs a passive check of the mainClock, snapshot state, and recomposer to determine if there is any pending work. Unlike waitForIdle, calling this method does not advance the clock or drain the main message queue.

This is particularly useful when autoAdvance is disabled, allowing you to inspect the state of the UI while an animation or other work is still active. If autoAdvance is true, the testing framework continuously processes pending work. In that scenario, calling this method acts as a momentary snapshot and will generally return false. It may briefly return true if work is queued but the framework hasn't auto-advanced yet, making the result fleeting and unreliable for driving test logic.

Last updated: