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
- 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.
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.
- 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.
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.
- 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.
- 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.
- 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.
- 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.
- 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.
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.
- 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.
- 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.
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.
// 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.
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.
@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
1---2name: widget-golden-and-a11y-testing3description: 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.4---56# Widget, golden and accessibility testing78One harness pins the surface; layout and accessibility are asserted by **computed9geometry on the real widget tree**, not by blessed pixels. Goldens are a narrow,10honest safety net for glyph shaping and mirroring — never the layout gate. This11skill covers the widget tier; for the pure-core / Drift / Notifier tiers see12`testing-strategy`.1314Read the reference for the task at hand:15- `references/harness-and-mediaquery.md` — pumpApp, Device presets, the four load-bearing lines, driving MediaQuery flags, finder policy.16- `references/overflow-and-textscale.md` — the two overflow classes, the three traps, the matrix, the fit assertion, and the four wrong fixes.17- `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.18- `references/golden-two-lanes.md` — the golden-refusal argument, the Ahem-vs-real-font lanes, RTL goldens, and blocking accidental blessing.1920Run `scripts/check-test-hygiene.sh` before a PR.2122## Non-negotiable rules23241. **Pin the device on every layout/geometry test.** The default widget surface25 is **800x600 logical** — wider than any phone. Unpinned, content is ~2x too26 wide, everything fits, the suite is green, and the shipped phone is broken.27 `useDevice(...)` first, `pumpApp(...)` second.282. **`physicalSize` is in PHYSICAL pixels — always multiply by DPR.**29 `view.physicalSize = Size(320, 640)` at the default DPR 3.0 is a 107x21330 logical surface, not a phone. Set `devicePixelRatio` and31 `physicalSize = logical * dpr`, then `addTearDown(view.reset)` — a leaked size32 poisons every later test in the file.333. **Layer `MediaQuery` ABOVE `MaterialApp`, built from `.copyWith`.**34 `MaterialApp` inserts no MediaQuery of its own; the one from `pumpWidget`'s35 `View` is nearest. A bare `MediaQueryData()` zeroes the view-derived size the36 device just pinned — the test then measures a 0x0 screen and passes.374. **`pump()`, never `pumpAndSettle()` as an animation wait.** `pumpAndSettle`38 carries a 10-minute timeout and truncates its stack trace, and hangs forever39 on an infinite splash/shimmer/spinner. Use `pump()` for state changes and40 `pump(duration)` / `fakeAsync` for timer-driven async — `pump()` does **not**41 advance the fake clock.425. **Never suppress overflow.** A `RenderFlex` overflow already **fails** a43 widget test (it routes through `FlutterError.reportError` and the binding44 rethrows at test end). Never call `takeException()` to swallow it, never assign45 `FlutterError.onError`, never copy `ignoreOverflowErrors`, and never46 `takeException()` in a global `tearDown` — each disarms the whole net.476. **One `testWidgets` per (device, scale, bold) tuple — never a loop inside a48 test.** Overflow is reported **once per RenderObject** (the flag resets only on49 `reassemble()`), so looping scales inside one test silently under-reports every50 scale after the first. Loop *around* the `testWidgets` call.517. **Assert the fit, not just absence of overflow.** A clipped `Text` reports52 nothing — `RenderParagraph` has no overflow indicator. `takeException(), isNull`53 is necessary but not sufficient; add a `getSize`/`getRect` assertion that the54 label fits inside its computed cell.558. **Prefer computed geometry over goldens for layout.** Assert cells in a row56 share a `top` and cells in a column share a `left` (`moreOrLessEquals`,57 `epsilon: 0.5`); assert tap targets with a `getSize` loop. These fail with a58 sentence a human can act on. Goldens cannot *assert* anything — a blessed59 screen of clipped, unreadable text passes forever.609. **Assert contrast on colour VALUES in pure Dart, never on pixels.**61 `meetsGuideline(textContrastGuideline)` screenshots and histograms the layer —62 white text on `#FAFAFA` **passes** (an open Flutter defect). A pure-Dart WCAG +63 APCA test on the theme's colours cannot false-pass. For a state pair64 distinguished only by chroma (selected vs surface at equal luminance), assert65 `wcag(theme.selected, theme.surface)` directly — a grayscale-mode user perceives66 exactly that luminance gap, so no separate grayscale channel is needed.6710. **`await expectLater(...)` for `meetsGuideline` — it returns an68 `AsyncMatcher`.** A plain `expect()` looks right and asserts nothing. Keep the69 four built-in guidelines only as advisory tripwires; the geometry and70 pure-Dart contrast tests are the gate.7111. **Two golden lanes, both `loadAppFonts()`; block accidental72 `--update-goldens`.** Ahem squares are byte-stable cross-OS and prove73 geometry/mirroring but **not** glyph shaping; one narrow real-font lane on a74 pinned OS proves script joining and numeral glyphs. Tag every golden75 `@Tags(['golden'])` and generate blessed files in one pinned environment only.7612. **Never clamp `TextScaler`.** `withClampedTextScaling`, `textScaleFactor`, and77 `FittedBox` defeat the matrix while contrast and tap-target stay green, and78 override the user's own OS setting. Fix the layout, not the text.7980## The harness — one file, imported by every test8182`test/support/harness.dart` holds a `Device` value type and a `pumpApp` extension.83Seams throw until overridden, so an un-overridden dependency fails loudly instead84of quietly constructing a live service in a test. See85`references/harness-and-mediaquery.md` and `examples/harness.dart`.8687```dart88class Device {89 const Device(this.name, this.logicalSize, this.dpr);90 final String name;91 final Size logicalSize;92 final double dpr;9394 // Neutral presets; name them by measured size, not a marketing model.95 static const compact = Device('compact_320', Size(320, 640), 2.0);96 static const small = Device('small_360', Size(360, 800), 3.0);97 static const medium = Device('medium_412', Size(412, 915), 2.625);98 static const all = <Device>[compact, small, medium];99}100101extension TestHarness on WidgetTester {102 void useDevice(Device d) {103 view.devicePixelRatio = d.dpr;104 view.physicalSize = d.logicalSize * d.dpr; // physical px — multiply by DPR105 addTearDown(view.reset); // one call; nothing forgotten106 }107108 Future<void> pumpApp({109 List<Override> overrides = const <Override>[],110 TextScaler textScaler = TextScaler.noScaling,111 bool boldText = false,112 bool accessibleNavigation = false,113 }) async {114 await pumpWidget(115 ProviderScope(116 overrides: overrides,117 child: Builder(118 builder: (context) => MediaQuery(119 data: MediaQuery.of(context).copyWith( // copyWith, NOT MediaQueryData()120 textScaler: textScaler,121 boldText: boldText,122 accessibleNavigation: accessibleNavigation,123 ),124 child: const App(),125 ),126 ),127 ),128 );129 await pump(); // one frame; never pumpAndSettle130 }131}132```133134## The overflow + fit matrix135136`Device.all` x `[1.0, 1.3, 1.5, 2.0, 3.0]` x `[false, true]` bold = one137`testWidgets` per tuple. `TextScaler.linear` is a deliberate over-approximation138(Android 14+ scales large text *less*) — conservative, not device-faithful. 1.3139and 1.5 are in the list precisely because nonlinear device scaling makes the140mid-range the non-obvious part. `boldText` widens advance widths — **but only when141a real proportional font is loaded**. Under the default Ahem test font every glyph142is a fixed em-square regardless of weight, so the bold axis is a no-op unless the143matrix calls `loadAppFonts()` first; with real fonts loaded it overflows content144that passes unbolded, and stresses real character widths the em-square hides.145146```dart147// Real proportional fonts, or the bold + character-width axes are inert under Ahem.148setUpAll(loadAppFonts);149150for (final device in Device.all) {151 for (final scale in const <double>[1.0, 1.3, 1.5, 2.0, 3.0]) {152 for (final bold in const <bool>[false, true]) {153 testWidgets('no overflow @ ${device.name} x$scale${bold ? ' bold' : ''}',154 (tester) async {155 tester.useDevice(device);156 await tester.pumpApp(textScaler: TextScaler.linear(scale), boldText: bold);157 // Explicit for a readable message; the binding also rethrows at test end.158 expect(tester.takeException(), isNull,159 reason: 'content overflowed at ${device.name} x$scale');160 });161 }162 }163}164```165166The fit assertion is the real gate — it catches the *silent* class a clipped167`Text` never reports. See `references/overflow-and-textscale.md` and168`examples/overflow_matrix_test.dart`.169170## The accessibility gate171172Semantics is ON by default in `testWidgets`. Assert the node's role and label173directly with `isSemantics` (not the deprecated `containsSemantics`), measure tap174targets with an explicit `getSize` loop (the built-in guideline skips every node175flush with the view edge), and assert contrast on colour values in pure Dart.176177```dart178testWidgets('each item exposes a button labelled by its display name',179 (tester) async {180 await tester.pumpApp();181 final node = tester.getSemantics(find.byKey(const ValueKey('item_0')));182 expect(node, isSemantics(183 label: 'First item',184 isButton: true,185 hasEnabledState: true,186 isEnabled: true,187 hasTapAction: true,188 ));189});190```191192Be honest: Flutter ships **four** machine-checkable guidelines (one known-broken),193covering a small minority of real accessibility. Never claim a suite "tests194accessibility". Switch Access / Switch Control cannot be tested automatically at195all. See `references/a11y-guidelines-and-limits.md` and `examples/a11y_test.dart`.196197## Golden lanes198199Layout is proven by computed geometry, so goldens are narrow: glyph shaping,200mirroring, and numeral rendering that geometry cannot see. Two lanes, both call201`loadAppFonts()`; RTL goldens pump under `Directionality`. See202`references/golden-two-lanes.md`.203204```dart205@Tags(['golden'])206library;207208testWidgets('card mirrors correctly in RTL', (tester) async {209 await tester.pumpWidget(const Directionality(210 textDirection: TextDirection.rtl,211 child: _CardHarness(),212 ));213 await expectLater(find.byType(ItemCard),214 matchesGoldenFile('goldens/item_card_rtl.png'));215});216```217218## Anti-patterns219220- **Unpinned layout test.** Passes on 800x600, ships a broken 360dp phone. Rule 1.221- **`for` loop over scales inside one `testWidgets`.** Overflow reports once per222 RenderObject; scales 2..n are silently unchecked. Rule 6.223- **`takeException()` in a global `tearDown`.** Clears `_pendingExceptionDetails`224 before the binding rethrows — turns the entire overflow net into a no-op.225- **`ignoreOverflowErrors` / `FlutterError.onError = ...` in a layout test.** The226 popular helper that loses the net that already exists.227- **`FittedBox` / `TextOverflow.ellipsis` / `withClampedTextScaling` to green a228 red matrix.** Makes the most complex label the smallest, cancels the user's229 TextScaler, and passes while the product is unreadable. Fix the layout.230- **`expect(tester, meetsGuideline(...))` without `await expectLater`.** Asserts231 nothing — the matcher is async.232- **Tap-target claim resting on `meetsGuideline` alone.** It skips every node233 flush with the view edge; add the explicit `getSize` loop.234- **Contrast claim resting on `textContrastGuideline`.** Open false-negative on235 low-variance backgrounds; assert the colour ratio in pure Dart.236- **`find.byType(SomeWidget)` for behaviour or geometry.** Couples the test to the237 class hierarchy; a rename reds the suite for nothing. Use `find.bySemanticsLabel`238 for behaviour, `find.byKey` for geometry.239- **A layout golden as the gate.** It blesses whatever shipped, including clipped240 text, and reds on any host that rasterizes fonts differently.241242## Definition of done243244- Every layout/geometry test — and any a11y test that measures size or position —245 calls `useDevice(...)` before `pumpApp(...)`. Pure-semantics tests (role/label,246 traversal) need no device.247- Overflow matrix exists as one `testWidgets` per (device, scale, bold) tuple; no248 suppression anywhere in `test/`.249- A fit assertion (`getSize`/`getRect` inside the computed cell) backs the250 overflow matrix; a geometry invariant replaces the layout golden.251- Tap targets measured with an explicit `getSize` loop; `meetsGuideline` used only252 as advisory, always via `await expectLater`.253- Semantics asserted with `isSemantics`; label carries the display name, not254 internal data; traversal order asserted if it is a deliberate design decision.255- Contrast asserted in pure Dart (WCAG + APCA) over theme colour values, looped256 over every theme; chroma-only state pairs assert `wcag` between the two state257 colours directly.258- Goldens (if any) are tagged `@Tags(['golden'])`, both lanes call `loadAppFonts`,259 RTL goldens exist, and CI blocks `--update-goldens`.260- `scripts/check-test-hygiene.sh` passes.261262## Related skills263264- See `testing-strategy` for the overall test doctrine (pure clock-injected core,265 fakes-over-mocks, real in-memory DB, coverage policy) this widget tier sits on.266- See `accessibility-as-code` for authoring the Semantics/roles/labels these tests267 assert, and `i18n-rtl-l10n` for the Directional geometry the RTL goldens verify.268- See `design-system-structure` for the theme/ColorScheme the pure-Dart contrast269 gate reads, and `flutter-performance` for the const/rebuild rules layout tests270 should not try to re-prove.271- See `state-management-riverpod` for the `ProviderScope`/override seam `pumpApp`272 uses, and `ci-pipeline-and-gates` for wiring the golden lanes and greps into CI.273- See `run-goldens-rebaseline` for the one sanctioned ritual that overwrites274 committed golden images, and `ui-states-and-feedback` for the four screen states275 each golden and widget test pumps.276277## References278279- Flutter — Widget testing introduction: https://docs.flutter.dev/cookbook/testing/widget/introduction280- Flutter API — `matchesGoldenFile` (OS/font/version sensitivity, `--update-goldens`): https://api.flutter.dev/flutter/flutter_test/matchesGoldenFile.html281- Flutter API — `WidgetTester` / `TestFlutterView`: https://api.flutter.dev/flutter/flutter_test/WidgetTester-class.html282- Flutter — Accessibility guidelines / `meetsGuideline`: https://api.flutter.dev/flutter/flutter_test/AccessibilityGuideline-class.html283- Flutter — Semantics matchers (`isSemantics`, `matchesSemantics`): https://api.flutter.dev/flutter/flutter_test/matchesSemantics.html284- `alchemist` — golden testing (`loadAppFonts`, CI vs platform lanes): https://pub.dev/packages/alchemist285- WCAG 2.2 contrast (1.4.3 / 1.4.6) and APCA: https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html