SuperAPI AI 工具集

asserting-bounds-and-dimensions

测试与质量

查看项目

更新于 10-16 02:01

技能介绍

用途

该技能用于在 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 值。

使用方式

  1. 按问题类型选择断言。所有 API 位于 BoundsAssertions.kt。

  2. 布局计算使用未裁剪边界,判断“用户实际看到什么”使用裁剪边界。getUnclippedBoundsInRoot() 返回忽略视口裁剪的布局矩形;getBoundsInRoot() 会裁剪到视口。内边距/间距计算使用未裁剪边界,部分可见性检查使用裁剪边界。

  3. 默认容差为半个 dp。Dp.assertIsEqualTo 默认接受最多 0.5 dp 的偏差,因为布局取整会引入亚 dp 漂移。仅在明确的测量契约要求更严格精度时才覆盖该容差。

  4. 通过两个未裁剪矩形相减来计算内边距,然后调用 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 字符串会出现在失败信息中。

  1. 对于最小尺寸契约,使用 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)
  1. 验证点击目标可访问性时使用触摸边界。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