Assertions in Dart and Flutter tests: accessibility matchers

This is the part of the ultimate cheat sheet dedicated to:

  • accessibility matchers,
  • accessibility guidelines.

In this series:

Accessibility matchers

There is only one pair of accessibility matchers: meetsGuideline and doesNotMeetGuideline. These matchers are asynchronous, and thus should be used with the expectLater assertion function mentioned before.

They accept AccessibilityGuideline object, which represents the type of performed accessibility check.

Accessibility guidelines

There are several predefined guidelines to check against:

  • androidTapTargetGuideline checks that tappable nodes have a minimum size of 48 by 48 pixels;
  • iOSTapTargetGuideline checks that tappable nodes have a minimum size of 44 by 44 pixels;
  • textContrastGuideline provides guidance for text contrast requirements specified by WCAG;
  • labeledTapTargetGuideline enforces that all nodes with a tap or long press action also have a label.

Your own guidelines can be created by inheriting AccessibilityGuideline class or creating your own instances of MinimumTapTargetGuideline, MinimumTextContrastGuideline, LabeledTapTargetGuideline.

Let’s take a look at this example:

testWidgets('meetsGuideline: iOSTapTargetGuideline βœ…', (tester) async {
final handle = tester.ensureSemantics();
final content = SizedBox.square(
dimension: 46.0,
child: GestureDetector(onTap: () {}),
);
await tester.pumpWidget(MaterialApp(home: Center(child: content)));
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
handle.dispose();
});

The GestureDetector will have a size of 46x46, which is just enough to satisfy the iOSTapTargetGuideline, which requires a 44x44 tap area. A similar test that uses androidTapTargetGuideline, which requires a 48x48 tap area, fails:

testWidgets('meetsGuideline: androidTapTargetGuideline ❌', (tester) async {
final handle = tester.ensureSemantics();
final content = SizedBox.square(
dimension: 46.0,
child: GestureDetector(onTap: () {}),
);
await tester.pumpWidget(MaterialApp(home: Center(child: content)));
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});

with the following output:

Expected: Tappable objects should be at least Size(48.0, 48.0)
Actual: <Instance of 'WidgetTester'>
Which: SemanticsNode(Rect.fromLTRB(377.0, 277.0, 423.0, 323.0), actions: [tap]):
expected tap target size of at least Size(48.0, 48.0), but found Size(46.0, 46.0)

Let’s check an example using textContrastGuideline:

testWidgets('meetsGuideline: textContrastGuideline βœ…', (tester) async {
final handle = tester.ensureSemantics();
final content = Container(
color: Colors.white,
child: Text(
'Text contrast test',
style: TextStyle(color: Colors.black),
),
);
await tester.pumpWidget(MaterialApp(home: Scaffold(body: content)));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});

Black text on white background has a good contrast ratio, so this test passes. However, small orange text on a white background is hard to read, and this test fails:

testWidgets('meetsGuideline: textContrastGuideline ❌', (tester) async {
final handle = tester.ensureSemantics();
final content = Container(
color: Colors.white,
child: Text(
'Text contrast test',
style: TextStyle(color: Colors.deepOrange),
),
);
await tester.pumpWidget(MaterialApp(home: Scaffold(body: content)));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});

with the following output:

Expected: Text contrast should follow WCAG guidelines
Actual: <Instance of 'WidgetTester'>
Which: SemanticsNode(Rect.fromLTRB(0.0, 0.0, 14.0, 14.0), label: "Text contrast test", textDirection: ltr):
Expected contrast ratio of at least 4.5 but found 3.03 for a font size of 14.0.

As you see, the output mentions that the expected contrast ratio depends in the font size. In the example above, when no text style was provided in Text widget, nor in MaterialApp, the default text size of 14 was applied. Interestingly enough, if the text font is increased, the same test passes because larger texts are easier to read even when the contrast ratio is not perfect:

testWidgets('meetsGuideline: textContrastGuideline βœ…', (tester) async {
final handle = tester.ensureSemantics();
final content = Container(
color: Colors.white,
child: Text(
'Text contrast test',
style: TextStyle(
fontSize: 20,
color: Colors.deepOrange,
),
),
);
await tester.pumpWidget(MaterialApp(home: Scaffold(body: content)));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});

Originally published at Invertase blog. Check out their awesome Authors Program!

Hi! πŸ‘‹πŸ» I’m Anna, Google Developer Expert in Flutter from Ukraine πŸ‡ΊπŸ‡¦ Follow me on Twitter, GitHub, YouTube, Medium to get notifications about my latest work.

It’s early 2023, and we in Ukraine are still fighting against russians committing genocide on our lands. If you find this content useful and have a coin to spare, support us with your donations. Stand with Ukraine!

--

--

Anna Leushchenko πŸ‘©β€πŸ’»πŸ’™πŸ“±πŸ‡ΊπŸ‡¦

Google Developer Expert in Dart and Flutter | Author, speaker at tech events, mentor, OSS contributor | Passionate mobile apps creator