Skip to content
Back to skills

Widget Golden And A11y Testing

ASecurity

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 loadAp...

  • 15 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 19, 2026
ai-agentsgonodetestingapiperformance

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 9 files and shows the line behind each finding

Scanned September 19, 2026

npx -y skills add zakariaf/Flutter-Skills --skill widget-golden-and-a11y-testing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Widget Golden And A11y Testing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Widget Golden And A11y Testing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/zakariaf-widget-golden-and-a11y-testing/badge)](https://www.skillsdirectory.com/skills/zakariaf-widget-golden-and-a11y-testing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
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.md15.4 KB
  • examples/a11y_test.dart5.2 KB
  • examples/harness.dart3.8 KB
  • examples/overflow_matrix_test.dart5.2 KB
  • references/a11y-guidelines-and-limits.md10.8 KB
  • references/golden-two-lanes.md5.2 KB
  • references/harness-and-mediaquery.md6.2 KB
  • references/overflow-and-textscale.md8.6 KB
  • scripts/check-test-hygiene.sh4.5 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.