A biblioteca de testes androidx.a2ui.compose:compose-ui-testing fornece APIs de teste
que usam um padrão de controlador idiomático para bibliotecas de teste do Jetpack, como
o TestNavHostController da navegação.
Ao contrário dos componentes padrão do Jetpack Compose, que usam parâmetros estáticos e emitem
interfaces, os componentes da A2UI são contextuais. Eles dependem do A2uiComponentScope para
avaliar vinculações de dados dinâmicas, enviar ações de saída ao agente, gravar
de volta em vinculações de dados bidirecionais e inflar modelos filhos dinâmicos.
As APIs de teste simplificam a configuração do teste ao provisionar instâncias A2uiMessageProcessor reais, executando as corrotinas vinculadas ao ambiente de teste do Compose.
Componentes isolados
Você pode verificar se um componente individual resolve os dados, envia ações e renderiza corretamente no tema do sistema de design:
@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 superfície
É possível testar hosts de superfície, como A2uiSurface, incluindo estados e transições:
@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()
}
Vinculação bidirecional
É possível testar componentes como campos de texto que gravam no modelo de dados durante a entrada do usuário e verificar atualizações reativas quando o agente muda o modelo de dados:
@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 com filhos com modelos
É possível testar componentes projetados para mostrar coleções de filhos definidos
usando modelos ChildList do 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()
}
Substituições de erros para erros do agente
É possível verificar se as plataformas e os componentes processam erros do agente, como alucinações, de maneira adequada:
@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)
}
Renderização progressiva
É possível testar estados intermediários em que um componente pai foi carregado, mas os componentes filhos ainda estão pendentes:
@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()
}
Detalhes de implementação
As seções a seguir explicam substituições de componentes, validação de esquema e sincronização de corrotinas no framework de teste.
A biblioteca de testes apresenta as seguintes APIs principais:
A2uiTestController: funções de construtor de extensão e interface principal do controlador de teste.A2uiComponentStub: stubs e substituições para componentes filhos e de catálogo.A2uiTestSurface: um utilitário combinável leve que monta uma superfície de teste.
Substituições de componentes x simulação padrão
Para eliminar frameworks de simulação pesados de terceiros, os componentes filhos e as dependências externas
são ignorados usando stubs de interface (A2uiComponentStub).
A2uiComponentStub.withId intercepta uma instância de componente específica por ID, enquanto
A2uiComponentStub.withType substitui a renderização de um tipo de catálogo inteiro.
Validação de esquema com falha rápida
A estrutura de teste aplica o contrato do protocolo A2UI de forma síncrona. Quando o controlador inicializa ou atualiza componentes, ele executa A2uiCoreSchemaValidator em relação aos payloads fornecidos. Se uma propriedade inválida for definida, como um campo obrigatório ausente ou uma incompatibilidade de tipo, o teste vai falhar imediatamente com um A2uiValidationException.
Sincronização de corrotinas
O A2uiTestController.start se conecta ao contexto da corrotina de teste
fornecido por runComposeUiTest(). Ele extrai currentCoroutineContext(), mapeia loops em segundo plano para um Job independente e se cancela automaticamente quando o bloco de teste é concluído, evitando execuções de teste pendentes. waitForIdle()
aguarda a conclusão de todas as corrotinas em segundo plano pendentes.