| name | asserting-bounds-and-dimensions |
| description | Use this skill to verify Compose layout measurements from a UI test using `assertWidthIsEqualTo`, `assertHeightIsEqualTo`, `assertWidthIsAtLeast`, `assertHeightIsAtLeast`, `assertTouchWidthIsEqualTo`, `assertTouchHeightIsEqualTo`, `assertPositionInRootIsEqualTo`, `assertTopPositionInRootIsEqualTo`, `assertLeftPositionInRootIsEqualTo`, plus read helpers `getUnclippedBoundsInRoot`, `getBoundsInRoot`, `getAlignmentLinePosition`, `getFirstLinkBounds`, and the underlying `Dp.assertIsEqualTo(expected, subject, tolerance = Dp(.5f))`. Covers the half-dp default tolerance, the unclipped vs clipped distinction, the canonical "compute padding from two unclipped rects" pattern, and minimum-touch-target assertions like `assertHeightIsAtLeast(MinHeight + 1.dp)`. Use when the developer wants to assert sizes, padding, alignment, position in dp, or asks about `getUnclippedBoundsInRoot`, `DpRect`, touch-target size, or compares widths in pixels. If the developer is comparing layout dimensions from a test, use this skill. |
| license | Apache-2.0. See LICENSE for complete terms. |
| metadata | {"author":"Jaewoong Eum (skydoves)","keywords":["jetpack-compose","ui-testing","assertWidthIsEqualTo","assertHeightIsAtLeast","assertPositionInRootIsEqualTo","getUnclippedBoundsInRoot","DpRect","touch-target","dp-tolerance","layout-assertions"]} |
Asserting Bounds and Dimensions — Layout Math in Dp, Not Pixels
Layout assertions belong in dp, run with a half-dp tolerance, and most of the interesting checks (padding, gap, alignment) are subtractions between two getUnclippedBoundsInRoot() rectangles. This skill picks the right size/position assertion, explains clipped vs unclipped, and shows the canonical "compute padding from two rects" pattern lifted directly from material3/ButtonTest.kt.
When to use this skill
- The developer wants to verify a Button is 48 dp tall, a Spacer is 16 dp wide, an Icon is at position
(24.dp, 12.dp).
- The developer asks how to assert the padding between two composables.
- The developer asks about minimum touch target sizes (
ChipDefaults.MinHeight + 1.dp).
- The developer is comparing layout values in pixels and wants the dp-typed equivalent.
- The developer mentions
assertWidthIsEqualTo, getUnclippedBoundsInRoot, DpRect, getAlignmentLinePosition, getFirstLinkBounds.
When NOT to use this skill
- The check is about state (enabled, on, selected) — see
./asserting-node-state-and-text/SKILL.md.
- The check is "is the node on screen at all" —
assertIsDisplayed() is enough; bounds math adds friction without value.
- The composable's bounds depend on an animation in flight — pause the clock first; see
../../synchronization/testing-animations-deterministically/SKILL.md.
- The bounds are relative to a screenshot — use a screenshot test instead.
Prerequisites
- A working
ComposeTestRule / ComposeUiTest. See ../../setup/configuring-test-dependencies/SKILL.md.
- The target composable has finished measuring and placing. If it animates in, advance the test clock first — see
../../synchronization/controlling-the-test-clock/SKILL.md.
- For touch-target assertions, the target node has a click action so
touchBoundsInRoot is meaningful.
Workflow
Patterns
Pattern: dp typed assertions over pixel reads
@Test
fun submit_isMin48dpTall() {
rule.setContent { CheckoutScreen() }
val node = rule.onNodeWithTag(SubmitTag).fetchSemanticsNode()
val heightPx = node.size.height
assert(heightPx >= 48 * Resources.getSystem().displayMetrics.density)
}
@Test
fun submit_isMin48dpTall() {
rule.setContent { CheckoutScreen() }
rule.onNodeWithTag(SubmitTag).assertHeightIsAtLeast(48.dp)
}
Pattern: padding by subtracting two unclipped rects
@Test
fun button_text_has24dpPadding() {
rule.setContent {
Button(onClick = {}, modifier = Modifier.testTag(ButtonTestTag)) {
Text("Submit", modifier = Modifier.testTag(TextTestTag).semantics(mergeDescendants = true) {})
}
}
val buttonBounds = rule.onNodeWithTag(ButtonTestTag).getUnclippedBoundsInRoot()
val textBounds = rule.onNodeWithTag(TextTestTag).getUnclippedBoundsInRoot()
(textBounds.left - buttonBounds.left).assertIsEqualTo(24.dp, "start padding")
(buttonBounds.right - textBounds.right).assertIsEqualTo(24.dp, "end padding")
}
The merge bypass on the inner Text is the same trick used by material3/ButtonTest.kt:202-226 to keep the inner Text addressable from the merged tree.
Pattern: assertPositionInRootIsEqualTo for absolute placement
rule.onNodeWithTag(CloseButtonTag)
.assertPositionInRootIsEqualTo(expectedLeft = 320.dp, expectedTop = 0.dp)
If only one axis matters, use assertLeftPositionInRootIsEqualTo / assertTopPositionInRootIsEqualTo to avoid coupling the test to layout decisions on the other axis.
Pattern: alignment line for baseline math
val baselineDp = rule.onNodeWithTag(LabelTag)
.getAlignmentLinePosition(FirstBaseline)
require(!baselineDp.isUnspecified) { "Label has no first baseline" }
baselineDp.assertIsEqualTo(20.dp, "first baseline of label")
getAlignmentLinePosition returns Dp.Unspecified when the alignment line is not provided (BoundsAssertions.kt:172-179). Always check isUnspecified before comparing.
Pattern: tolerance override for sub-dp precision
rule.onNodeWithTag(IconTag).getUnclippedBoundsInRoot().width.assertIsEqualTo(24.dp, "icon width")
val width = rule.onNodeWithTag(IconTag).getUnclippedBoundsInRoot().width
width.assertIsEqualTo(expected = 24.dp, subject = "icon width", tolerance = 0.1.dp)
Pattern: clipped vs unclipped — partial visibility
val unclipped = rule.onNodeWithTag(BannerTag).getUnclippedBoundsInRoot()
val clipped = rule.onNodeWithTag(BannerTag).getBoundsInRoot()
unclipped.height.assertIsEqualTo(200.dp, "banner intrinsic height")
clipped.height.assertIsEqualTo(80.dp, "banner visible height")
For "is any of it visible", prefer assertIsDisplayed() — see ./asserting-node-state-and-text/SKILL.md.
Mandatory rules
- MUST assert in dp using the typed
assertWidthIsEqualTo / assertHeightIsEqualTo / assertPositionInRootIsEqualTo. MUST NOT read fetchSemanticsNode().size.width and compare pixels.
- MUST use
getUnclippedBoundsInRoot() for padding / gap / alignment math; MUST use getBoundsInRoot() only when the contract is "what the user sees after clipping".
- MUST pass a meaningful
subject string to Dp.assertIsEqualTo so the failure message identifies which measurement failed.
- MUST prefer
assertHeightIsAtLeast(MinHeight + 1.dp) over assertHeightIsEqualTo(MinHeight + N.dp) when the goal is "the layout grows past the minimum at large font scales".
- MUST NOT assume zero tolerance. Layout rounding produces sub-dp drift; rely on the half-dp default and override only when justified.
- PREFERRED: when an animation is in flight, pause
mainClock.autoAdvance = false and step deterministically before reading bounds. Skydoves hot take #3.
Verification
References