androidx.a2ui.compose:compose-ui-testing টেস্টিং লাইব্রেরিটি এমন টেস্টিং এপিআই প্রদান করে যা জেটপ্যাক টেস্টিং লাইব্রেরিগুলোর, যেমন নেভিগেশন-এর TestNavHostController , প্রচলিত কন্ট্রোলার প্যাটার্ন ব্যবহার করে।
সাধারণ Jetpack Compose কম্পোনেন্টগুলো স্ট্যাটিক প্যারামিটার গ্রহণ করে এবং UI নির্গত করে, কিন্তু A2UI কম্পোনেন্টগুলো প্রসঙ্গ-নির্ভর। এগুলো ডাইনামিক ডেটা বাইন্ডিং মূল্যায়ন করতে, এজেন্টের কাছে আউটবাউন্ড অ্যাকশন প্রেরণ করতে, দ্বি-মুখী ডেটা বাইন্ডিং-এ রাইট-ব্যাক করতে এবং ডাইনামিক চাইল্ড টেমপ্লেট ইনফ্লেট করতে A2uiComponentScope উপর নির্ভর করে।
টেস্টিং এপিআইগুলো আসল A2uiMessageProcessor ইনস্ট্যান্স সরবরাহ করার মাধ্যমে টেস্ট সেটআপকে সহজ করে তোলে এবং সেগুলোর কো-রুটিনগুলোকে কম্পোজ টেস্ট এনভায়রনমেন্টের সাথে সংযুক্ত করে চালায়।
বিচ্ছিন্ন উপাদান
আপনার ডিজাইন সিস্টেম থিমের মধ্যে আপনি যাচাই করতে পারেন যে, কোনো একটি স্বতন্ত্র কম্পোনেন্ট তার ডেটা সমাধান করছে, অ্যাকশন প্রেরণ করছে এবং সঠিকভাবে রেন্ডার হচ্ছে কি না:
@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 স্টাব ( A2uiComponentStub ) ব্যবহার করে চাইল্ড কম্পোনেন্ট এবং এক্সটার্নাল ডিপেন্ডেন্সিগুলো বাইপাস করা হয়। A2uiComponentStub.withId আইডি দ্বারা একটি নির্দিষ্ট কম্পোনেন্ট ইনস্ট্যান্সকে ইন্টারসেপ্ট করে, অন্যদিকে A2uiComponentStub.withType একটি সম্পূর্ণ ক্যাটালগ টাইপের রেন্ডারিং ওভাররাইড করে।
ত্রুটিমুক্ত স্কিমা যাচাইকরণ
টেস্ট ফ্রেমওয়ার্কটি সিনক্রোনাসভাবে A2UI প্রোটোকল কন্ট্রাক্ট প্রয়োগ করে। যখন কন্ট্রোলার কম্পোনেন্টগুলো ইনিশিয়ালাইজ বা আপডেট করে, তখন এটি প্রদত্ত পেলোডগুলোর বিপরীতে A2uiCoreSchemaValidator রান করে। যদি কোনো অবৈধ প্রপার্টি সেট করা হয়, যেমন কোনো প্রয়োজনীয় ফিল্ড অনুপস্থিত থাকা বা টাইপের অমিল, তাহলে টেস্টটি সাথে সাথে একটি A2uiValidationException দিয়ে ক্র্যাশ করে।
কোরুটিন সিঙ্ক্রোনাইজেশন
A2uiTestController.start runComposeUiTest() দ্বারা প্রদত্ত টেস্ট কো-রুটিন কনটেক্সটে হুক করে। এটি currentCoroutineContext() নিষ্কাশন করে, ব্যাকগ্রাউন্ড লুপগুলোকে একটি ডিটাচড Job সাথে ম্যাপ করে এবং টেস্ট ব্লকটি সম্পূর্ণ হলে স্বয়ংক্রিয়ভাবে নিজেকে বাতিল করে দেয়, ফলে অসমাপ্ত টেস্ট এক্সিকিউশন প্রতিরোধ করা যায়। waitForIdle() অপেক্ষমান সমস্ত ব্যাকগ্রাউন্ড কো-রুটিন শেষ হওয়া পর্যন্ত অপেক্ষা করে।