کتابخانه تست androidx.a2ui.compose:compose-ui-testing APIهای تستی را ارائه میدهد که از یک الگوی کنترلر که مختص کتابخانههای تست Jetpack است، مانند TestNavHostController از Navigation، استفاده میکنند.
برخلاف کامپوننتهای استاندارد Jetpack Compose که پارامترهای استاتیک میگیرند و رابط کاربری (UI) منتشر میکنند، کامپوننتهای A2uiComponentScope وابسته به متن هستند. آنها برای ارزیابی اتصالهای داده پویا، ارسال اقدامات خروجی به عامل، نوشتن مجدد به اتصالهای داده دوطرفه و ایجاد قالبهای فرزند پویا، به A2uiComponentScope متکی هستند.
APIهای تست، تنظیمات تست را ساده میکنند و در عین حال نمونههای واقعی A2uiMessageProcessor را آماده میکنند و کوروتینهای آنها را که به محیط تست Compose متصل هستند، اجرا میکنند.
اجزای ایزوله
شما میتوانید تأیید کنید که یک کامپوننت منفرد، دادههای خود را حل میکند، اکشنها را ارسال میکند و به درستی در قالب سیستم طراحی شما رندر میشود:
@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"])
}
حالتهای سطحی
شما میتوانید میزبانهای سطحی مانند A2uiSurface ، از جمله حالتها و انتقالهای آنها را آزمایش کنید:
@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()
}
اتصال دو طرفه
شما میتوانید کامپوننتهایی مانند فیلدهای متنی را که در طول ورودی کاربر به مدل داده بازمیگردند، آزمایش کنید و بهروزرسانیهای واکنشی را هنگامی که عامل، مدل داده را تغییر میدهد، تأیید کنید:
@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()
}
کامپوننتهایی با فرزندان الگوسازیشده
شما میتوانید کامپوننتهایی را که برای نمایش مجموعههای فرزند تعریف شدهاند، با استفاده از قالبهای 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()
}
خطاهای جایگزین برای خطاهای عامل
شما میتوانید تأیید کنید که سطوح و اجزا، خطاهای عامل، مانند توهمات، را به خوبی مدیریت میکنند:
@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)
}
رندرینگ پیشرونده
میتوانید حالتهای میانی را که در آنها کامپوننت والد بارگذاری شده اما کامپوننتهای فرزند هنوز در انتظار بارگذاری هستند، آزمایش کنید:
@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()
}
جزئیات پیادهسازی
بخشهای بعدی، لغو کامپوننتها، اعتبارسنجی طرحواره و همگامسازی کوروتین در چارچوب تست را توضیح میدهند.
کتابخانه تست، API های اصلی زیر را معرفی میکند:
-
A2uiTestController: توابع سازنده افزونه و رابط کنترل کننده تست اصلی. -
A2uiComponentStub: خلاصهها و بازنویسیها برای کامپوننتهای فرزند و کاتالوگ. -
A2uiTestSurface: یک ابزار سبک و قابل ترکیب که یک سطح آزمایشی را نصب میکند.
لغو کامپوننت در مقابل تقلید استاندارد
برای حذف چارچوبهای تقلیدی شخص ثالث سنگین، کامپوننتهای فرزند و وابستگیهای خارجی با استفاده از UI stubها ( A2uiComponentStub ) دور زده میشوند. A2uiComponentStub.withId یک نمونه کامپوننت خاص را بر اساس شناسه رهگیری میکند، در حالی که A2uiComponentStub.withType رندرینگ را برای کل یک نوع کاتالوگ لغو میکند.
اعتبارسنجی طرحواره بدون خطا
چارچوب تست، قرارداد پروتکل A2UI را به صورت همزمان اجرا میکند. هنگامی که کنترلر، کامپوننتها را مقداردهی اولیه یا بهروزرسانی میکند، A2uiCoreSchemaValidator را برای بارهای داده شده اجرا میکند. اگر یک ویژگی نامعتبر تنظیم شود، مانند یک فیلد الزامی از دست رفته یا عدم تطابق نوع، تست بلافاصله با یک A2uiValidationException از کار میافتد.
همگامسازی کوروتین
A2uiTestController.start به زمینه کوروتین تست ارائه شده توسط runComposeUiTest() قلاب میکند. این currentCoroutineContext() را استخراج میکند، حلقههای پسزمینه را به یک Job جدا نگاشت میکند و پس از اتمام بلوک تست، به طور خودکار خود را لغو میکند و از اجرای تستهای معلق جلوگیری میکند. waitForIdle() منتظر میماند تا تمام کوروتینهای پسزمینه در حال انتظار به پایان برسند.