Unverified Commit 399cd6a5 authored by pdblasi-google's avatar pdblasi-google Committed by GitHub

Refactors accessibility guidelines to remove the single window assumption. (#122760)

Refactors accessibility guidelines to remove the single window assumption
parent 3dd3c021
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'dart:ui';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -42,11 +43,11 @@ class Evaluation { ...@@ -42,11 +43,11 @@ class Evaluation {
} }
final StringBuffer buffer = StringBuffer(); final StringBuffer buffer = StringBuffer();
if (reason != null) { if (reason != null && reason!.isNotEmpty) {
buffer.write(reason); buffer.write(reason);
buffer.write(' '); buffer.writeln();
} }
if (other.reason != null) { if (other.reason != null && other.reason!.isNotEmpty) {
buffer.write(other.reason); buffer.write(other.reason);
} }
return Evaluation._( return Evaluation._(
...@@ -122,16 +123,22 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline { ...@@ -122,16 +123,22 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
@override @override
FutureOr<Evaluation> evaluate(WidgetTester tester) { FutureOr<Evaluation> evaluate(WidgetTester tester) {
return _traverse( Evaluation result = const Evaluation.pass();
tester, for (final FlutterView view in tester.platformDispatcher.views) {
result += _traverse(
view,
// TODO(pdblasi-google): Get the specific semantics root for this view when available
tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!, tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!,
); );
} }
Evaluation _traverse(WidgetTester tester, SemanticsNode node) { return result;
}
Evaluation _traverse(FlutterView view, SemanticsNode node) {
Evaluation result = const Evaluation.pass(); Evaluation result = const Evaluation.pass();
node.visitChildren((SemanticsNode child) { node.visitChildren((SemanticsNode child) {
result += _traverse(tester, child); result += _traverse(view, child);
return true; return true;
}); });
if (node.isMergedIntoParent) { if (node.isMergedIntoParent) {
...@@ -152,7 +159,7 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline { ...@@ -152,7 +159,7 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
// skip node if it is touching the edge of the screen, since it might // skip node if it is touching the edge of the screen, since it might
// be partially scrolled offscreen. // be partially scrolled offscreen.
const double delta = 0.001; const double delta = 0.001;
final Size physicalSize = tester.binding.window.physicalSize; final Size physicalSize = view.physicalSize;
if (paintBounds.left <= delta || if (paintBounds.left <= delta ||
paintBounds.top <= delta || paintBounds.top <= delta ||
(paintBounds.bottom - physicalSize.height).abs() <= delta || (paintBounds.bottom - physicalSize.height).abs() <= delta ||
...@@ -160,7 +167,7 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline { ...@@ -160,7 +167,7 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
return result; return result;
} }
// shrink by device pixel ratio. // shrink by device pixel ratio.
final Size candidateSize = paintBounds.size / tester.binding.window.devicePixelRatio; final Size candidateSize = paintBounds.size / view.devicePixelRatio;
if (candidateSize.width < size.width - delta || if (candidateSize.width < size.width - delta ||
candidateSize.height < size.height - delta) { candidateSize.height < size.height - delta) {
result += Evaluation.fail( result += Evaluation.fail(
...@@ -210,11 +217,21 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline { ...@@ -210,11 +217,21 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline {
@override @override
FutureOr<Evaluation> evaluate(WidgetTester tester) { FutureOr<Evaluation> evaluate(WidgetTester tester) {
final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!; Evaluation result = const Evaluation.pass();
Evaluation traverse(SemanticsNode node) {
// TODO(pdblasi-google): Use view to retrieve the appropriate root semantics node when available.
// ignore: unused_local_variable
for (final FlutterView view in tester.platformDispatcher.views) {
result += _traverse(tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!);
}
return result;
}
Evaluation _traverse(SemanticsNode node) {
Evaluation result = const Evaluation.pass(); Evaluation result = const Evaluation.pass();
node.visitChildren((SemanticsNode child) { node.visitChildren((SemanticsNode child) {
result += traverse(child); result += _traverse(child);
return true; return true;
}); });
if (node.isMergedIntoParent || if (node.isMergedIntoParent ||
...@@ -232,14 +249,11 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline { ...@@ -232,14 +249,11 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline {
if ((data.label.isEmpty) && (data.tooltip.isEmpty)) { if ((data.label.isEmpty) && (data.tooltip.isEmpty)) {
result += Evaluation.fail( result += Evaluation.fail(
'$node: expected tappable node to have semantic label, ' '$node: expected tappable node to have semantic label, '
'but none was found.\n', 'but none was found.',
); );
} }
return result; return result;
} }
return traverse(root);
}
} }
/// A guideline which verifies that all nodes that contribute semantics via text /// A guideline which verifies that all nodes that contribute semantics via text
...@@ -283,22 +297,28 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { ...@@ -283,22 +297,28 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
@override @override
Future<Evaluation> evaluate(WidgetTester tester) async { Future<Evaluation> evaluate(WidgetTester tester) async {
final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner!.rootSemanticsNode!; Evaluation result = const Evaluation.pass();
for (final FlutterView view in tester.platformDispatcher.views) {
// TODO(pdblasi): This renderView will need to be retrieved from view when available.
final RenderView renderView = tester.binding.renderView; final RenderView renderView = tester.binding.renderView;
final OffsetLayer layer = renderView.debugLayer! as OffsetLayer; final OffsetLayer layer = renderView.debugLayer! as OffsetLayer;
final SemanticsNode root = renderView.owner!.semanticsOwner!.rootSemanticsNode!;
late ui.Image image; late ui.Image image;
final ByteData? byteData = await tester.binding.runAsync<ByteData?>( final ByteData? byteData = await tester.binding.runAsync<ByteData?>(
() async { () async {
// Needs to be the same pixel ratio otherwise our dimensions won't match // Needs to be the same pixel ratio otherwise our dimensions won't match
// the last transform layer. // the last transform layer.
final double ratio = 1 / tester.binding.window.devicePixelRatio; final double ratio = 1 / view.devicePixelRatio;
image = await layer.toImage(renderView.paintBounds, pixelRatio: ratio); image = await layer.toImage(renderView.paintBounds, pixelRatio: ratio);
return image.toByteData(); return image.toByteData();
}, },
); );
return _evaluateNode(root, tester, image, byteData!); result += await _evaluateNode(root, tester, image, byteData!, view);
}
return result;
} }
Future<Evaluation> _evaluateNode( Future<Evaluation> _evaluateNode(
...@@ -306,6 +326,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { ...@@ -306,6 +326,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
WidgetTester tester, WidgetTester tester,
ui.Image image, ui.Image image,
ByteData byteData, ByteData byteData,
FlutterView view,
) async { ) async {
Evaluation result = const Evaluation.pass(); Evaluation result = const Evaluation.pass();
...@@ -327,7 +348,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { ...@@ -327,7 +348,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
return true; return true;
}); });
for (final SemanticsNode child in children) { for (final SemanticsNode child in children) {
result += await _evaluateNode(child, tester, image, byteData); result += await _evaluateNode(child, tester, image, byteData, view);
} }
if (shouldSkipNode(data)) { if (shouldSkipNode(data)) {
return result; return result;
...@@ -335,7 +356,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { ...@@ -335,7 +356,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
final String text = data.label.isEmpty ? data.value : data.label; final String text = data.label.isEmpty ? data.value : data.label;
final Iterable<Element> elements = find.text(text).hitTestable().evaluate(); final Iterable<Element> elements = find.text(text).hitTestable().evaluate();
for (final Element element in elements) { for (final Element element in elements) {
result += await _evaluateElement(node, element, tester, image, byteData); result += await _evaluateElement(node, element, tester, image, byteData, view);
} }
return result; return result;
} }
...@@ -346,6 +367,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { ...@@ -346,6 +367,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
WidgetTester tester, WidgetTester tester,
ui.Image image, ui.Image image,
ByteData byteData, ByteData byteData,
FlutterView view,
) async { ) 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;
...@@ -401,7 +423,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline { ...@@ -401,7 +423,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
throw StateError('Unexpected widget type: ${widget.runtimeType}'); throw StateError('Unexpected widget type: ${widget.runtimeType}');
} }
if (isNodeOffScreen(paintBoundsWithOffset, tester.binding.window)) { if (isNodeOffScreen(paintBoundsWithOffset, view)) {
return const Evaluation.pass(); return const Evaluation.pass();
} }
...@@ -512,27 +534,41 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline { ...@@ -512,27 +534,41 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline {
@override @override
Future<Evaluation> evaluate(WidgetTester tester) async { Future<Evaluation> evaluate(WidgetTester tester) async {
// Compute elements to be evaluated. // Compute elements to be evaluated.
final List<Element> elements = finder.evaluate().toList(); final List<Element> elements = finder.evaluate().toList();
final Map<FlutterView, ui.Image> images = <FlutterView, ui.Image>{};
final Map<FlutterView, ByteData> byteDatas = <FlutterView, ByteData>{};
// Obtain rendered image. // Collate all evaluations into a final evaluation, then return.
Evaluation result = const Evaluation.pass();
for (final Element element in elements) {
final FlutterView view = tester.viewOf(find.byElementPredicate((Element e) => e == element));
// TODO(pdblasi): Obtain this renderView from view when possible.
final RenderView renderView = tester.binding.renderView; final RenderView renderView = tester.binding.renderView;
final OffsetLayer layer = renderView.debugLayer! as OffsetLayer; final OffsetLayer layer = renderView.debugLayer! as OffsetLayer;
late ui.Image image;
final ByteData? byteData = await tester.binding.runAsync<ByteData?>( late final ui.Image image;
() async { late final ByteData byteData;
// Needs to be the same pixel ratio otherwise our dimensions won't match
// the last transform layer. // Obtain a previously rendered image or render one for a new view.
final double ratio = 1 / tester.binding.window.devicePixelRatio; await tester.binding.runAsync(() async {
image = await layer.toImage(renderView.paintBounds, pixelRatio: ratio); image = images[view] ??= await layer.toImage(
return image.toByteData(); renderView.paintBounds,
}, // Needs to be the same pixel ratio otherwise our dimensions
// won't match the last transform layer.
pixelRatio: 1 / view.devicePixelRatio,
); );
byteData = byteDatas[view] ??= (await image.toByteData())!;
});
// How to evaluate a single element. result = result + _evaluateElement(element, byteData, image);
}
return result;
}
Evaluation evaluateElement(Element element) { // How to evaluate a single element.
Evaluation _evaluateElement(Element element, ByteData byteData, ui.Image image) {
final RenderBox renderObject = element.renderObject! as RenderBox; final RenderBox renderObject = element.renderObject! as RenderBox;
final Rect originalPaintBounds = renderObject.paintBounds; final Rect originalPaintBounds = renderObject.paintBounds;
...@@ -544,7 +580,7 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline { ...@@ -544,7 +580,7 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline {
renderObject.localToGlobal(inflatedPaintBounds.bottomRight), renderObject.localToGlobal(inflatedPaintBounds.bottomRight),
); );
final Map<Color, int> colorHistogram = _colorsWithinRect(byteData!, paintBounds, image.width, image.height); final Map<Color, int> colorHistogram = _colorsWithinRect(byteData, paintBounds, image.width, image.height);
if (colorHistogram.isEmpty) { if (colorHistogram.isEmpty) {
return const Evaluation.pass(); return const Evaluation.pass();
...@@ -565,17 +601,6 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline { ...@@ -565,17 +601,6 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline {
); );
} }
} }
// Collate all evaluations into a final evaluation, then return.
Evaluation result = const Evaluation.pass();
for (final Element element in elements) {
result = result + evaluateElement(element);
}
return result;
}
} }
/// A class that reports the contrast ratio of a part of the screen. /// A class that reports the contrast ratio of a part of the screen.
......
...@@ -252,19 +252,17 @@ abstract class WidgetController { ...@@ -252,19 +252,17 @@ abstract class WidgetController {
/// The [TestFlutterView] provided by default when testing with /// The [TestFlutterView] provided by default when testing with
/// [WidgetTester.pumpWidget]. /// [WidgetTester.pumpWidget].
/// ///
/// If the test requires multiple views, it will need to use [viewOf] instead /// If the test uses multiple views, this will return the view that is painted
/// to ensure that the view related to the widget being evaluated is the one /// into by [WidgetTester.pumpWidget]. If a different view needs to be
/// that gets updated. /// accessed use [viewOf] to ensure that the view related to the widget being
/// evaluated is the one that gets updated.
/// ///
/// See also: /// See also:
/// ///
/// * [viewOf], which can find a [TestFlutterView] related to a given finder. /// * [viewOf], which can find a [TestFlutterView] related to a given finder.
/// This is how to modify view properties for testing when dealing with /// This is how to modify view properties for testing when dealing with
/// multiple views. /// multiple views.
TestFlutterView get view { TestFlutterView get view => platformDispatcher.implicitView!;
assert(platformDispatcher.views.length == 1, 'When testing with multiple views, use `viewOf` instead.');
return platformDispatcher.views.single;
}
/// Provides access to a [SemanticsController] for testing anything related to /// Provides access to a [SemanticsController] for testing anything related to
/// the [Semantics] tree. /// the [Semantics] tree.
......
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