Add labeled tappable target guideline and gallery tests (#22824)
diff --git a/examples/flutter_gallery/test/accessibility_test.dart b/examples/flutter_gallery/test/accessibility_test.dart
index bd651a7..27a3c51 100644
--- a/examples/flutter_gallery/test/accessibility_test.dart
+++ b/examples/flutter_gallery/test/accessibility_test.dart
@@ -8,231 +8,464 @@
testWidgets('backdrop_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: BackdropDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('bottom_app_bar_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: BottomAppBarDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('bottom_navigation_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: BottomNavigationDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('buttons_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: ButtonsDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('cards_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: CardsDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('chip_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: ChipDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('data_table_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: DataTableDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('date_and_time_picker_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: DateAndTimePickerDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('dialog_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: DialogDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('drawer_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: DrawerDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('elevation_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: ElevationDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('expansion_panels_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: ExpansionPanelsDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('grid_list_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(const MaterialApp(home: GridListDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('icons_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: IconsDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('leave_behind_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(const MaterialApp(home: LeaveBehindDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('list_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(const MaterialApp(home: ListDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('menu_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(const MaterialApp(home: MenuDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('modal_bottom_sheet_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: ModalBottomSheetDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('overscroll_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(const MaterialApp(home: OverscrollDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('page_selector_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: PageSelectorDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('persistent_bottom_sheet_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: PersistentBottomSheetDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('progress_indicator_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: ProgressIndicatorDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('reorderable_list_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(const MaterialApp(home: ReorderableListDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('scrollable_tabs_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: ScrollableTabsDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('search_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: SearchDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('selection_controls_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: SelectionControlsDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('slider_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: SliderDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('snack_bar_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(const MaterialApp(home: SnackBarDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('tabs_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: TabsDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('tabs_fab_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: TabsFabDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('text_form_field_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(const MaterialApp(home: TextFormFieldDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('tooltip_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: TooltipDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('two_level_list_demo', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(MaterialApp(home: TwoLevelListDemo()));
- expect(tester, meetsGuideline(androidTapTargetGuideline));
+ await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
+ handle.dispose();
+ });
+ });
+
+ group('All material demos have labeled tap targets', () {
+ testWidgets('backdrop_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: BackdropDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('bottom_app_bar_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: BottomAppBarDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('bottom_navigation_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: BottomNavigationDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('buttons_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: ButtonsDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('cards_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: CardsDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('chip_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: ChipDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('data_table_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: DataTableDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('date_and_time_picker_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: DateAndTimePickerDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('dialog_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: DialogDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('drawer_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: DrawerDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('elevation_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: ElevationDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('expansion_panels_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: ExpansionPanelsDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('grid_list_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(const MaterialApp(home: GridListDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('icons_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: IconsDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('leave_behind_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(const MaterialApp(home: LeaveBehindDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('list_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(const MaterialApp(home: ListDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('menu_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(const MaterialApp(home: MenuDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('modal_bottom_sheet_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: ModalBottomSheetDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('overscroll_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(const MaterialApp(home: OverscrollDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('page_selector_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: PageSelectorDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('persistent_bottom_sheet_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: PersistentBottomSheetDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('progress_indicator_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: ProgressIndicatorDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('reorderable_list_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(const MaterialApp(home: ReorderableListDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('scrollable_tabs_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: ScrollableTabsDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('search_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: SearchDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('selection_controls_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: SelectionControlsDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('slider_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: SliderDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('snack_bar_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(const MaterialApp(home: SnackBarDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('tabs_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: TabsDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('tabs_fab_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: TabsFabDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('text_form_field_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(const MaterialApp(home: TextFormFieldDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ }, skip: true);
+
+ testWidgets('tooltip_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: TooltipDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
+ handle.dispose();
+ });
+
+ testWidgets('two_level_list_demo', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(MaterialApp(home: TwoLevelListDemo()));
+ await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
handle.dispose();
});
});
diff --git a/packages/flutter_test/lib/src/accessibility.dart b/packages/flutter_test/lib/src/accessibility.dart
index ade0de6..3433ae0 100644
--- a/packages/flutter_test/lib/src/accessibility.dart
+++ b/packages/flutter_test/lib/src/accessibility.dart
@@ -124,6 +124,41 @@
String get description => 'Tappable objects should be at least $size';
}
+/// A guideline which enforces that all nodes with a tap or long press action
+/// also have a label.
+@visibleForTesting
+class LabeledTapTargetGuideline extends AccessibilityGuideline {
+ const LabeledTapTargetGuideline._();
+
+ @override
+ String get description => 'Tappable widgets should have a semantic label';
+
+ @override
+ FutureOr<Evaluation> evaluate(WidgetTester tester) {
+ final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
+ Evaluation traverse(SemanticsNode node) {
+ Evaluation result = const Evaluation.pass();
+ node.visitChildren((SemanticsNode child) {
+ result += traverse(child);
+ return true;
+ });
+ if (node.isMergedIntoParent)
+ return result;
+ final SemanticsData data = node.getSemanticsData();
+ // Skip node if it has no actions, or is marked as hidden.
+ if (!data.hasAction(ui.SemanticsAction.longPress) && !data.hasAction(ui.SemanticsAction.tap))
+ return result;
+ if (data.label == null || data.label.isEmpty) {
+ result += Evaluation.fail(
+ '$node: expected tappable node to have semantic label, but none was found\n',
+ );
+ }
+ return result;
+ }
+ return traverse(root);
+ }
+}
+
/// A guideline which verifies that all nodes that contribute semantics via text
/// meet minimum contrast levels.
///
@@ -403,3 +438,7 @@
/// foreground and background colors. The contrast ratio is calculated from
/// these colors according to the [WCAG](https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html#contrast-ratiodef)
const AccessibilityGuideline textContrastGuideline = MinimumTextContrastGuideline._();
+
+/// A guideline which enforces that all nodes with a tap or long press action
+/// also have a label.
+const AccessibilityGuideline labeledTapTargetGuideline = LabeledTapTargetGuideline._();
diff --git a/packages/flutter_test/test/accessibility_test.dart b/packages/flutter_test/test/accessibility_test.dart
index 7c32847..6fad639 100644
--- a/packages/flutter_test/test/accessibility_test.dart
+++ b/packages/flutter_test/test/accessibility_test.dart
@@ -361,6 +361,63 @@
handle.dispose();
});
});
+
+ group('Labeled tappable node guideline', () {
+ testWidgets('Passes when node is labeled', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(_boilerplate(Semantics(
+ container: true,
+ child: const SizedBox(width: 10.0, height: 10.0),
+ onTap: () {},
+ label: 'test',
+ )));
+ final Evaluation result = await labeledTapTargetGuideline.evaluate(tester);
+ expect(result.passed, true);
+ handle.dispose();
+ });
+ testWidgets('Fails if long-press has no label', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(_boilerplate(Semantics(
+ container: true,
+ child: const SizedBox(width: 10.0, height: 10.0),
+ onLongPress: () {},
+ label: '',
+ )));
+ final Evaluation result = await labeledTapTargetGuideline.evaluate(tester);
+ expect(result.passed, false);
+ handle.dispose();
+ });
+
+ testWidgets('Fails if tap has no label', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(_boilerplate(Semantics(
+ container: true,
+ child: const SizedBox(width: 10.0, height: 10.0),
+ onTap: () {},
+ label: '',
+ )));
+ final Evaluation result = await labeledTapTargetGuideline.evaluate(tester);
+ expect(result.passed, false);
+ handle.dispose();
+ });
+
+ testWidgets('Passes if tap is merged into labeled node', (WidgetTester tester) async {
+ final SemanticsHandle handle = tester.ensureSemantics();
+ await tester.pumpWidget(_boilerplate(Semantics(
+ container: true,
+ onLongPress: () {},
+ label: '',
+ child: Semantics(
+ label: 'test',
+ child: const SizedBox(width: 10.0, height: 10.0),
+ ),
+ )));
+ final Evaluation result = await labeledTapTargetGuideline.evaluate(tester);
+ expect(result.passed, true);
+ handle.dispose();
+ });
+ });
+
}
Widget _boilerplate(Widget child) {