La libreria di test androidx.a2ui.compose:compose-ui-testing fornisce API di test che utilizzano un pattern di controller idiomatico per le librerie di test Jetpack, come TestNavHostController di Navigation.
A differenza dei componenti Jetpack Compose standard che accettano parametri statici ed emettono
UI, i componenti A2UI sono contestuali. Si basano su A2uiComponentScope per
valutare le associazioni di dati dinamiche, inviare azioni in uscita all'agente, scrivere
nelle associazioni di dati bidirezionali e gonfiare i modelli secondari dinamici.
Le API di test semplificano la configurazione dei test durante il provisioning di istanze A2uiMessageProcessor reali, eseguendo le relative coroutine associate all'ambiente di test Compose.
Componenti isolati
Puoi verificare che un singolo componente risolva i dati, invii azioni e venga visualizzato correttamente all'interno del tema del sistema di progettazione:
@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"])
}
Stati delle superfici
Puoi testare gli host di superficie come A2uiSurface, inclusi i relativi stati
e transizioni:
@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()
}
Associazione bidirezionale
Puoi testare componenti come i campi di testo che scrivono nuovamente nel modello di dati durante l'input dell'utente e verificare gli aggiornamenti reattivi quando l'agente modifica il modello di dati:
@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()
}
Componenti con elementi secondari basati su modelli
Puoi testare i componenti progettati per visualizzare raccolte di bambini definite
utilizzando i modelli ChildList 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()
}
Errori di fallback per gli errori dell'agente
Puoi verificare che le superfici e i componenti gestiscano gli errori dell'agente, ad esempio le allucinazioni, in modo controllato:
@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)
}
Rendering progressivo
Puoi testare gli stati intermedi in cui un componente principale è stato caricato, ma i componenti secondari sono ancora in attesa:
@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()
}
Dettagli di implementazione
Le sezioni seguenti spiegano gli override dei componenti, la convalida dello schema e la sincronizzazione delle coroutine nel framework di test.
La libreria di test introduce le seguenti API principali:
A2uiTestController: Funzioni del costruttore di estensioni e interfaccia principale del controller di test.A2uiComponentStub: Stub e override per i componenti secondari e del catalogo.A2uiTestSurface: un'utilità composable leggera che monta una superficie di test.
Override dei componenti e simulazione standard
Per eliminare i framework di simulazione di terze parti pesanti, i componenti secondari e le dipendenze esterne vengono ignorati utilizzando stub dell'interfaccia utente (A2uiComponentStub).
A2uiComponentStub.withId intercetta un'istanza di componente specifica per ID, mentre
A2uiComponentStub.withType esegue l'override del rendering per un intero tipo di catalogo.
Convalida dello schema rapida
Il framework di test applica il contratto del protocollo A2UI in modo sincrono. Quando il controller inizializza o aggiorna i componenti, esegue A2uiCoreSchemaValidator in base ai payload forniti. Se viene impostata una proprietà non valida, ad esempio un campo obbligatorio mancante o un tipo non corrispondente, il test si arresta immediatamente in modo anomalo con un A2uiValidationException.
Sincronizzazione delle coroutine
A2uiTestController.start si integra nel contesto della coroutine di test
fornito da runComposeUiTest(). Estrae currentCoroutineContext(), mappa
i loop di sfondo a un Job separato e si annulla automaticamente al termine del
blocco di test, impedendo l'esecuzione di test in sospeso. waitForIdle()
attende il completamento di tutte le coroutine in background in attesa.