用途
该技能用于在 Compose UI 测试中验证布局的测量结果,包括宽度、高度、位置、内边距、间距、对齐线以及触摸目标尺寸。核心原则是:布局断言应以 dp 为单位,而非像素,并默认使用半个 dp 的容差。大多数有价值的检查(内边距、间隙、对齐)都是两个 getUnclippedBoundsInRoot() 矩形之间的减法运算。
主要能力
该技能提供以下断言 API(均位于 commonMain/.../BoundsAssertions.kt):
- assertWidthIsEqualTo(expectedWidth: Dp):布局宽度是否恰好为 W dp。
- assertHeightIsEqualTo(expectedHeight: Dp):布局高度是否恰好为 H dp。
- assertWidthIsAtLeast(expectedMinWidth: Dp):宽度是否至少为 W。
- assertHeightIsAtLeast(expectedMinHeight: Dp):高度是否至少为 H。
- assertTouchWidthIsEqualTo(expectedWidth: Dp):触摸目标宽度。
- assertTouchHeightIsEqualTo(expectedHeight: Dp):触摸目标高度。
- assertPositionInRootIsEqualTo(left: Dp, top: Dp):在根布局中的精确位置。
- assertTopPositionInRootIsEqualTo(top: Dp):仅顶部位置。
- assertLeftPositionInRootIsEqualTo(left: Dp):仅左侧位置。
- getUnclippedBoundsInRoot(): DpRect:读取完整的未裁剪边界。
- getBoundsInRoot(): DpRect:读取裁剪后的边界。
- getAlignmentLinePosition(line: AlignmentLine): Dp:以 dp 读取对齐线位置。
- getFirstLinkBounds(predicate):读取 Text 中 LinkAnnotation 的边界。
- Dp.assertIsEqualTo(expected, subject, tolerance = Dp(.5f)):比较任意两个 Dp 值。
使用方式
-
按问题类型选择断言。所有 API 位于 BoundsAssertions.kt。
-
布局计算使用未裁剪边界,判断“用户实际看到什么”使用裁剪边界。getUnclippedBoundsInRoot() 返回忽略视口裁剪的布局矩形;getBoundsInRoot() 会裁剪到视口。内边距/间距计算使用未裁剪边界,部分可见性检查使用裁剪边界。
-
默认容差为半个 dp。Dp.assertIsEqualTo 默认接受最多 0.5 dp 的偏差,因为布局取整会引入亚 dp 漂移。仅在明确的测量契约要求更严格精度时才覆盖该容差。
-
通过两个未裁剪矩形相减来计算内边距,然后调用 Dp.assertIsEqualTo。这是 material3/ButtonTest.kt 中的经典模式:
val buttonBounds = rule.onNodeWithTag(ButtonTestTag).getUnclippedBoundsInRoot()
val textBounds = rule.onNodeWithTag(TextTestTag).getUnclippedBoundsInRoot()
(textBounds.left - buttonBounds.left).assertIsEqualTo(
24.dp,
"padding between the start of the button and the start of the text.",
)
(buttonBounds.right - textBounds.right).assertIsEqualTo(
24.dp,
"padding between the end of the text and the end of the button.",
)
buttonBounds.height.assertIsEqualTo(ButtonDefaults.MinHeight, "height of button.")
subject 字符串会出现在失败信息中。
- 对于最小尺寸契约,使用 assertHeightIsAtLeast / assertWidthIsAtLeast。当某个 composable 即使在大字体缩放下也不应小于某个常量时很有用。示例来自 material/ChipTest.kt:
rule.setMaterialContent { Chip(onClick = {}) { Text(text = "Test chip", fontSize = 50.sp) } }
rule.onNode(hasClickAction()).assertHeightIsAtLeast(ChipDefaults.MinHeight + 1.dp)
- 验证点击目标可访问性时使用触摸边界。assertTouchWidthIsEqualTo / assertTouchHeightIsEqualTo 读取 node.touchBoundsInRoot,当 composable 应用了 Modifier.minimumInteractiveComponentSize() 或类似修饰符时,触摸边界可能超出视觉边界。视觉边界使用 getUnclippedBoundsInRoot;触摸边界回答“点击会落在哪里”。
适用场景
- 开发者想验证 Button 高度为 48 dp、Spacer 宽度为 16 dp、Icon 位于 (24.dp, 12.dp)。
- 开发者询问如何断言两个 composable 之间的内边距。
- 开发者询问最小触摸目标尺寸(ChipDefaults.MinHeight + 1.dp)。
- 开发者正在比较像素单位的布局值,想要 dp 类型的等价写法。
- 开发者提到 assertWidthIsEqualTo、getUnclippedBoundsInRoot、DpRect、getAlignmentLinePosition、getFirstLinkBounds。
限制
以下情况不适用该技能:
- 检查的是状态(enabled、on、selected)——参见 ./asserting-node-state-and-text/SKILL.md。
- 检查的是“节点是否在屏幕上”——assertIsDisplayed() 已足够;边界计算只会增加摩擦而无价值。
- composable 的边界依赖正在进行的动画——先暂停时钟;参见 ../../synchronization/testing-animations-deterministically/SKILL.md。
- 边界相对于截图——应改用截图测试。
前置条件:
- 可用的 ComposeTestRule / ComposeUiTest。参见 ../../setup/configuring-test-dependencies/SKILL.md。
- 目标 composable 已完成测量和放置。如果它带动画进入,先推进测试时钟——参见 ../../synchronization/controlling-the-test-clock/SKILL.md。
- 对于触摸目标断言,目标节点需有点击动作,这样 touchBoundsInRoot 才有意义。
强制规则
- 必须使用类型化的 assertWidthIsEqualTo / assertHeightIsEqualTo / assertPositionInRootIsEqualTo 以 dp 进行断言。不得读取 fetchSemanticsNode().size.width 并比较像素。
- 内边距/间隙/对齐计算必须使用 getUnclippedBoundsInRoot();仅当契约是“裁剪后用户看到什么”时才使用 getBoundsInRoot()。
- 必须向 Dp.assertIsEqualTo 传入有意义的 subject 字符串,以便失败信息能识别哪个测量失败。
- 当目标是“布局在大字体缩放下增长超过最小值”时,必须优先使用 assertHeightIsAtLeast(MinHeight + 1.dp) 而非 assertHeightIsEqualTo(MinHeight + N.dp)。
- 不得假设零容差。布局取整会产生亚 dp 漂移;依赖半个 dp 的默认值,仅在合理时覆盖。
- 推荐:当动画进行中时,暂停 mainClock.autoAdvance = false 并在读取边界前确定性地步进。
验证
- 不再有 node.size.width / node.size.height 读取。所有尺寸检查使用类型化的 assert*IsEqualTo / Dp.assertIsEqualTo。
- 内边距/间隙计算使用 getUnclippedBoundsInRoot;裁剪读取仅在有注释解释原因时出现。
- 每个 Dp.assertIsEqualTo(...) 都传入非空 subject 字符串。
- 验证可访问性约束时,触摸目标断言使用 assertTouchWidthIsEqualTo / assertTouchHeightIsEqualTo 而非视觉边界。
- ./gradlew :app:connectedDebugAndroidTest 通过;失败信息通过 subject 识别具体失败的测量。
参考
- Compose 测试概览:https://developer.android.com/develop/ui/compose/testing
- Compose 测试速查表:https://developer.android.com/develop/ui/compose/testing-cheatsheet
- Compose 中的布局:https://developer.android.com/develop/ui/compose/layouts
- compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/BoundsAssertions.kt
- compose/material3/material3/src/androidDeviceTest/kotlin/androidx/compose/material3/ButtonTest.kt:202-226
- compose/material/material/src/androidDeviceTest/kotlin/androidx/compose/material/ChipTest.kt:226-233