Unverified Commit f477c8b1 authored by Ankur Jain's avatar Ankur Jain Committed by GitHub

Update accessibility contrast test coverage (#109784)

parent fa2deadd
...@@ -331,61 +331,79 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { ...@@ -331,61 +331,79 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
if (shouldSkipNode(data)) { if (shouldSkipNode(data)) {
return result; return result;
} }
final String text = data.label.isEmpty ? data.value : data.label;
final Iterable<Element> elements = find.text(text).hitTestable().evaluate();
for (final Element element in elements) {
result += await _evaluateElement(node, element, tester, image, byteData);
}
return result;
}
Future<Evaluation> _evaluateElement(
SemanticsNode node,
Element element,
WidgetTester tester,
ui.Image image,
ByteData byteData,
) async {
// Look up inherited text properties to determine text size and weight. // Look up inherited text properties to determine text size and weight.
late bool isBold; late bool isBold;
double? fontSize; double? fontSize;
final String text = data.label.isEmpty ? data.value : data.label;
final List<Element> elements = find.text(text).hitTestable().evaluate().toList();
late final Rect paintBounds; late final Rect paintBounds;
late final Rect paintBoundsWithOffset;
if (elements.length == 1) { final RenderObject? renderBox = element.renderObject;
final Element element = elements.single; if (renderBox is! RenderBox) {
final RenderObject? renderBox = element.renderObject; throw StateError('Unexpected renderObject type: $renderBox');
if (renderBox is! RenderBox) { }
throw StateError('Unexpected renderObject type: $renderBox');
}
const Offset offset = Offset(4.0, 4.0); const Offset offset = Offset(4.0, 4.0);
paintBounds = Rect.fromPoints( paintBoundsWithOffset = Rect.fromPoints(
renderBox.localToGlobal(renderBox.paintBounds.topLeft - offset), renderBox.localToGlobal(renderBox.paintBounds.topLeft - offset),
renderBox.localToGlobal(renderBox.paintBounds.bottomRight + offset), renderBox.localToGlobal(renderBox.paintBounds.bottomRight + offset),
); );
final Widget widget = element.widget;
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(element); paintBounds = Rect.fromPoints(
if (widget is Text) { renderBox.localToGlobal(renderBox.paintBounds.topLeft),
final TextStyle? style = widget.style; renderBox.localToGlobal(renderBox.paintBounds.bottomRight),
final TextStyle effectiveTextStyle = style == null || style.inherit );
? defaultTextStyle.style.merge(widget.style)
: style; final Offset? nodeOffset = node.transform != null ? MatrixUtils.getAsTranslation(node.transform!) : null;
isBold = effectiveTextStyle.fontWeight == FontWeight.bold;
fontSize = effectiveTextStyle.fontSize; final Rect nodeBounds = node.rect.shift(nodeOffset ?? Offset.zero);
} else if (widget is EditableText) { final Rect intersection = nodeBounds.intersect(paintBounds);
isBold = widget.style.fontWeight == FontWeight.bold; if (intersection.width <= 0 || intersection.height <= 0) {
fontSize = widget.style.fontSize; // Skip this element since it doesn't correspond to the given semantic
} else { // node.
throw StateError('Unexpected widget type: ${widget.runtimeType}'); return const Evaluation.pass();
} }
} else if (elements.length > 1) {
return Evaluation.fail( final Widget widget = element.widget;
'Multiple nodes with the same label: ${data.label}\n', final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(element);
); if (widget is Text) {
final TextStyle? style = widget.style;
final TextStyle effectiveTextStyle = style == null || style.inherit
? defaultTextStyle.style.merge(widget.style)
: style;
isBold = effectiveTextStyle.fontWeight == FontWeight.bold;
fontSize = effectiveTextStyle.fontSize;
} else if (widget is EditableText) {
isBold = widget.style.fontWeight == FontWeight.bold;
fontSize = widget.style.fontSize;
} else { } else {
// If we can't find the text node then assume the label does not throw StateError('Unexpected widget type: ${widget.runtimeType}');
// correspond to actual text.
return result;
} }
if (isNodeOffScreen(paintBounds, tester.binding.window)) { if (isNodeOffScreen(paintBoundsWithOffset, tester.binding.window)) {
return result; return const Evaluation.pass();
} }
final Map<Color, int> colorHistogram = _colorsWithinRect(byteData, paintBounds, image.width, image.height); final Map<Color, int> colorHistogram = _colorsWithinRect(byteData, paintBoundsWithOffset, image.width, image.height);
// Node was too far off screen. // Node was too far off screen.
if (colorHistogram.isEmpty) { if (colorHistogram.isEmpty) {
return result; return const Evaluation.pass();
} }
final _ContrastReport report = _ContrastReport(colorHistogram); final _ContrastReport report = _ContrastReport(colorHistogram);
...@@ -394,19 +412,18 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { ...@@ -394,19 +412,18 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
final double targetContrastRatio = this.targetContrastRatio(fontSize, bold: isBold); final double targetContrastRatio = this.targetContrastRatio(fontSize, bold: isBold);
if (contrastRatio - targetContrastRatio >= _tolerance) { if (contrastRatio - targetContrastRatio >= _tolerance) {
return result + const Evaluation.pass(); return const Evaluation.pass();
} }
return result + return Evaluation.fail(
Evaluation.fail( '$node:\n'
'$node:\n' 'Expected contrast ratio of at least $targetContrastRatio '
'Expected contrast ratio of at least $targetContrastRatio ' 'but found ${contrastRatio.toStringAsFixed(2)} '
'but found ${contrastRatio.toStringAsFixed(2)} ' 'for a font size of $fontSize.\n'
'for a font size of $fontSize.\n' 'The computed colors was:\n'
'The computed colors was:\n' 'light - ${report.lightColor}, dark - ${report.darkColor}\n'
'light - ${report.lightColor}, dark - ${report.darkColor}\n' 'See also: '
'See also: ' 'https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html',
'https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html', );
);
} }
/// Returns whether node should be skipped. /// Returns whether node should be skipped.
......
...@@ -23,6 +23,61 @@ void main() { ...@@ -23,6 +23,61 @@ void main() {
handle.dispose(); handle.dispose();
}); });
testWidgets('Multiple text with same label', (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(
_boilerplate(
Column(
children: const <Widget>[
Text(
'this is a test',
style: TextStyle(fontSize: 14.0, color: Colors.black),
),
Text(
'this is a test',
style: TextStyle(fontSize: 14.0, color: Colors.black),
),
],
),
),
);
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});
testWidgets(
'Multiple text with same label but Nodes excluded from '
'semantic tree have failing contrast should pass a11y guideline ',
(WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics();
await tester.pumpWidget(
_boilerplate(
Column(
children: const <Widget>[
Text(
'this is a test',
style: TextStyle(fontSize: 14.0, color: Colors.black),
),
SizedBox(height: 50),
Text(
'this is a test',
style: TextStyle(fontSize: 14.0, color: Colors.black),
),
SizedBox(height: 50),
ExcludeSemantics(
child: Text(
'this is a test',
style: TextStyle(fontSize: 14.0, color: Colors.white),
),
),
],
),
),
);
await expectLater(tester, meetsGuideline(textContrastGuideline));
handle.dispose();
});
testWidgets('white text on black background - Text Widget - direct style', testWidgets('white text on black background - Text Widget - direct style',
(WidgetTester tester) async { (WidgetTester tester) async {
final SemanticsHandle handle = tester.ensureSemantics(); final SemanticsHandle handle = tester.ensureSemantics();
......
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