npx skills add ...
npx skills add zakariaf/flutter-skills --skill widget-golden-and-a11y-testing
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.
npx skills add zakariaf/flutter-skills --skill widget-golden-and-a11y-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.
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.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.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.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.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.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.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.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.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.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.
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.
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.
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.
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.
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.
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.meetsGuideline alone. It skips every node
flush with the view edge; add the explicit getSize loop.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.useDevice(...) before pumpApp(...). Pure-semantics tests (role/label,
traversal) need no device.testWidgets per (device, scale, bold) tuple; no
suppression anywhere in test/.getSize/getRect inside the computed cell) backs the
overflow matrix; a geometry invariant replaces the layout golden.getSize loop; meetsGuideline used only
as advisory, always via await expectLater.isSemantics; label carries the display name, not
internal data; traversal order asserted if it is a deliberate design decision.wcag between the two state
colours directly.@Tags(['golden']), both lanes call loadAppFonts,
RTL goldens exist, and CI blocks --update-goldens.scripts/check-test-hygiene.sh passes.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.accessibility-as-code for authoring the Semantics/roles/labels these tests
assert, and i18n-rtl-l10n for the Directional geometry the RTL goldens verify.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.state-management-riverpod for the ProviderScope/override seam pumpApp
uses, and ci-pipeline-and-gates for wiring the golden lanes and greps into CI.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.matchesGoldenFile (OS/font/version sensitivity, --update-goldens): https://api.flutter.dev/flutter/flutter_test/matchesGoldenFile.htmlWidgetTester / TestFlutterView: https://api.flutter.dev/flutter/flutter_test/WidgetTester-class.htmlmeetsGuideline: https://api.flutter.dev/flutter/flutter_test/AccessibilityGuideline-class.htmlisSemantics, matchesSemantics): https://api.flutter.dev/flutter/flutter_test/matchesSemantics.htmlalchemist — golden testing (loadAppFonts, CI vs platform lanes): https://pub.dev/packages/alchemist// 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');
});
}
}
}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,
));
});@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'));
});