用途
该技能用于在 Compose UI 测试中定位语义节点(semantics node)。每当开发者遇到“no node matched”“multiple nodes matched”“tag not found”或“ambiguous match”等来自 SemanticsNodeInteraction 的错误时,可使用本技能选择合适的查找器、推理计数契约,并决定何时切换 useUnmergedTree。
主要能力
- 根据信号类型选择查找器:Modifier.testTag 对应 onNodeWithTag;可见/可编辑/输入文本对应 onNodeWithText;无障碍内容描述对应 onNodeWithContentDescription;自定义谓词对应 onNode(matcher);整棵树对应 onRoot。
- 理解计数契约:单节点 SemanticsNodeInteraction 在匹配 0 或 >1 个节点时抛出 AssertionError;集合 SemanticsNodeInteractionCollection 在 0 匹配时返回空集合,assertCountEquals(0) 可通过。
- 决定 useUnmergedTree:合并树会将 Button 内部的 Text 和 Icon 折叠为一个节点;要定位内部 Text 需传 useUnmergedTree = true。该标志具有粘性,一旦创建查找器后,后续 onChild/onParent/filter 都会保持开启。
- 读取计数不匹配时自动附加的未合并树提示:当查找器针对合并树找到零个节点时,框架会针对未合并树重新运行同一查询,并将匹配结果嵌入错误消息。
- 支持子串或忽略大小写的文本匹配:onNodeWithText("Sub", substring = true) 可匹配 "Submit" 和 "Subscribe"。
使用方式
- 默认使用 onNodeWithTag(SomeTag)。在生产代码中通过 const val SomeTag = "Some" 定义常量,并添加 Modifier.testTag(SomeTag)。测试按标签查找,而非按文本。
- 按信号类型选择查找器。每个 onNodeWith* 都是 onNode(hasTestTag(...))/onNode(hasText(...))/onNode(hasContentDescription(...)) 的薄 @CheckResult 包装,行为与匹配器形式相同。
- 遵守计数契约。根据预期基数选择单数或复数变体。
- 仅对内部元素翻转 useUnmergedTree = true。默认使用合并树,仅在定位组合细节时翻转。
- 当计数不匹配错误显示未合并树匹配时,优先翻转 useUnmergedTree = true,而不是重写匹配器。
- 对于子串或忽略大小写的文本匹配,显式传入布尔标志。结合 onAllNodesWithText 与 assertCountEquals 表达意图。
适用场景
- 开发者报告“no node matched”“multiple nodes matched”“tag not found”或“ambiguous match”。
- 开发者询问如何在测试中定位 Button、Text、Icon、IconButton、Switch 或其他 composable。
- 开发者提及 onNodeWithTag、onNodeWithText、onNodeWithContentDescription、onAllNodesWith* 或 onRoot。
- 测试在本地通过,但对文案编辑或 UI 字符串的国际化轮换很脆弱。
- 开发者询问 useUnmergedTree、合并树,或“为什么我的内部 Text 不存在”。
限制
- 不适用于:节点已找到但断言失败(应参考断言节点状态与文本或断言边界与尺寸的技能);需要自定义谓词(应参考组合语义匹配器);节点仅作为子/兄弟/后代存在(应参考遍历语义树);需要检查语义树本身(应参考打印语义树)。
- 前置条件:需要配置 androidx.compose.ui:ui-test-junit4(或 v2);需要 ComposeTestRule 或 ComposeUiTest 在作用域内;被测 composable 必须在查找器运行前调用 setContent { … }。查找器是惰性的,每次操作都会重新求值。
强制规则
- 必须优先使用 onNodeWithTag,而非 onNodeWithText 和 onNodeWithContentDescription。
- 必须将 Modifier.testTag(...) 放在生产代码中并定义为常量。不得在生产源码和测试中重复定义相同的字符串字面量。
- 必须默认 useUnmergedTree = false。仅在定位被合并折叠的内部元素或验证组合细节时翻转为 true。
- 当计数本身是测试契约的一部分时,必须使用 onAllNodesWith* + assertCountEquals。不得依赖 onNodeWith* 通过副作用暗示“恰好一个”。
- 不得在单个语句中链式调用查找器超出清晰范围(例如 onNode(...).onChild().onChildAt(2).onChild())。应改为添加标签。
验证
- 测试文件中的每个查找器都使用 onNodeWithTag,除非断言明确关于用户可见文本或无障碍内容描述。
- 每个测试标签都在生产源码中定义为 const val,而非内联在测试中。
- useUnmergedTree = true 仅出现在定位合并 composable(Button、IconButton、ListItem 等)内部元素的位置。
- 在标签可胜任的地方不出现原始字符串字面量文本比较。
- ./gradlew :app:connectedDebugAndroidTest(设备)或 :app:testDebugUnitTest(Robolectric 主机)在本地通过。
- 不再残留“Reason: Expected exactly '1' node”错误。