La biblioteca de pruebas androidx.a2ui.compose:compose-ui-testing proporciona APIs de prueba que usan un patrón de controlador idiomático para las bibliotecas de pruebas de Jetpack, como TestNavHostController de Navigation.
A diferencia de los componentes estándar de Jetpack Compose que toman parámetros estáticos y emiten IU, los componentes de A2UI son contextuales. Se basan en A2uiComponentScope para evaluar las vinculaciones de datos dinámicas, enviar acciones salientes al agente, escribir en vinculaciones de datos bidireccionales y expandir plantillas secundarias dinámicas.
Las APIs de prueba simplifican la configuración de las pruebas mientras aprovisionan instancias reales de A2uiMessageProcessor, y ejecutan sus corrutinas vinculadas al entorno de prueba de Compose.
Componentes aislados
Puedes verificar que un componente individual resuelva sus datos, envíe acciones y se renderice correctamente dentro del tema de tu sistema de diseño:
@Test
fun button_resolvesStubChildAndDispatchesAction() = runComposeUiTest {
// 1. Create the test controller
val controller = A2uiTestController(
// Provide a catalog containing the component under test
catalog = CustomComponentCatalog,
// Configure the component under test with concrete properties
initialComponents = listOf(
A2uiComponentPayload(
id = "root",
type = "Button",
properties = mapOf(
"child" to "btn_text",
"variant" to "primary",
"action" to mapOf(
"event" to mapOf(
"name" to "submit_form",
"context" to mapOf("username" to mapOf("path" to "/user/name")),
),
),
),
),
A2uiComponentPayload("btn_text"),
),
// Stub the required child component
componentStubs = listOf(
A2uiComponentStub.withId("btn_text") { _, modifier ->
Text("Submit", modifier = modifier)
},
),
// Provide initial dynamic data
initialData = mapOf("user" to mapOf("name" to "Test User")),
)
// 2. Start background processing and initialize the surface
val surface = controller.start()
// 3. Mount the UI
setContent {
A2uiTestSurface(surface)
}
// 4. Interact using standard Compose UI semantics
onNodeWithText("Submit").performClick()
// 5. Wait for Compose and A2UI background processes to settle
waitForIdle()
controller.waitForIdle()
// 6. Assert outbound actions were correctly evaluated and intercepted
val action = controller.dispatchedActions.single() as A2uiEventAction
assertEquals("submit_form", action.eventName)
assertEquals("Test User", action.context["username"])
}
Estados de la superficie
Puedes probar hosts de superficies, como A2uiSurface, incluidos sus estados y transiciones:
@Test
fun surface_displaysLoading_thenTransitionsToContent() = runComposeUiTest {
// 1. Create an empty controller to simulate a pending network request
val controller = A2uiTestController(
catalog = CustomComponentCatalog,
// Pre-register a stub for the expected root component type
componentStubs = listOf(
A2uiComponentStub.withType("RootLayout") { _, modifier ->
Text("Content Ready", modifier = modifier)
},
),
)
val surface = controller.start()
// 2. Mount the surface UI
setContent {
A2uiSurface(surfaceModel = surface)
}
// 3. Assert the loading placeholder is active
onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)).assertExists()
// 4. Simulate the agent pushing the layout payload over the network
controller.updateComponent(
id = "root",
type = "RootLayout",
properties = emptyMap(),
)
// 5. Wait for the data layer and animation to settle
controller.waitForIdle()
waitForIdle()
// 6. Assert the loading state is gone and content is visible
onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)).assertDoesNotExist()
onNodeWithText("Content Ready").assertIsDisplayed()
}
Vinculación bidireccional
Puedes probar componentes como los campos de texto que escriben en el modelo de datos durante la entrada del usuario y verificar las actualizaciones reactivas cuando el agente muta el modelo de datos:
@Test
fun textField_writesToDataModelAndReactsToAgent() = runComposeUiTest {
val controller = A2uiTestController(
catalog = CustomComponentCatalog,
initialComponents = listOf(
A2uiComponentPayload(
id = "root",
type = "TextField",
properties = mapOf(
"label" to "Username",
"value" to mapOf("path" to "/form/username"),
),
),
),
initialData = mapOf("form" to mapOf("username" to "Initial")),
)
val surface = controller.start()
setContent {
A2uiTestSurface(surface)
}
// 1. User interaction updates the global DataModel locally
onNodeWithText("Initial").performTextReplacement("LocallyTyped")
waitForIdle()
// 2. Assert the component wrote back to the DataModel
assertEquals("LocallyTyped", controller.getData<String>("/form/username"))
// 3. Simulate the agent pushing a data update for the same path
controller.updateData("/form/username", "ServerOverridden")
controller.waitForIdle()
// 4. Assert the component reactively updated the UI
onNodeWithText("ServerOverridden").assertIsDisplayed()
}
Componentes con elementos secundarios basados en plantillas
Puedes probar los componentes diseñados para mostrar colecciones de elementos secundarios definidos con plantillas ChildList de A2UI:
@Test
fun column_rendersDynamicChildTemplates() = runComposeUiTest {
val controller = A2uiTestController(
catalog = CustomComponentCatalog,
initialData = mapOf(
"catalog" to mapOf(
"products" to listOf(
mapOf("title" to "Camera"),
mapOf("title" to "Laptop"),
),
),
),
initialComponents = listOf(
A2uiComponentPayload(
id = "root",
type = "Column",
properties = mapOf(
"children" to mapOf(
"path" to "/catalog/products",
"componentId" to "product_template",
),
),
),
// Bind the initial properties for the dynamically instantiated
// template stub.
A2uiComponentPayload(
id = "product_template",
properties = mapOf("title" to mapOf("path" to "title")),
),
),
componentStubs = listOf(
A2uiComponentStub.withId(id = "product_template") { props, modifier ->
val titleProp = remember { A2uiProperty.dynamicString("title") }
val title = props.bind(titleProp) ?: "Unknown"
Text(text = "Stubbed: $title", modifier = modifier)
},
),
)
val surface = controller.start()
setContent { A2uiTestSurface(surface) }
// Verify the template was instantiated twice with relative data
onNodeWithText("Stubbed: Camera").assertExists()
onNodeWithText("Stubbed: Laptop").assertExists()
// Simulate appending a new item to the data model array
controller.updateData("/catalog/products/-", mapOf("title" to "Tablet"))
controller.waitForIdle()
// Verify the Column dynamically instantiated a new child stub
onNodeWithText("Stubbed: Tablet").assertExists()
}
Respaldo en caso de errores del agente
Puedes verificar que las plataformas y los componentes controlen los errores del agente, como las alucinaciones, de forma correcta:
@Test
fun surface_displaysErrorFallback_onAgentHallucination() = runComposeUiTest {
val controller = A2uiTestController(catalog = CustomComponentCatalog)
val surface = controller.start()
// 1. Mount the surface orchestrator with error boundaries
setContent { A2uiSurface(surfaceModel = surface) }
// 2. Simulate an agent hallucinating a broken component layout
controller.failComponent(
id = "root",
exception = A2uiException.A2uiValidationException(
message = "HallucinatedType",
path = "/components/root"
),
)
controller.waitForIdle()
// 3. Assert the surface displayed the fallback error state
onNodeWithText("Failed to load: HallucinatedType").assertIsDisplayed()
// 4. Assert the core layer dispatched an error to the server
val errorMsg = controller.outboundErrors.single()
assertEquals("VALIDATION_FAILED", errorMsg.code)
}
Renderización progresiva
Puedes probar estados intermedios en los que se cargó un componente principal, pero los componentes secundarios aún están pendientes:
@Test
fun progressiveRendering_parentRendersWhileChildIsPending() = runComposeUiTest {
// 1. Mount the parent, omitting the child instance
val controller = A2uiTestController(
catalog = CustomComponentCatalog,
initialComponents = listOf(
A2uiComponentPayload(
id = "root",
type = "Button",
properties = mapOf(
"child" to "delayed_text_id",
"action" to mapOf("event" to mapOf("name" to "click")),
),
),
),
)
val surface = controller.start()
setContent {
A2uiTestSurface(surface)
}
// 2. Initial state: parent is rendered, child displays loading state
onNodeWithText("Submit").assertDoesNotExist()
onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)).assertExists()
// 3. Simulate arrival of the child component
controller.updateComponent(
id = "delayed_text_id",
type = "Text",
properties = mapOf("text" to "Submit"),
)
controller.waitForIdle()
// 4. Assert that progressive rendering completed
onNode(hasProgressBarRangeInfo(ProgressBarRangeInfo.Indeterminate)).assertDoesNotExist()
onNodeWithText("Submit").assertIsDisplayed()
}
Detalles de la implementación
En las siguientes secciones, se explican las anulaciones de componentes, la validación de esquemas y la sincronización de corrutinas en el framework de pruebas.
La biblioteca de pruebas introduce las siguientes APIs principales:
A2uiTestController: Funciones de constructor de extensiones y la interfaz principal del controlador de pruebas.A2uiComponentStub: Son los stubs y las anulaciones para los componentes secundarios y del catálogo.A2uiTestSurface: Es una utilidad de componibilidad ligera que monta una superficie de prueba.
Invalidaciones de componentes en comparación con la simulación estándar
Para eliminar los frameworks de simulación pesados de terceros, se omiten los componentes secundarios y las dependencias externas con stubs de IU (A2uiComponentStub). A2uiComponentStub.withId intercepta una instancia de componente específica por ID, mientras que A2uiComponentStub.withType anula la renderización de un tipo de catálogo completo.
Validación de esquema con detección rápida de errores
El marco de pruebas aplica el contrato del protocolo A2UI de forma síncrona. Cuando el controlador inicializa o actualiza componentes, ejecuta A2uiCoreSchemaValidator en las cargas útiles proporcionadas. Si se establece una propiedad no válida, como un campo obligatorio faltante o una discrepancia de tipo, la prueba falla de inmediato con un A2uiValidationException.
Sincronización de corrutinas
A2uiTestController.start se conecta al contexto de corrutina de prueba que proporciona runComposeUiTest(). Extrae currentCoroutineContext(), asigna bucles de segundo plano a un Job separado y se cancela automáticamente cuando se completa el bloque de prueba, lo que evita ejecuciones de prueba pendientes. waitForIdle() espera a que finalicen todas las corrutinas en segundo plano pendientes.