interface A2uiTestController


A controller for orchestrating A2UI component and surface tests.

It provides access to the underlying A2uiSurfaceModel for rendering, tracks outbound actions dispatched by the UI, and exposes synchronous methods to simulate incoming protocol messages (data and component updates) from an agent.

import androidx.a2ui.compose.runtime.A2uiProperty
import androidx.a2ui.compose.ui.A2uiCatalog
import androidx.a2ui.compose.ui.testing.A2uiComponentPayload
import androidx.a2ui.compose.ui.testing.A2uiComponentStub
import androidx.a2ui.compose.ui.testing.A2uiTestController
import androidx.a2ui.compose.ui.testing.A2uiTestSurface
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    val titleProp = A2uiProperty.string("title")
    val rootStub =
        A2uiComponentStub.withId("root") { props, modifier ->
            val title = props[titleProp] ?: "Initial Title"
            BasicText(title, modifier = modifier.testTag("root_tag"))
        }

    // Initialize the controller with catalog and stubs
    val controller =
        A2uiTestController(
            catalog = A2uiCatalog("test_catalog", emptyList()),
            initialComponents = listOf(A2uiComponentPayload("root")),
            componentStubs = listOf(rootStub),
        )

    // Start message processing and initialize a surface model
    val surface = controller.start()

    setContent { A2uiTestSurface(surface = surface) }

    onNodeWithText("Initial Title").assertIsDisplayed()

    // Simulate an agent pushing an incremental property update to the component
    controller.updateComponent(id = "root", properties = mapOf("title" to "Updated Title"))
    // Suspend until the update components message is fully processed
    controller.waitForIdle()

    onNodeWithText("Updated Title").assertIsDisplayed()
}

Summary

Public functions

Unit

Clears the history of dispatchedActions.

Unit

Clears the history of outboundErrors.

Unit

Clears the history of outboundEvents.

Unit
failComponent(id: String, exception: A2uiException)

Simulates an agent hallucination or validation failure for a given component.

Any?

Reads a value synchronously from the underlying test data model.

suspend A2uiSurfaceModel

Starts the controller's background processing loops, initializes the surface with the configured theme, initial data, and initial components, and waits for the reactive state to settle.

Unit
updateComponent(id: String, properties: Map<StringAny?>)

Simulates the agent pushing an incremental property update to an already existing component on the surface, reusing its previously recorded type.

Unit
updateComponent(id: String, type: String, properties: Map<StringAny?>)

Simulates the agent pushing a structural component update to the surface.

Unit
updateData(path: String, value: Any?)

Simulates the agent sending a data model update to the test surface.

suspend Unit

Suspends test execution until the coroutine scheduler processes all pending background data layer tasks and evaluator actions.

Public properties

List<A2uiUserAction>

A sequentially ordered record of all A2uiUserActions dispatched by components during the test.

List<A2uiClientErrorMessage>

A sequentially ordered record of all A2uiClientErrorMessages queued for transmission to the server (agent), useful for asserting self-correction feedback loops.

List<A2uiClientEventMessage>

A sequentially ordered record of all A2uiClientEventMessages queued for transmission to the server (agent).

A2uiSurfaceModel

The A2uiSurfaceModel instance managed by this controller.

Extension functions

inline T?

Reads a value from the underlying data model at the specified JSON pointer path and casts it to the requested type T.

Public functions

clearDispatchedActions

Added in 1.0.0-alpha01
fun clearDispatchedActions(): Unit

Clears the history of dispatchedActions.

clearOutboundErrors

Added in 1.0.0-alpha01
fun clearOutboundErrors(): Unit

Clears the history of outboundErrors.

clearOutboundEvents

Added in 1.0.0-alpha01
fun clearOutboundEvents(): Unit

Clears the history of outboundEvents.

failComponent

Added in 1.0.0-alpha01
fun failComponent(id: String, exception: A2uiException): Unit

Simulates an agent hallucination or validation failure for a given component.

Parameters
id: String

The unique identifier of the component to fail.

exception: A2uiException

The A2uiException simulating the error.

getRawData

Added in 1.0.0-alpha01
fun getRawData(path: String): Any?

Reads a value synchronously from the underlying test data model.

Parameters
path: String

The JSON pointer path to read.

Returns
Any?

The raw data model value, or null if the path does not exist.

start

suspend fun start(): A2uiSurfaceModel

Starts the controller's background processing loops, initializes the surface with the configured theme, initial data, and initial components, and waits for the reactive state to settle.

