How do Flutter's meetsGuideline checks for tap targets, labels and text contrast work in a widget test, and what do they miss?
answer
- enable semantics first
- expectLater(tester, meetsGuideline(...))
- 48x48 Android, 44x44 iOS
- labeledTapTargetGuideline for unnamed taps
- contrast check is a naive pixel sample
basics
~20 sAfter tester.ensureSemantics(), expectLater(tester, meetsGuideline(guideline)) scans the pumped screen's semantics nodes: tap targets against 48x48 or 44x44, tappable nodes for a label, and text for WCAG contrast from sampled pixels. They check one state, so they miss much.
solid answer
~40 s`flutter_test` ships `AccessibilityGuideline`s that evaluate the current screen through its semantics tree, so the test calls `tester.ensureSemantics()` first and disposes the handle at the end. `await expectLater(tester, meetsGuideline(androidTapTargetGuideline))` fails for any tappable node smaller than 48 by 48; `iOSTapTargetGuideline` uses 44 by 44; `labeledTapTargetGuideline` fails for nodes with a tap or long-press action but no label; `textContrastGuideline` renders the screen, splits the pixels around each text node naively into light and dark, and computes a WCAG contrast ratio. `doesNotMeetGuideline` is the inverse. They only see the state you pumped — not pressed, error or disabled states, not 2x text unless you set it — and the contrast sampling is unreliable over images and gradients. They are regression checks, not an audit.
code
dart · 28 linesimport 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
testWidgets('timetable meets tap-target, label and contrast guidelines', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Departures'),
actions: <Widget>[
IconButton(icon: const Icon(Icons.refresh), tooltip: 'Refresh', onPressed: () {}),
],
),
body: const Center(child: Text('17:42 London King\'s Cross')),
),
),
);
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});
}go deeper
Recall the four built-in guidelines and that the test calls tester.ensureSemantics() before expectLater(tester, meetsGuideline(...)).
Explain that the checks read semantics node sizes, actions and labels, and that contrast comes from sampled pixels.
Run the guidelines per key screen at default and 2x text, and state clearly which defects still need device testing.
Position automated guideline checks as a floor in the release process, paired with scheduled assistive-technology reviews.
## What the guidelines are `package:flutter_test` includes a small set of automated accessibility checks, each an `AccessibilityGuideline` evaluated against the widget tree a test has pumped. They are used with the async matchers **`meetsGuideline`** and **`doesNotMeetGuideline`**: ```dart final SemanticsHandle handle = tester.ensureSemantics(); await tester.pumpWidget(const TimetableApp()); await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); handle.dispose(); ``` The guidelines walk Flutter's **semantics tree**, so semantics must be enabled with `tester.ensureSemantics()`, and the handle must be disposed or the test fails for leaving it active. ## The built-in guidelines | Guideline | Checks | Fails when | |---|---|---| | `androidTapTargetGuideline` | tappable semantics nodes | a node is smaller than 48 by 48 | | `iOSTapTargetGuideline` | tappable semantics nodes | a node is smaller than 44 by 44 | | `labeledTapTargetGuideline` | nodes with a tap or long-press action | the node has no label | | `textContrastGuideline` | nodes whose label or value comes from `Text` or editable text | the estimated contrast is below the WCAG minimum | There is also `MinimumTextContrastGuidelineAAA` for the stricter level, and `MinimumTapTargetGuideline(size: ..., link: ...)` for a custom minimum. ## How the checks decide - **Tap targets** are measured from each semantics node's rectangle, so padding that Material widgets add counts, and a tiny `GestureDetector` wrapped in `Semantics(button: true)` is caught. Nodes touching the edge of a scrollable, or the edge of the view itself, are skipped, to avoid false failures on partly clipped targets — which also means a button flush with the screen edge is never measured. - **Labels** come from the semantics tree: a `tooltip`, a `semanticLabel`, text inside a merged node. - **Contrast** takes a screenshot of the area around each text node, partitions the colours very naively into "light" and "dark", picks the most frequent colour of each as foreground and background, and computes the ratio. It is a heuristic by design. ## What they miss The checks are narrow and see one frozen frame: 1. **Only the state you pumped.** Pressed, hovered, focused, disabled and error states, dialogs and later screens are unchecked unless each is pumped and checked. 2. **Only the text size you set.** Run the same checks with `tester.platformDispatcher.textScaleFactorTestValue = 2.0` if large text matters. 3. **Contrast over complex backgrounds.** Text over images, gradients or translucent layers defeats the light/dark split in either direction. 4. **Non-text contrast.** Icons, focus indicators and chart lines are not evaluated. 5. **Meaning.** A label of "button 3" passes `labeledTapTargetGuideline`; reading order, heading structure and whether announcements make sense need people with screen readers. 6. **Edge-touching targets.** A small button flush against the screen edge is skipped by the tap-target checks. 7. **Non-semantic targets.** A tappable area that produces no semantics node is invisible to the tap-target checks, which is itself a bug the label check will not catch. ## Using them well - Add one test per key screen that runs all four guidelines at the default size and again at 2x text. - Treat a failure as a real defect; do not relax the guideline to make it pass. - Use `doesNotMeetGuideline` in tests of your own components, to prove a check catches the bad configuration you are guarding against. - Keep manual device passes with TalkBack and VoiceOver in the process; the automated checks cover a small, mechanical slice.
- Why does a Flutter meetsGuideline test need tester.ensureSemantics()?The guidelines evaluate semantics nodes — their rectangles, actions and labels — and Flutter builds the semantics tree only while a client holds a `SemanticsHandle`. `tester.ensureSemantics()` provides one for the test. The handle must be disposed before the test ends, because flutter_test reports an active handle at the end of a test as a failure.
- Why can Flutter's textContrastGuideline pass text that users still cannot read?It samples the pixels around each text node and naively splits them into light and dark, taking the most frequent colour of each as foreground and background. Over a photo, gradient or translucent overlay that estimate can be far from what the eye sees, and only the state you pumped is checked, so hover, disabled and error styles go untested.
saying these in an interview costs you the question
- meetsGuideline works without enabling semantics in the test
- Passing all four guidelines means the screen is accessible
- labeledTapTargetGuideline checks that labels are meaningful
- textContrastGuideline reads colours from the theme, not rendered pixels
- The tap-target guidelines measure the visible drawing, not the semantics node