ComposeUiTest

Known direct subclasses
AndroidComposeUiTest

Variant of ComposeUiTest for when you want to have access the current activity of type A.


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.

Summary

Public functions

suspend T
<T : Any?> awaitAndRunWhenIdle(action: () -> T)

Executes the given action on the UI thread.

Cmn
android
suspend Unit

Suspends until the UI is idle.

Cmn
android
Boolean

Returns whether the Compose UI has any pending work.

Cmn
android
Unit

Registers an IdlingResource in this test.

android
T
<T : Any?> runOnIdle(action: () -> T)

Executes the given action in the same way as runOnUiThread but waits until the app is idle before executing the action.

Cmn
android
T
<T : Any?> runOnUiThread(action: () -> T)

Runs the given action on the UI thread.

Cmn
android
T
<T : Any?> runWhenIdle(action: () -> T)

Executes the given action on the UI thread.

Cmn
android
Unit
setContent(composable: @Composable () -> Unit)

Sets the given composable as the content to be tested.

Cmn
android
Unit

Unregisters an IdlingResource from this test.

android
Unit

Waits for the UI to become idle.

Cmn
android
Unit
waitUntil(
    conditionDescription: String?,
    timeoutMillis: Long,
    condition: () -> Boolean
)

Blocks until the given condition is satisfied.

Cmn
android

Public properties

Density

Current device screen's density.

Cmn
android
MainTestClock

Clock that drives frames and recompositions in compose tests.

Cmn
android

Extension functions

SemanticsNodeInteractionsProvider

Scopes the Compose interaction to the View hierarchy matched by the provided Espresso ViewInteraction.

android
Unit

Disables accessibility checks.

android
Unit

Enables accessibility checks using an accessibilityValidator that will be run before every action that is expected to change the UI.

android
Unit

Blocks until at least one node matches the given matcher.

Cmn
Unit

Blocks until no nodes match the given matcher.

Cmn
Unit

Blocks until exactly one node matches the given matcher.

Cmn
Unit
@ExperimentalTestApi
ComposeUiTest.waitUntilNodeCount(
    matcher: SemanticsMatcher,
    count: Int,
    timeoutMillis: Long
)

Blocks until the number of nodes matching the given matcher is equal to the given count.

Cmn

Inherited functions

From androidx.compose.ui.test.SemanticsNodeInteractionsProvider
SemanticsNodeInteractionCollection
onAllNodes(matcher: SemanticsMatcher, useUnmergedTree: Boolean)

Finds all semantics nodes that match the given condition.

android
SemanticsNodeInteractionCollection
onAllNodes(matcher: SemanticsMatcher, useUnmergedTree: Boolean)

Finds all semantics nodes that match the given condition.

Cmn
SemanticsNodeInteraction
onNode(matcher: SemanticsMatcher, useUnmergedTree: Boolean)

Finds a semantics node that matches the given condition.

android
SemanticsNodeInteraction
onNode(matcher: SemanticsMatcher, useUnmergedTree: Boolean)

Finds a semantics node that matches the given condition.

Cmn

Public functions

awaitAndRunWhenIdle

suspend fun <T : Any?> awaitAndRunWhenIdle(action: () -> T): T

Executes the given action on the UI thread. It first suspends until the app is idle before executing the action.

This method skips unnecessary synchronization inside the provided action block. Standard node queries (like fetchSemanticsNode()) normally trigger a redundant waitForIdle() under the hood, which can impose a significant performance toll in tests that manually step through frames. Bypassing this implicit wait makes this the highly optimized and preferred API for state inspection.

This block is intended primarily for inspecting the UI state, making assertions, or capturing properties without the performance overhead of repeated synchronization. You should avoid mutating UI state (e.g., performing clicks, text input, or advancing the clock) inside this block. If your test requires driving the UI to a new state, perform those interactions outside of this block to ensure the framework can properly synchronize.

import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    setContent { ExpandingCard() }

    mainClock.autoAdvance = false

    // Trigger the interaction that starts the animation
    onNodeWithTag("ExpandButton").performClick()

    // Manually advance the clock to the halfway point (250ms)
    mainClock.advanceTimeBy(250L)

    // Inspect the intermediate UI state.
    // The framework waits for the layout pass triggered by advanceTimeBy()
    // to settle, then executes these checks.
    awaitAndRunWhenIdle {
        val midAnimationBounds = onNodeWithTag("CardContent").fetchSemanticsNode().boundsInRoot
        assertTrue(
            "Card height should be greater than collapsed state",
            midAnimationBounds.height > COLLAPSED_HEIGHT,
        )
        onNodeWithTag("ExpandedTextDetails").assertExists()
    }
}

awaitIdle

suspend fun awaitIdle(): Unit

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.

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.

import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    setContent { ExpandingCard() }

    mainClock.autoAdvance = false

    // Trigger the animation
    onNodeWithTag("ExpandButton").performClick()

    while (hasPendingWork()) {
        // Advance the clock by exactly one frame
        mainClock.advanceTimeByFrame()

        awaitAndRunWhenIdle {
            // Make intermediate assertions (e.g., check bounds, alpha, or translation)
        }
    }
    awaitAndRunWhenIdle {
        // Assert the final state.
    }
}

registerIdlingResource

fun registerIdlingResource(idlingResource: IdlingResource): Unit

Registers an IdlingResource in this test.

runOnIdle

fun <T : Any?> 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. T

Prefer using runWhenIdle as it optimizes performance by suppressing redundant synchronization during node queries.

This method blocks until the action is complete.

runOnUiThread

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

Runs the given action on the UI thread.

This method blocks until the action is complete.

runWhenIdle

fun <T : Any?> runWhenIdle(action: () -> T): T

Executes the given action on the UI thread. It first waits until the app is idle before executing the action.

This method skips unnecessary synchronization inside the provided action block. Standard node queries (like fetchSemanticsNode()) normally trigger a redundant waitForIdle() under the hood, which can impose a significant performance toll in tests that manually step through frames. Bypassing this implicit wait makes this the highly optimized and preferred API for state inspection.

This block is intended primarily for inspecting the UI state, making assertions, or capturing properties without the performance overhead of repeated synchronization. You should avoid mutating UI state (e.g., performing clicks, text input, or advancing the clock) inside this block. If your test requires driving the UI to a new state, perform those interactions outside of this block to ensure the framework can properly synchronize.

import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    // ExpandingCard has a 500ms expansion animation
    setContent { ExpandingCard() }

    mainClock.autoAdvance = false

    // Trigger the interaction that starts the animation
    onNodeWithTag("ExpandButton").performClick()

    // Manually advance the clock to the halfway point (250ms)
    mainClock.advanceTimeBy(250L)

    // Inspect the intermediate UI state.
    // The framework waits for the layout pass triggered by advanceTimeBy()
    // to settle, then executes these checks.
    runWhenIdle {
        val midAnimationBounds = onNodeWithTag("CardContent").fetchSemanticsNode().boundsInRoot
        assertTrue(
            "Card height should be greater than collapsed state",
            midAnimationBounds.height > COLLAPSED_HEIGHT,
        )
        onNodeWithTag("ExpandedTextDetails").assertExists()
    }
}

setContent

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

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

Throws
IllegalStateException

if called more than once per test, or if the implementation doesn't have access to a host to set content in.

unregisterIdlingResource

fun unregisterIdlingResource(idlingResource: IdlingResource): Unit

Unregisters an IdlingResource from this test.

waitForIdle

fun waitForIdle(): Unit

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.

waitUntil

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

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: String? = null

An optional human-readable description of condition that will be included in the timeout exception if thrown.

timeoutMillis: Long

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

Condition that must be satisfied in order for this method to successfully finish.

Throws
androidx.compose.ui.test.ComposeTimeoutException

If the condition is not satisfied after timeoutMillis (in wall clock time).

Public properties

density

val densityDensity

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

val mainClockMainTestClock

Clock that drives frames and recompositions in compose tests.

Extension functions

ComposeUiTest.onRootWithViewInteraction

@ExperimentalTestApi
fun ComposeUiTest.onRootWithViewInteraction(interaction: ViewInteraction): SemanticsNodeInteractionsProvider

Scopes the Compose interaction to the View hierarchy matched by the provided Espresso ViewInteraction.

It resolves the View from the Espresso interaction, locates all Compose roots within that view hierarchy, and creates a new, scoped SemanticsNodeInteractionsProvider.

import androidx.compose.ui.test.junit4.onRootWithViewInteraction
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.withId

// Select the "Header" View container
val headerInteraction = onView(withId(header_id))

// Scope the Compose interaction to only the Header
composeTestRule
    .onRootWithViewInteraction(headerInteraction)
    .onNodeWithContentDescription("Settings")
    .performClick()
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.onRootWithViewInteraction
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText

// Select the specific View row containing "Item #5"
val specificRowInteraction =
    onView(allOf(withId(recycler_item_root_id), hasDescendant(withText("Item #5"))))

// Scope interaction to that specific row View
composeTestRule
    .onRootWithViewInteraction(specificRowInteraction)
    .onNodeWithTag("fav_icon")
    .assertIsDisplayed()
    .performClick()
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.junit4.onRootWithViewInteraction
import androidx.compose.ui.test.onNodeWithText
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.withId

// Select the container for the Detail Fragment
val detailContainerInteraction = onView(withId(detail_fragment_container_id))

// Assert that the submit button exists/is enabled only in the detail fragment
composeTestRule
    .onRootWithViewInteraction(detailContainerInteraction)
    .onNodeWithText("Submit")
    .assertIsEnabled()

ComposeUiTest.disableAccessibilityChecks

@ExperimentalTestApi
@RequiresApi(value = 34)
fun ComposeUiTest.disableAccessibilityChecks(): Unit

Disables accessibility checks.

import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.accessibility.disableAccessibilityChecks
import androidx.compose.ui.test.accessibility.enableAccessibilityChecks
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import androidx.compose.ui.test.tryPerformAccessibilityChecks
import androidx.compose.ui.test.v2.runComposeUiTest
import androidx.test.espresso.accessibility.AccessibilityChecks

@Test
@OptIn(ExperimentalTestApi::class)
fun testWithAccessibilityChecks() = runComposeUiTest {
    // Enable accessibility checks with default configuration:
    enableAccessibilityChecks()

    // When enabled, accessibility checks run automatically when performing an action:
    onNodeWithText("Submit").performClick()

    // You can also manually run accessibility checks:
    onRoot().tryPerformAccessibilityChecks()

    // When disabling accessibility checks..
    disableAccessibilityChecks()

    // .. they no longer run when performing an action:
    onNodeWithTag("list").performScrollToIndex(15)
}

ComposeUiTest.enableAccessibilityChecks

@ExperimentalTestApi
@RequiresApi(value = 34)
fun ComposeUiTest.enableAccessibilityChecks(
    accessibilityValidator: AccessibilityValidator = AccessibilityValidator().setRunChecksFromRootView(true)
): Unit

Enables accessibility checks using an accessibilityValidator that will be run before every action that is expected to change the UI.

This requires API 34+ (Android U), and currently does not work on Robolectric.

import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.accessibility.disableAccessibilityChecks
import androidx.compose.ui.test.accessibility.enableAccessibilityChecks
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import androidx.compose.ui.test.tryPerformAccessibilityChecks
import androidx.compose.ui.test.v2.runComposeUiTest
import androidx.test.espresso.accessibility.AccessibilityChecks

@Test
@OptIn(ExperimentalTestApi::class)
fun testWithAccessibilityChecks() = runComposeUiTest {
    // Enable accessibility checks with default configuration:
    enableAccessibilityChecks()

    // When enabled, accessibility checks run automatically when performing an action:
    onNodeWithText("Submit").performClick()

    // You can also manually run accessibility checks:
    onRoot().tryPerformAccessibilityChecks()

    // When disabling accessibility checks..
    disableAccessibilityChecks()

    // .. they no longer run when performing an action:
    onNodeWithTag("list").performScrollToIndex(15)
}
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.accessibility.enableAccessibilityChecks
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.espresso.accessibility.AccessibilityChecks

@Test
@OptIn(ExperimentalTestApi::class)
fun testWithAccessibilityChecks() =
    runAndroidComposeUiTest<ComponentActivity> {
        // Configure your own AccessibilityValidator
        val accessibilityValidator =
            AccessibilityValidator().also {
                it.setThrowExceptionFor(AccessibilityCheckResultType.ERROR)
            }

        // Enable accessibility checks with your own AccessibilityValidator:
        enableAccessibilityChecks(accessibilityValidator)
    }

If you have a hybrid application with both Compose and Views, and you use both Compose Test and Espresso, then you should set up accessibility checks in both frameworks and share the configuration in the following way:

import androidx.activity.ComponentActivity
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.accessibility.enableAccessibilityChecks
import androidx.compose.ui.test.v2.runAndroidComposeUiTest
import androidx.test.espresso.accessibility.AccessibilityChecks

@Test
@OptIn(ExperimentalTestApi::class)
fun testWithAccessibilityChecks() =
    runAndroidComposeUiTest<ComponentActivity> {
        // Enable accessibility checks in both Espresso and Compose, and share the configuration
        val accessibilityValidator = AccessibilityChecks.enable()
        enableAccessibilityChecks(accessibilityValidator)
    }

ComposeUiTest.waitUntilAtLeastOneExists

@ExperimentalTestApi
fun ComposeUiTest.waitUntilAtLeastOneExists(
    matcher: SemanticsMatcher,
    timeoutMillis: Long
): Unit

Blocks until at least one node matches the given matcher.

Parameters
matcher: SemanticsMatcher

The matcher that will be used to filter nodes.

timeoutMillis: Long

The time after which this method throws an exception if no nodes match the given matcher. This observes wall clock time, not frame time.

Throws
androidx.compose.ui.test.ComposeTimeoutException

If no nodes match the given matcher after timeoutMillis (in wall clock time).

See also
waitUntil

ComposeUiTest.waitUntilDoesNotExist

@ExperimentalTestApi
fun ComposeUiTest.waitUntilDoesNotExist(
    matcher: SemanticsMatcher,
    timeoutMillis: Long
): Unit

Blocks until no nodes match the given matcher.

Parameters
matcher: SemanticsMatcher

The matcher that will be used to filter nodes.

timeoutMillis: Long

The time after which this method throws an exception if any nodes match the given matcher. This observes wall clock time, not frame time.

Throws
androidx.compose.ui.test.ComposeTimeoutException

If any nodes match the given matcher after timeoutMillis (in wall clock time).

See also
waitUntil

ComposeUiTest.waitUntilExactlyOneExists

@ExperimentalTestApi
fun ComposeUiTest.waitUntilExactlyOneExists(
    matcher: SemanticsMatcher,
    timeoutMillis: Long
): Unit

Blocks until exactly one node matches the given matcher.

Parameters
matcher: SemanticsMatcher

The matcher that will be used to filter nodes.

timeoutMillis: Long

The time after which this method throws an exception if exactly one node does not match the given matcher. This observes wall clock time, not frame time.

Throws
androidx.compose.ui.test.ComposeTimeoutException

If exactly one node does not match the given matcher after timeoutMillis (in wall clock time).

See also
waitUntil

ComposeUiTest.waitUntilNodeCount

@ExperimentalTestApi
fun ComposeUiTest.waitUntilNodeCount(
    matcher: SemanticsMatcher,
    count: Int,
    timeoutMillis: Long
): Unit

Blocks until the number of nodes matching the given matcher is equal to the given count.

Parameters
matcher: SemanticsMatcher

The matcher that will be used to filter nodes.

count: Int

The number of nodes that are expected to be matched.

timeoutMillis: Long

The time after which this method throws an exception if the number of nodes that match the matcher is not count. This observes wall clock time, not frame time.

Throws
androidx.compose.ui.test.ComposeTimeoutException

If the number of nodes that match the matcher is not count after timeoutMillis (in wall clock time).

See also
waitUntil