Unverified Commit d3e482ec authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

Add tests/matchers for automatic accessibility testing (#20462)

\
parent 4d9c3cc3
......@@ -46,6 +46,7 @@ library flutter_test;
export 'dart:async' show Future;
export 'src/accessibility.dart';
export 'src/all_elements.dart';
export 'src/binding.dart';
export 'src/controller.dart';
......
This diff is collapsed.
......@@ -17,9 +17,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'accessibility.dart';
import 'binding.dart';
import 'finders.dart';
import 'goldens.dart';
import 'widget_tester.dart' show WidgetTester;
/// Asserts that the [Finder] matches no widgets in the widget tree.
///
......@@ -461,6 +463,37 @@ Matcher matchesSemanticsData({
);
}
/// Asserts that the currently rendered widget meets the provided accessibility
/// `guideline`.
///
/// This matcher requires the result to be awaited and for semantics to be
/// enabled first.
///
/// ## Sample code
///
/// ```dart
/// final SemanticsHandle handle = tester.ensureSemantics();
/// await meetsGuideline(tester, meetsGuideline(textContrastGuideline));
/// handle.dispose();
/// ```
///
/// Supported accessibility guidelines:
///
/// * [androidTapTargetGuideline], for Android minimum tapable area guidelines.
/// * [iOSTapTargetGuideline], for iOS minimum tapable area guidelines.
/// * [textContrastGuideline], for WCAG minimum text contrast guidelines.
AsyncMatcher meetsGuideline(AccessibilityGuideline guideline) {
return new _MatchesAccessibilityGuideline(guideline);
}
/// The inverse matcher of [meetsGuideline].
///
/// This is needed because the [isNot] matcher does not compose with an
/// [AsyncMatcher].
AsyncMatcher doesNotMeetGuideline(AccessibilityGuideline guideline) {
return new _DoesNotMatchAccessibilityGuideline(guideline);
}
class _FindsWidgetMatcher extends Matcher {
const _FindsWidgetMatcher(this.min, this.max);
......@@ -1660,3 +1693,41 @@ class _MatchesSemanticsData extends Matcher {
return mismatchDescription.add(matchState['failure']);
}
}
class _MatchesAccessibilityGuideline extends AsyncMatcher {
_MatchesAccessibilityGuideline(this.guideline);
final AccessibilityGuideline guideline;
@override
Description describe(Description description) {
return description.add(guideline.description);
}
@override
Future<String> matchAsync(covariant WidgetTester tester) async {
final Evaluation result = await guideline.evaluate(tester);
if (result.passed)
return null;
return result.reason;
}
}
class _DoesNotMatchAccessibilityGuideline extends AsyncMatcher {
_DoesNotMatchAccessibilityGuideline(this.guideline);
final AccessibilityGuideline guideline;
@override
Description describe(Description description) {
return description.add('Does not ' + guideline.description);
}
@override
Future<String> matchAsync(covariant WidgetTester tester) async {
final Evaluation result = await guideline.evaluate(tester);
if (result.passed)
return 'Failed';
return null;
}
}
\ No newline at end of file
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('text contrast guideline', () {
testWidgets('black text on white background - Text Widget - direct style', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
const Text(
'this is a test',
style: TextStyle(fontSize: 14.0, color: Colors.black),
),
));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});
testWidgets('white text on black background - Text Widget - direct style', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
new Container(
width: 200.0,
height: 200.0,
color: Colors.black,
child: const Text(
'this is a test',
style: TextStyle(fontSize: 14.0, color: Colors.white),
),
),
));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});
testWidgets('black text on white background - Text Widget - inherited style', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
new DefaultTextStyle(
style: const TextStyle(fontSize: 14.0, color: Colors.black),
child: new Container(
color: Colors.white,
child: const Text('this is a test'),
),
),
));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});
testWidgets('white text on black background - Text Widget - inherited style', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
new DefaultTextStyle(
style: const TextStyle(fontSize: 14.0, color: Colors.white),
child: new Container(
width: 200.0,
height: 200.0,
color: Colors.black,
child: const Text('this is a test'),
),
),
));
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});
testWidgets('Material text field - amber on amber', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(new MaterialApp(
home: new Scaffold(
body: new Container(
width: 200.0,
height: 200.0,
color: Colors.amberAccent,
child: new TextField(
style: const TextStyle(color: Colors.amber),
controller: new TextEditingController(text: 'this is a test'),
),
),
),
));
await expectLater(tester, doesNotMeetGuideline(textContrastGuideline));
handle.dispose();
});
testWidgets('Material text field - default style', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(new MaterialApp(
home: new Scaffold(
body: new Center(
child: new TextField(
controller: new TextEditingController(text: 'this is a test'),
),
),
),
),
);
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});
testWidgets('grey text on white background fails with correct message', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
new Container(
width: 200.0,
height: 200.0,
color: Colors.yellow,
child: const Text(
'this is a test',
style: TextStyle(fontSize: 14.0, color: Colors.yellowAccent),
),
),
));
final Evaluation result = await textContrastGuideline.evaluate(tester);
expect(result.passed, false);
expect(result.reason,
'SemanticsNode#21(Rect.fromLTRB(300.0, 200.0, 500.0, 400.0), label: "this is a test",'
' textDirection: ltr):\nExpected contrast ratio of at least '
'4.5 but found 1.17 for a font size of 14.0. '
'The computed foreground color was: Color(0xfffafafa), '
'The computed background color was: Color(0xffffeb3b)\n'
'See also: https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html');
handle.dispose();
});
});
group('tap target size guideline', () {
testWidgets('Tappable box at 48 by 48', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
new SizedBox(
width: 48.0,
height: 48.0,
child: new GestureDetector(
onTap: () {},
),
),
));
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('Tappable box at 47 by 48', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
new SizedBox(
width: 47.0,
height: 48.0,
child: new GestureDetector(
onTap: () {},
),
),
));
await expectLater(tester, doesNotMeetGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('Tappable box at 48 by 47', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
new SizedBox(
width: 48.0,
height: 47.0,
child: new GestureDetector(
onTap: () {},
),
),
));
await expectLater(tester, doesNotMeetGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('Tappable box at 48 by 48 shrunk by transform', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
new Transform.scale(
scale: 0.5, // should have new height of 24 by 24.
child: new SizedBox(
width: 48.0,
height: 48.0,
child: new GestureDetector(
onTap: () {},
),
),
),
));
await expectLater(tester, doesNotMeetGuideline(androidTapTargetGuideline));
handle.dispose();
});
testWidgets('Too small tap target fails with the correct message', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(_boilerplate(
new SizedBox(
width: 48.0,
height: 47.0,
child: new GestureDetector(
onTap: () {},
),
),
));
final Evaluation result = await androidTapTargetGuideline.evaluate(tester);
expect(result.passed, false);
expect(result.reason,
'SemanticsNode#36(Rect.fromLTRB(376.0, 276.5, 424.0, 323.5), actions: [tap]): expected tap '
'target size of at least Size(48.0, 48.0), but found Size(48.0, 47.0)\n'
'See also: https://support.google.com/accessibility/android/answer/7101858?hl=en');
handle.dispose();
});
});
}
Widget _boilerplate(Widget child) {
return new MaterialApp(
home: new Scaffold(body: new Center(child: child)),
);
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment