Spent a while debugging a Compose screenshot test suite that was green on every PR for months, then watched the actual app clip a button label the first time someone bumped their phone's font size to "Larger."
The test suite "passed" the entire time. That's the part worth writing up.
The gap: your snapshots only exist at fontScale 1.0
Most Compose screenshot setups — Paparazzi, Roborazzi, Compose UI's own snapshot tooling — render each @Preview or test composable through one fixed device config. Unless someone deliberately parameterizes it, that config uses fontScale = 1.0f, because that's the default on both the emulator image and DeviceConfig's own defaults.
That's a real gap, not a hypothetical one: Android's accessibility settings let users scale system font size up to 1.3x ("Larger"), and some OEM skins + the newer non-linear font scaling in Android 14+ push effective scale even higher. None of that ever touches a snapshot test that only renders at 1.0.
Here's a layout that looks completely fine in every screenshot:
@Composable
fun OrderRow(item: String, price: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = item,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(Modifier.width(8.dp))
Text(
text = price,
modifier = Modifier.width(72.dp), // fixed width, "it always fits"
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
At fontScale = 1.0f a price like $1,249.00 fits inside 72dp with room to spare. At fontScale = 1.3f it doesn't, and TextOverflow.Ellipsis quietly truncates it to $1,24… — a price, silently wrong, with no crash, no lint warning, and a fully green test suite, because nothing ever rendered that composable at anything other than the default scale.
Making the gap visible: parameterize the device config, not just the composable
The fix isn't a new testing library, it's actually using the config knob that's already there. Paparazzi's DeviceConfig has a fontScale field — most setups just never touch it:
class OrderRowScreenshotTest {
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = DeviceConfig.PIXEL_5
)
@Test
fun orderRow_defaultScale() {
paparazzi.snapshot {
OrderRow(item = "Wireless Keyboard", price = "$1,249.00")
}
}
@Test
fun orderRow_largeFontScale() {
val scaledConfig = paparazzi.deviceConfig.copy(fontScale = 1.3f)
paparazzi.unsafeUpdateConfig(deviceConfig = scaledConfig)
paparazzi.snapshot {
OrderRow(item = "Wireless Keyboard", price = "$1,249.00")
}
}
}
Run that second test once and the snapshot shows the clipped price immediately — no manual device testing, no accessibility scanner, just a second rendering pass at a scale real users actually use.
If you're not on Paparazzi, the same idea works with a plain Compose UI test by overriding LocalDensity directly, which is the mechanism fontScale ends up driving anyway:
@Test
fun orderRow_largeFontScale_composeTest() {
composeTestRule.setContent {
val scaledDensity = Density(
density = LocalDensity.current.density,
fontScale = 1.3f
)
CompositionLocalProvider(LocalDensity provides scaledDensity) {
OrderRow(item = "Wireless Keyboard", price = "$1,249.00")
}
}
// assert on layout bounds / semantics instead of a snapshot if you're not on Paparazzi/Roborazzi
}
What actually to do with this
Don't try to snapshot every screen at every scale — that's a combinatorial mess and most of your screens don't have the fixed-width-next-to-dynamic-text shape that breaks. Instead:
- Audit for the specific pattern: any
Row/Boxpairing aweight()-based text with a fixed-width sibling (price columns, trailing badges, icon+label chips) is the shape that breaks first. - Add one
fontScale = 1.3fvariant per screen that has that pattern, not per composable. You're testing "does this layout survive," not re-testing visual design at every scale. - If you're on Paparazzi,
deviceConfig.copy(fontScale = ...)plusunsafeUpdateConfigis enough — no new dependency. - Treat a passing snapshot suite as proof your layout works at the scales you actually tested, not proof it works for users who turn on accessibility text scaling. Those are different claims, and only one of them is usually true.
The fuller version of this — plus three other classes of screenshot-test blind spots (RTL layout direction, dark theme state capture, and dynamic color token drift) — is in the Modern Android Screenshot Testing Playbook if you want the rest written up together.
Top comments (0)