Library pengujian androidx.a2ui.compose:compose-ui-testing menyediakan API pengujian yang menggunakan pola pengontrol yang idiomatis untuk library pengujian Jetpack, seperti TestNavHostController Navigation.
Tidak seperti komponen Jetpack Compose standar yang mengambil parameter statis dan memancarkan
UI, komponen A2UI bersifat kontekstual. Mereka mengandalkan A2uiComponentScope untuk mengevaluasi binding data dinamis, mengirim tindakan keluar ke agen, menulis kembali ke binding data dua arah, dan memperluas template turunan dinamis.
API pengujian menyederhanakan penyiapan pengujian sekaligus menyediakan instance A2uiMessageProcessor yang sebenarnya, menjalankan rutinnya yang terikat ke lingkungan pengujian Compose.
Komponen terisolasi
Anda dapat memverifikasi bahwa setiap komponen menyelesaikan datanya, mengirimkan tindakan, dan merender dengan benar dalam tema sistem desain Anda:
@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"])
}
Status platform
Anda dapat menguji host platform seperti A2uiSurface, termasuk status dan transisinya:
@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()
}
Binding dua arah
Anda dapat menguji komponen seperti kolom teks yang menulis kembali ke model data selama input pengguna dan memverifikasi update reaktif saat agen mengubah model data:
@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()
}
Komponen dengan turunan yang dibuat menggunakan template
Anda dapat menguji komponen yang didesain untuk menampilkan kumpulan turunan yang ditentukan menggunakan template A2UI ChildList:
@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()
}
Penggantian error untuk error agen
Anda dapat memverifikasi bahwa platform dan komponen menangani error agen, seperti halusinasi, dengan baik:
@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 progresif
Anda dapat menguji status perantara saat komponen induk telah dimuat, tetapi komponen turunan masih menunggu:
@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()
}
Detail implementasi
Bagian berikut menjelaskan penggantian komponen, validasi skema, dan sinkronisasi coroutine dalam framework pengujian.
Library pengujian memperkenalkan API utama berikut:
A2uiTestController: Fungsi konstruktor ekstensi dan antarmuka pengontrol pengujian utama.A2uiComponentStub: Stub dan penggantian untuk komponen turunan dan katalog.A2uiTestSurface: Utilitas composable ringan yang memasang permukaan pengujian.
Penggantian komponen versus peniruan standar
Untuk menghilangkan framework tiruan pihak ketiga yang berat, komponen turunan dan dependensi eksternal dilewati menggunakan stub UI (A2uiComponentStub).
A2uiComponentStub.withId mencegat instance komponen tertentu berdasarkan ID, sementara
A2uiComponentStub.withType menggantikan rendering untuk seluruh jenis katalog.
Validasi skema yang langsung gagal
Framework pengujian menerapkan kontrak protokol A2UI secara sinkron. Saat
pengontrol menginisialisasi atau mengupdate komponen, pengontrol akan menjalankan A2uiCoreSchemaValidator
terhadap payload yang diberikan. Jika properti tidak valid ditetapkan, seperti kolom wajib diisi yang tidak ada atau ketidakcocokan jenis, pengujian akan langsung error dengan A2uiValidationException.
Sinkronisasi coroutine
A2uiTestController.start melakukan hook ke konteks coroutine pengujian
yang disediakan oleh runComposeUiTest(). Class ini mengekstrak currentCoroutineContext(), memetakan
loop latar belakang ke Job yang terpisah, dan membatalkan dirinya sendiri secara otomatis saat
blok pengujian selesai, sehingga mencegah eksekusi pengujian yang tidak teratur. waitForIdle()
menunggu semua coroutine latar belakang yang tertunda selesai.