This is expected to be called in a block passed to runComposeUiTest.

import androidx.a2ui.compose.runtime.A2uiProperty
import androidx.a2ui.compose.ui.A2uiCatalog
import androidx.a2ui.compose.ui.testing.A2uiComponentPayload
import androidx.a2ui.compose.ui.testing.A2uiComponentStub
import androidx.a2ui.compose.ui.testing.A2uiTestController
import androidx.a2ui.compose.ui.testing.A2uiTestSurface
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    val titleProp = A2uiProperty.string("title")
    val rootStub =
        A2uiComponentStub.withId("root") { props, modifier ->
            val title = props[titleProp] ?: "Initial Title"
            BasicText(title, modifier = modifier.testTag("root_tag"))
        }

    // Initialize the controller with catalog and stubs
    val controller =
        A2uiTestController(
            catalog = A2uiCatalog("test_catalog", emptyList()),
            initialComponents = listOf(A2uiComponentPayload("root")),
            componentStubs = listOf(rootStub),
        )

    // Start message processing and initialize a surface model
    val surface = controller.start()

    setContent { A2uiTestSurface(surface = surface) }

    onNodeWithText("Initial Title").assertIsDisplayed()

    // Simulate an agent pushing an incremental property update to the component
    controller.updateComponent(id = "root", properties = mapOf("title" to "Updated Title"))
    // Suspend until the update components message is fully processed
    controller.waitForIdle()

    onNodeWithText("Updated Title").assertIsDisplayed()
}
Returns
A2uiSurfaceModel

The fully initialized A2uiSurfaceModel ready to be mounted in a UI.

updateComponent

fun updateComponent(id: String, properties: Map<StringAny?>): Unit

Simulates the agent pushing an incremental property update to an already existing component on the surface, reusing its previously recorded type.

import androidx.a2ui.compose.runtime.A2uiProperty
import androidx.a2ui.compose.ui.A2uiCatalog
import androidx.a2ui.compose.ui.testing.A2uiComponentPayload
import androidx.a2ui.compose.ui.testing.A2uiComponentStub
import androidx.a2ui.compose.ui.testing.A2uiTestController
import androidx.a2ui.compose.ui.testing.A2uiTestSurface
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    val titleProp = A2uiProperty.string("title")
    val rootStub =
        A2uiComponentStub.withId("root") { props, modifier ->
            val title = props[titleProp] ?: "Initial Title"
            BasicText(title, modifier = modifier.testTag("root_tag"))
        }

    // Initialize the controller with catalog and stubs
    val controller =
        A2uiTestController(
            catalog = A2uiCatalog("test_catalog", emptyList()),
            initialComponents = listOf(A2uiComponentPayload("root")),
            componentStubs = listOf(rootStub),
        )

    // Start message processing and initialize a surface model
    val surface = controller.start()

    setContent { A2uiTestSurface(surface = surface) }

    onNodeWithText("Initial Title").assertIsDisplayed()

    // Simulate an agent pushing an incremental property update to the component
    controller.updateComponent(id = "root", properties = mapOf("title" to "Updated Title"))
    // Suspend until the update components message is fully processed
    controller.waitForIdle()

    onNodeWithText("Updated Title").assertIsDisplayed()
}
Parameters
id: String

The unique identifier of the component.

properties: Map<StringAny?>

The property map representing the component's configuration.

Throws
IllegalStateException

If no type has been recorded for id via initialComponents or a prior call to updateComponent.

updateComponent

fun updateComponent(id: String, type: String, properties: Map<StringAny?>): Unit

Simulates the agent pushing a structural component update to the surface.

import androidx.a2ui.compose.runtime.A2uiProperty
import androidx.a2ui.compose.ui.A2uiCatalog
import androidx.a2ui.compose.ui.testing.A2uiComponentPayload
import androidx.a2ui.compose.ui.testing.A2uiComponentStub
import androidx.a2ui.compose.ui.testing.A2uiTestController
import androidx.a2ui.compose.ui.testing.A2uiTestSurface
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    val titleProp = A2uiProperty.string("title")
    val rootStub =
        A2uiComponentStub.withId("root") { props, modifier ->
            val title = props[titleProp] ?: "Initial Title"
            BasicText(title, modifier = modifier.testTag("root_tag"))
        }

    // Initialize the controller with catalog and stubs
    val controller =
        A2uiTestController(
            catalog = A2uiCatalog("test_catalog", emptyList()),
            initialComponents = listOf(A2uiComponentPayload("root")),
            componentStubs = listOf(rootStub),
        )

    // Start message processing and initialize a surface model
    val surface = controller.start()

    setContent { A2uiTestSurface(surface = surface) }

    onNodeWithText("Initial Title").assertIsDisplayed()

    // Simulate an agent pushing an incremental property update to the component
    controller.updateComponent(id = "root", properties = mapOf("title" to "Updated Title"))
    // Suspend until the update components message is fully processed
    controller.waitForIdle()

    onNodeWithText("Updated Title").assertIsDisplayed()
}
Parameters
id: String

