Widget Golden And A11y Testing
ASecurityEnforces a disciplined widget/layout/golden/a11y test surface — one pumpApp harness that pins tester.view.physicalSize x devicePixelRatio and layers MediaQuery (textScaler/boldText/accessibleNavigation) above MaterialApp, an overflow net that never suppresses (one testWidgets per device x scale x bold tuple because overflow reports once per RenderObject), a computed getSize/getRect fit-and-geometry gate instead of goldens, two golden lanes (Ahem geometry + one pinned-OS real-font) with loadAp...
- 15 stars
- 0 votes
- 0 copies
- 2 views
- Added September 19, 2026
Works with
Security analysis
100/100Pro scans all 9 files and shows the line behind each finding
npx -y skills add zakariaf/Flutter-Skills --skill widget-golden-and-a11y-testing --agent claude-codeAre you the author of Widget Golden And A11y Testing?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/zakariaf-widget-golden-and-a11y-testing)---
name: widget-golden-and-a11y-testing
description: Enforces a disciplined widget/layout/golden/a11y test surface — one pumpApp harness that pins tester.view.physicalSize x devicePixelRatio and layers MediaQuery (textScaler/boldText/accessibleNavigation) above MaterialApp, an overflow net that never suppresses (one testWidgets per device x scale x bold tuple because overflow reports once per RenderObject), a computed getSize/getRect fit-and-geometry gate instead of goldens, two golden lanes (Ahem geometry + one pinned-OS real-font) with loadAppFonts and blocked --update-goldens, RTL goldens under Directionality, pure-Dart WCAG/APCA contrast on colour VALUES, and honest limits on meetsGuideline. Use when writing test/support/harness.dart, calling pumpWidget/pumpApp, chasing an "overflowed by N pixels" failure, reaching for takeException/ignoreOverflowErrors/FittedBox/withClampedTextScaling, adding matchesGoldenFile, or writing a11y_test.dart with isSemantics/simulatedAccessibilityTraversal/meetsGuideline.
---
# Widget, golden and accessibility testing
One harness pins the surface; layout and accessibility are asserted by **computed
geometry on the real widget tree**, not by blessed pixels. Goldens are a narrow,
honest safety net for glyph shaping and mirroring — never the layout gate. This
skill covers the widget tier; for the pure-core / Drift / Notifier tiers see
`testing-strategy`.
Read the reference for the task at hand:
- `references/harness-and-mediaquery.md` — pumpApp, Device presets, the four load-bearing lines, driving MediaQuery flags, finder policy.
- `references/overflow-and-textscale.md` — the two overflow classes, the three traps, the matrix, the fit assertion, and the four wrong fixes.
- `references/a11y-guidelines-and-limits.md` — the four built-in guidelines and their defects, the semantics/traversal gate, pure-Dart contrast, and what automation genuinely cannot cover.
- `references/golden-two-lanes.md` — the golden-refusal argument, the Ahem-vs-real-font lanes, RTL goldens, and blocking accidental blessing.
Run `scripts/check-test-hygiene.sh` before a PR.
## Non-negotiable rules
1. **Pin the device on every layout/geometry test.** The default widget surface
is **800x600 logical** — wider than any phone. Unpinned, content is ~2x too
wide, everything fits, the suite is green, and the shipped phone is broken.
`useDevice(...)` first, `pumpApp(...)` second.
2. **`physicalSize` is in PHYSICAL pixels — always multiply by DPR.**
`view.physicalSize = Size(320, 640)` at the default DPR 3.0 is a 107x213
logical surface, not a phone. Set `devicePixelRatio` and
`physicalSize = logical * dpr`, then `addTearDown(view.reset)` — a leaked size
poisons every later test in the file.
3. **Layer `MediaQuery` ABOVE `MaterialApp`, built from `.copyWith`.**
`MaterialApp` inserts no MediaQuery of its own; the one from `pumpWidget`'s
`View` is nearest. A bare `MediaQueryData()` zeroes the view-derived size the
device just pinned — the test then measures a 0x0 screen and passes.
4. **`pump()`, never `pumpAndSettle()` as an animation wait.** `pumpAndSettle`
carries a 10-minute timeout and truncates its stack trace, and hangs forever
on an infinite splash/shimmer/spinner. Use `pump()` for state changes and
`pump(duration)` / `fakeAsync` for timer-driven async — `pump()` does **not**
advance the fake clock.
5. **Never suppress overflow.** A `RenderFlex` overflow already **fails** a
widget test (it routes through `FlutterError.reportError` and the binding
rethrows at test end). Never call `takeException()` to swallow it, never assign
`FlutterError.onError`, never copy `ignoreOverflowErrors`, and never
`takeException()` in a global `tearDown` — each disarms the whole net.
6. **One `testWidgets` per (device, scale, bold) tuple — never a loop inside a
test.** Overflow is reported **once per RenderObject** (the flag resets only on
`reassemble()`), so looping scales inside one test silently under-reports every
scale after the first. Loop *around* the `testWidgets` call.
7. **Assert the fit, not just absence of overflow.** A clipped `Text` reports
nothing — `RenderParagraph` has no overflow indicator. `takeException(), isNull`
is necessary but not sufficient; add a `getSize`/`getRect` assertion that the
label fits inside its computed cell.
8. **Prefer computed geometry over goldens for layout.** Assert cells in a row
share a `top` and cells in a column share a `left` (`moreOrLessEquals`,
`epsilon: 0.5`); assert tap targets with a `getSize` loop. These fail with a
sentence a human can act on. Goldens cannot *assert* anything — a blessed
screen of clipped, unreadable text passes forever.
9. **Assert contrast on colour VALUES in pure Dart, never on pixels.**
`meetsGuideline(textContrastGuideline)` screenshots and histograms the layer —
white text on `#FAFAFA` **passes** (an open Flutter defect). A pure-Dart WCAG +
APCA test on the theme's colours cannot false-pass. For a state pair
distinguished only by chroma (selected vs surface at equal luminance), assert
`wcag(theme.selected, theme.surface)` directly — a grayscale-mode user perceives
exactly that luminance gap, so no separate grayscale channel is needed.
10. **`await expectLater(...)` for `meetsGuideline` — it returns an
`AsyncMatcher`.** A plain `expect()` looks right and asserts nothing. Keep the
four built-in guidelines only as advisory tripwires; the geometry and
pure-Dart contrast tests are the gate.
11. **Two golden lanes, both `loadAppFonts()`; block accidental
`--update-goldens`.** Ahem squares are byte-stable cross-OS and prove
geometry/mirroring but **not** glyph shaping; one narrow real-font lane on a
pinned OS proves script joining and numeral glyphs. Tag every golden
`@Tags(['golden'])` and generate blessed files in one pinned environment only.
12. **Never clamp `TextScaler`.** `withClampedTextScaling`, `textScaleFactor`, and
`FittedBox` defeat the matrix while contrast and tap-target stay green, and
override the user's own OS setting. Fix the layout, not the text.
## The harness — one file, imported by every test
`test/support/harness.dart` holds a `Device` value type and a `pumpApp` extension.
Seams throw until overridden, so an un-overridden dependency fails loudly instead
of quietly constructing a live service in a test. See
`references/harness-and-mediaquery.md` and `examples/harness.dart`.
```dart
class Device {
const Device(this.name, this.logicalSize, this.dpr);
final String name;
final Size logicalSize;
final double dpr;
// Neutral presets; name them by measured size, not a marketing model.
static const compact = Device('compact_320', Size(320, 640), 2.0);
static const small = Device('small_360', Size(360, 800), 3.0);
static const medium = Device('medium_412', Size(412, 915), 2.625);
static const all = <Device>[compact, small, medium];
}
extension TestHarness on WidgetTester {
void useDevice(Device d) {
view.devicePixelRatio = d.dpr;
view.physicalSize = d.logicalSize * d.dpr; // physical px — multiply by DPR
addTearDown(view.reset); // one call; nothing forgotten
}
Future<void> pumpApp({
List<Override> overrides = const <Override>[],
TextScaler textScaler = TextScaler.noScaling,
bool boldText = false,
bool accessibleNavigation = false,
}) async {
await pumpWidget(
ProviderScope(
overrides: overrides,
child: Builder(
builder: (context) => MediaQuery(
data: MediaQuery.of(context).copyWith( // copyWith, NOT MediaQueryData()
textScaler: textScaler,
boldText: boldText,
accessibleNavigation: accessibleNavigation,
),
child: const App(),
),
),
),
);
await pump(); // one frame; never pumpAndSettle
}
}
```
## The overflow + fit matrix
`Device.all` x `[1.0, 1.3, 1.5, 2.0, 3.0]` x `[false, true]` bold = one
`testWidgets` per tuple. `TextScaler.linear` is a deliberate over-approximation
(Android 14+ scales large text *less*) — conservative, not device-faithful. 1.3
and 1.5 are in the list precisely because nonlinear device scaling makes the
mid-range the non-obvious part. `boldText` widens advance widths — **but only when
a real proportional font is loaded**. Under the default Ahem test font every glyph
is a fixed em-square regardless of weight, so the bold axis is a no-op unless the
matrix calls `loadAppFonts()` first; with real fonts loaded it overflows content
that passes unbolded, and stresses real character widths the em-square hides.
```dart
// Real proportional fonts, or the bold + character-width axes are inert under Ahem.
setUpAll(loadAppFonts);
for (final device in Device.all) {
for (final scale in const <double>[1.0, 1.3, 1.5, 2.0, 3.0]) {
for (final bold in const <bool>[false, true]) {
testWidgets('no overflow @ ${device.name} x$scale${bold ? ' bold' : ''}',
(tester) async {
tester.useDevice(device);
await tester.pumpApp(textScaler: TextScaler.linear(scale), boldText: bold);
// Explicit for a readable message; the binding also rethrows at test end.
expect(tester.takeException(), isNull,
reason: 'content overflowed at ${device.name} x$scale');
});
}
}
}
```
The fit assertion is the real gate — it catches the *silent* class a clipped
`Text` never reports. See `references/overflow-and-textscale.md` and
`examples/overflow_matrix_test.dart`.
## The accessibility gate
Semantics is ON by default in `testWidgets`. Assert the node's role and label
directly with `isSemantics` (not the deprecated `containsSemantics`), measure tap
targets with an explicit `getSize` loop (the built-in guideline skips every node
flush with the view edge), and assert contrast on colour values in pure Dart.
```dart
testWidgets('each item exposes a button labelled by its display name',
(tester) async {
await tester.pumpApp();
final node = tester.getSemantics(find.byKey(const ValueKey('item_0')));
expect(node, isSemantics(
label: 'First item',
isButton: true,
hasEnabledState: true,
isEnabled: true,
hasTapAction: true,
));
});
```
Be honest: Flutter ships **four** machine-checkable guidelines (one known-broken),
covering a small minority of real accessibility. Never claim a suite "tests
accessibility". Switch Access / Switch Control cannot be tested automatically at
all. See `references/a11y-guidelines-and-limits.md` and `examples/a11y_test.dart`.
## Golden lanes
Layout is proven by computed geometry, so goldens are narrow: glyph shaping,
mirroring, and numeral rendering that geometry cannot see. Two lanes, both call
`loadAppFonts()`; RTL goldens pump under `Directionality`. See
`references/golden-two-lanes.md`.
```dart
@Tags(['golden'])
library;
testWidgets('card mirrors correctly in RTL', (tester) async {
await tester.pumpWidget(const Directionality(
textDirection: TextDirection.rtl,
child: _CardHarness(),
));
await expectLater(find.byType(ItemCard),
matchesGoldenFile('goldens/item_card_rtl.png'));
});
```
## Anti-patterns
- **Unpinned layout test.** Passes on 800x600, ships a broken 360dp phone. Rule 1.
- **`for` loop over scales inside one `testWidgets`.** Overflow reports once per
RenderObject; scales 2..n are silently unchecked. Rule 6.
- **`takeException()` in a global `tearDown`.** Clears `_pendingExceptionDetails`
before the binding rethrows — turns the entire overflow net into a no-op.
- **`ignoreOverflowErrors` / `FlutterError.onError = ...` in a layout test.** The
popular helper that loses the net that already exists.
- **`FittedBox` / `TextOverflow.ellipsis` / `withClampedTextScaling` to green a
red matrix.** Makes the most complex label the smallest, cancels the user's
TextScaler, and passes while the product is unreadable. Fix the layout.
- **`expect(tester, meetsGuideline(...))` without `await expectLater`.** Asserts
nothing — the matcher is async.
- **Tap-target claim resting on `meetsGuideline` alone.** It skips every node
flush with the view edge; add the explicit `getSize` loop.
- **Contrast claim resting on `textContrastGuideline`.** Open false-negative on
low-variance backgrounds; assert the colour ratio in pure Dart.
- **`find.byType(SomeWidget)` for behaviour or geometry.** Couples the test to the
class hierarchy; a rename reds the suite for nothing. Use `find.bySemanticsLabel`
for behaviour, `find.byKey` for geometry.
- **A layout golden as the gate.** It blesses whatever shipped, including clipped
text, and reds on any host that rasterizes fonts differently.
## Definition of done
- Every layout/geometry test — and any a11y test that measures size or position —
calls `useDevice(...)` before `pumpApp(...)`. Pure-semantics tests (role/label,
traversal) need no device.
- Overflow matrix exists as one `testWidgets` per (device, scale, bold) tuple; no
suppression anywhere in `test/`.
- A fit assertion (`getSize`/`getRect` inside the computed cell) backs the
overflow matrix; a geometry invariant replaces the layout golden.
- Tap targets measured with an explicit `getSize` loop; `meetsGuideline` used only
as advisory, always via `await expectLater`.
- Semantics asserted with `isSemantics`; label carries the display name, not
internal data; traversal order asserted if it is a deliberate design decision.
- Contrast asserted in pure Dart (WCAG + APCA) over theme colour values, looped
over every theme; chroma-only state pairs assert `wcag` between the two state
colours directly.
- Goldens (if any) are tagged `@Tags(['golden'])`, both lanes call `loadAppFonts`,
RTL goldens exist, and CI blocks `--update-goldens`.
- `scripts/check-test-hygiene.sh` passes.
## Related skills
- See `testing-strategy` for the overall test doctrine (pure clock-injected core,
fakes-over-mocks, real in-memory DB, coverage policy) this widget tier sits on.
- See `accessibility-as-code` for authoring the Semantics/roles/labels these tests
assert, and `i18n-rtl-l10n` for the Directional geometry the RTL goldens verify.
- See `design-system-structure` for the theme/ColorScheme the pure-Dart contrast
gate reads, and `flutter-performance` for the const/rebuild rules layout tests
should not try to re-prove.
- See `state-management-riverpod` for the `ProviderScope`/override seam `pumpApp`
uses, and `ci-pipeline-and-gates` for wiring the golden lanes and greps into CI.
- See `run-goldens-rebaseline` for the one sanctioned ritual that overwrites
committed golden images, and `ui-states-and-feedback` for the four screen states
each golden and widget test pumps.
## References
- Flutter — Widget testing introduction: https://docs.flutter.dev/cookbook/testing/widget/introduction
- Flutter API — `matchesGoldenFile` (OS/font/version sensitivity, `--update-goldens`): https://api.flutter.dev/flutter/flutter_test/matchesGoldenFile.html
- Flutter API — `WidgetTester` / `TestFlutterView`: https://api.flutter.dev/flutter/flutter_test/WidgetTester-class.html
- Flutter — Accessibility guidelines / `meetsGuideline`: https://api.flutter.dev/flutter/flutter_test/AccessibilityGuideline-class.html
- Flutter — Semantics matchers (`isSemantics`, `matchesSemantics`): https://api.flutter.dev/flutter/flutter_test/matchesSemantics.html
- `alchemist` — golden testing (`loadAppFonts`, CI vs platform lanes): https://pub.dev/packages/alchemist
- WCAG 2.2 contrast (1.4.3 / 1.4.6) and APCA: https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
Files in this skill
- SKILL.md
- examples/a11y_test.dart
- examples/harness.dart
- examples/overflow_matrix_test.dart
- references/a11y-guidelines-and-limits.md
- references/golden-two-lanes.md
- references/harness-and-mediaquery.md
- references/overflow-and-textscale.md
- scripts/check-test-hygiene.sh
Attribution
Comments
Loading comments…
Newsletter
Get the weekly list of safe new skills, and the ones we pulled.