evaluate method
- @override
override
Evaluate whether the current state of the tester
conforms to the rule.
Implementation
@override
Future<Evaluation> evaluate(WidgetTester tester) async {
final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode;
final RenderView renderView = tester.binding.renderView;
final OffsetLayer layer = renderView.layer;
ui.Image image;
final ByteData byteData = await tester.binding.runAsync<ByteData>(() async {
// Needs to be the same pixel ratio otherwise our dimensions won't match the
// last transform layer.
image = await layer.toImage(renderView.paintBounds, pixelRatio: 1.0);
return image.toByteData();
});
Future<Evaluation> evaluateNode(SemanticsNode node) async {
Evaluation result = const Evaluation.pass();
if (node.isInvisible || node.isMergedIntoParent || node.hasFlag(ui.SemanticsFlag.isHidden))
return result;
final SemanticsData data = node.getSemanticsData();
final List<SemanticsNode> children = <SemanticsNode>[];
node.visitChildren((SemanticsNode child) {
children.add(child);
return true;
});
for (SemanticsNode child in children)
result += await evaluateNode(child);
if (_shouldSkipNode(data))
return result;
// We need to look up the inherited text properties to determine the
// contrast ratio based on text size/weight.
double fontSize;
bool isBold;
final String text = (data.label?.isEmpty == true) ? data.value : data.label;
final List<Element> elements = find.text(text).hitTestable().evaluate().toList();
if (elements.length == 1) {
final Element element = elements.single;
final Widget widget = element.widget;
final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(element);
if (widget is Text) {
TextStyle effectiveTextStyle = widget.style;
if (widget.style == null || widget.style.inherit)
effectiveTextStyle = defaultTextStyle.style.merge(widget.style);
fontSize = effectiveTextStyle.fontSize;
isBold = effectiveTextStyle.fontWeight == FontWeight.bold;
} else if (widget is EditableText) {
isBold = widget.style.fontWeight == FontWeight.bold;
fontSize = widget.style.fontSize;
} else {
assert(false);
}
} else if (elements.length > 1) {
return Evaluation.fail('Multiple nodes with the same label: ${data.label}\n');
} else {
// If we can't find the text node then assume the label does not
// correspond to actual text.
return result;
}
// Transform local coordinate to screen coordinates.
Rect paintBounds = node.rect;
SemanticsNode current = node;
while (current != null && current.parent != null) {
if (current.transform != null)
paintBounds = MatrixUtils.transformRect(current.transform, paintBounds);
paintBounds = paintBounds.shift(current.parent?.rect?.topLeft ?? Offset.zero);
current = current.parent;
}
if (_isNodeOffScreen(paintBounds))
return result;
final List<int> subset = _subsetToRect(byteData, paintBounds, image.width, image.height);
// Node was too far off screen.
if (subset.isEmpty)
return result;
final _ContrastReport report = _ContrastReport(subset);
final double contrastRatio = report.contrastRatio();
const double delta = -0.01;
double targetContrastRatio;
if ((isBold && fontSize > kBoldTextMinimumSize) || (fontSize ?? 12.0) > kLargeTextMinimumSize) {
targetContrastRatio = kMinimumRatioLargeText;
} else {
targetContrastRatio = kMinimumRatioNormalText;
}
if (contrastRatio - targetContrastRatio >= delta)
return result + const Evaluation.pass();
return result + Evaluation.fail(
'$node:\nExpected contrast ratio of at least '
'$targetContrastRatio but found ${contrastRatio.toStringAsFixed(2)} for a font size of $fontSize. '
'The computed foreground color was: ${report.lightColor}, '
'The computed background color was: ${report.darkColor}\n'
'See also: https://www.w3.org/TR/UNDERSTANDING-WCAG20/visual-audio-contrast-contrast.html'
);
}
return evaluateNode(root);
}