The unique identifier of the component.

type: String

The string type identifier of the component.

properties: Map<StringAny?>

The property map representing the component's configuration.

updateData

Added in 1.0.0-alpha01
fun updateData(path: String, value: Any?): Unit

Simulates the agent sending a data model update to the test surface.

import androidx.a2ui.compose.ui.A2uiCatalog
import androidx.a2ui.compose.ui.testing.A2uiTestController
import androidx.a2ui.compose.ui.testing.getData
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    val controller =
        A2uiTestController(
            catalog = A2uiCatalog("test_catalog", emptyList()),
            initialData = mapOf("user" to mapOf("name" to "Alice")),
        )
    controller.start()

    // Read typed data from the controller's underlying data model
    val initialName: String? = controller.getData("/user/name")
    assertThat(initialName).isEqualTo("Alice")

    // Simulate the agent sending a data layer update
    controller.updateData("/user/name", "Bob")
    controller.waitForIdle()

    val updatedName: String? = controller.getData("/user/name")
    assertThat(updatedName).isEqualTo("Bob")
}
Parameters
path: String

The JSON pointer path to update.

value: Any?

The new value to place at the specified path.

waitForIdle

Added in 1.0.0-alpha01
suspend fun waitForIdle(): Unit

Suspends test execution until the coroutine scheduler processes all pending background data layer tasks and evaluator actions.

Note: This settles the A2UI data model and component registry layers. Typically, calling ComposeUiTest.waitForIdle() afterward is still needed to allow the Compose UI tree to recompose and reflect the new states.

This is expected to be called in a block passed to runComposeUiTest.

import androidx.a2ui.compose.runtime.A2uiProperty
import androidx.a2ui.compose.ui.A2uiCatalog
import androidx.a2ui.compose.ui.testing.A2uiComponentPayload
import androidx.a2ui.compose.ui.testing.A2uiComponentStub
import androidx.a2ui.compose.ui.testing.A2uiTestController
import androidx.a2ui.compose.ui.testing.A2uiTestSurface
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    val titleProp = A2uiProperty.string("title")
    val rootStub =
        A2uiComponentStub.withId("root") { props, modifier ->
            val title = props[titleProp] ?: "Initial Title"
            BasicText(title, modifier = modifier.testTag("root_tag"))
        }

    // Initialize the controller with catalog and stubs
    val controller =
        A2uiTestController(
            catalog = A2uiCatalog("test_catalog", emptyList()),
            initialComponents = listOf(A2uiComponentPayload("root")),
            componentStubs = listOf(rootStub),
        )

    // Start message processing and initialize a surface model
    val surface = controller.start()

    setContent { A2uiTestSurface(surface = surface) }

    onNodeWithText("Initial Title").assertIsDisplayed()

    // Simulate an agent pushing an incremental property update to the component
    controller.updateComponent(id = "root", properties = mapOf("title" to "Updated Title"))
    // Suspend until the update components message is fully processed
    controller.waitForIdle()

    onNodeWithText("Updated Title").assertIsDisplayed()
}

Public properties

dispatchedActions

Added in 1.0.0-alpha01
val dispatchedActionsList<A2uiUserAction>

A sequentially ordered record of all A2uiUserActions dispatched by components during the test. This includes both local client-side function calls and server-bound events.

To inspect only the events queued for network transmission to the agent, use outboundEvents. Use clearDispatchedActions to clear this record.

import androidx.a2ui.compose.ui.A2uiCatalog
import androidx.a2ui.compose.ui.testing.A2uiComponentPayload
import androidx.a2ui.compose.ui.testing.A2uiComponentStub
import androidx.a2ui.compose.ui.testing.A2uiTestController
import androidx.a2ui.compose.ui.testing.A2uiTestSurface
import androidx.compose.foundation.clickable
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    val buttonStub =
        A2uiComponentStub.withId("button") { _, modifier ->
            BasicText(
                "Submit",
                modifier.testTag("submit_button").clickable {
                    dispatchAction(
                        mapOf(
                            "event" to
                                mapOf("name" to "on_submit", "context" to emptyMap<String, Any?>())
                        )
                    )
                },
            )
        }

    val controller =
        A2uiTestController(
            catalog = A2uiCatalog("test_catalog", emptyList()),
            initialComponents = listOf(A2uiComponentPayload("button")),
            componentStubs = listOf(buttonStub),
        )
    val surface = controller.start()

    setContent { A2uiTestSurface(surface = surface) }

    // Perform a click on the rendered button
    onNodeWithTag("submit_button").performClick()
    waitForIdle()
    controller.waitForIdle()

    // Assert that the dispatched event was recorded by the controller
    assertThat(controller.dispatchedActions).hasSize(1)
    assertThat(controller.outboundEvents).hasSize(1)
    assertThat(controller.outboundEvents.first().type).isEqualTo("on_submit")
}

outboundErrors

Added in 1.0.0-alpha01
val outboundErrorsList<A2uiClientErrorMessage>

A sequentially ordered record of all A2uiClientErrorMessages queued for transmission to the server (agent), useful for asserting self-correction feedback loops.

Use clearOutboundErrors to clear this record.

outboundEvents

Added in 1.0.0-alpha01
val outboundEventsList<A2uiClientEventMessage>

A sequentially ordered record of all A2uiClientEventMessages queued for transmission to the server (agent).

Local function calls are not included in this list. Use clearOutboundEvents to clear this record.

import androidx.a2ui.compose.ui.A2uiCatalog
import androidx.a2ui.compose.ui.testing.A2uiComponentPayload
import androidx.a2ui.compose.ui.testing.A2uiComponentStub
import androidx.a2ui.compose.ui.testing.A2uiTestController
import androidx.a2ui.compose.ui.testing.A2uiTestSurface
import androidx.compose.foundation.clickable
import androidx.compose.foundation.text.BasicText
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    val buttonStub =
        A2uiComponentStub.withId("button") { _, modifier ->
            BasicText(
                "Submit",
                modifier.testTag("submit_button").clickable {
                    dispatchAction(
                        mapOf(
                            "event" to
                                mapOf("name" to "on_submit", "context" to emptyMap<String, Any?>())
                        )
                    )
                },
            )
        }

    val controller =
        A2uiTestController(
            catalog = A2uiCatalog("test_catalog", emptyList()),
            initialComponents = listOf(A2uiComponentPayload("button")),
            componentStubs = listOf(buttonStub),
        )
    val surface = controller.start()

    setContent { A2uiTestSurface(surface = surface) }

    // Perform a click on the rendered button
    onNodeWithTag("submit_button").performClick()
    waitForIdle()
    controller.waitForIdle()

    // Assert that the dispatched event was recorded by the controller
    assertThat(controller.dispatchedActions).hasSize(1)
    assertThat(controller.outboundEvents).hasSize(1)
    assertThat(controller.outboundEvents.first().type).isEqualTo("on_submit")
}

surface

Added in 1.0.0-alpha01
val surfaceA2uiSurfaceModel

The A2uiSurfaceModel instance managed by this controller.

Throws
IllegalStateException

If accessed before calling start.

Extension functions

A2uiTestController.getData

inline fun <T : Any?> A2uiTestController.getData(path: String): T?

Reads a value from the underlying data model at the specified JSON pointer path and casts it to the requested type T.

import androidx.a2ui.compose.ui.A2uiCatalog
import androidx.a2ui.compose.ui.testing.A2uiTestController
import androidx.a2ui.compose.ui.testing.getData
import androidx.compose.ui.test.v2.runComposeUiTest

runComposeUiTest {
    val controller =
        A2uiTestController(
            catalog = A2uiCatalog("test_catalog", emptyList()),
            initialData = mapOf("user" to mapOf("name" to "Alice")),
        )
    controller.start()

    // Read typed data from the controller's underlying data model
    val initialName: String? = controller.getData("/user/name")
    assertThat(initialName).isEqualTo("Alice")

    // Simulate the agent sending a data layer update
    controller.updateData("/user/name", "Bob")
    controller.waitForIdle()

    val updatedName: String? = controller.getData("/user/name")
    assertThat(updatedName).isEqualTo("Bob")
}
Parameters
path: String

The JSON pointer path to read (e.g., "/user/name").

Returns
T?

The data model value cast to T, or null if the path does not exist.

Throws
ClassCastException

if the value at path cannot be cast to T.