44
55import 'dart:async' ;
66import 'dart:ui' as ui;
7+ import 'dart:ui' ;
78
89import 'package:flutter/foundation.dart' ;
910import 'package:flutter/rendering.dart' ;
@@ -42,11 +43,11 @@ class Evaluation {
4243 }
4344
4445 final StringBuffer buffer = StringBuffer ();
45- if (reason != null ) {
46+ if (reason != null && reason ! .isNotEmpty ) {
4647 buffer.write (reason);
47- buffer.write ( ' ' );
48+ buffer.writeln ( );
4849 }
49- if (other.reason != null ) {
50+ if (other.reason != null && other.reason ! .isNotEmpty ) {
5051 buffer.write (other.reason);
5152 }
5253 return Evaluation ._(
@@ -122,16 +123,22 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
122123
123124 @override
124125 FutureOr <Evaluation > evaluate (WidgetTester tester) {
125- return _traverse (
126- tester,
127- tester.binding.pipelineOwner.semanticsOwner! .rootSemanticsNode! ,
128- );
126+ Evaluation result = const Evaluation .pass ();
127+ for (final FlutterView view in tester.platformDispatcher.views) {
128+ result += _traverse (
129+ view,
130+ // TODO(pdblasi-google): Get the specific semantics root for this view when available
131+ tester.binding.pipelineOwner.semanticsOwner! .rootSemanticsNode! ,
132+ );
133+ }
134+
135+ return result;
129136 }
130137
131- Evaluation _traverse (WidgetTester tester , SemanticsNode node) {
138+ Evaluation _traverse (FlutterView view , SemanticsNode node) {
132139 Evaluation result = const Evaluation .pass ();
133140 node.visitChildren ((SemanticsNode child) {
134- result += _traverse (tester , child);
141+ result += _traverse (view , child);
135142 return true ;
136143 });
137144 if (node.isMergedIntoParent) {
@@ -152,15 +159,15 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
152159 // skip node if it is touching the edge of the screen, since it might
153160 // be partially scrolled offscreen.
154161 const double delta = 0.001 ;
155- final Size physicalSize = tester.binding.window .physicalSize;
162+ final Size physicalSize = view .physicalSize;
156163 if (paintBounds.left <= delta ||
157164 paintBounds.top <= delta ||
158165 (paintBounds.bottom - physicalSize.height).abs () <= delta ||
159166 (paintBounds.right - physicalSize.width).abs () <= delta) {
160167 return result;
161168 }
162169 // shrink by device pixel ratio.
163- final Size candidateSize = paintBounds.size / tester.binding.window .devicePixelRatio;
170+ final Size candidateSize = paintBounds.size / view .devicePixelRatio;
164171 if (candidateSize.width < size.width - delta ||
165172 candidateSize.height < size.height - delta) {
166173 result += Evaluation .fail (
@@ -210,35 +217,42 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline {
210217
211218 @override
212219 FutureOr <Evaluation > evaluate (WidgetTester tester) {
213- final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner! .rootSemanticsNode! ;
214- Evaluation traverse (SemanticsNode node) {
215- Evaluation result = const Evaluation .pass ();
216- node.visitChildren ((SemanticsNode child) {
217- result += traverse (child);
218- return true ;
219- });
220- if (node.isMergedIntoParent ||
221- node.isInvisible ||
222- node.hasFlag (ui.SemanticsFlag .isHidden) ||
223- node.hasFlag (ui.SemanticsFlag .isTextField)) {
224- return result;
225- }
226- final SemanticsData data = node.getSemanticsData ();
227- // Skip node if it has no actions, or is marked as hidden.
228- if (! data.hasAction (ui.SemanticsAction .longPress) &&
229- ! data.hasAction (ui.SemanticsAction .tap)) {
230- return result;
231- }
232- if ((data.label.isEmpty) && (data.tooltip.isEmpty)) {
233- result += Evaluation .fail (
234- '$node : expected tappable node to have semantic label, '
235- 'but none was found.\n ' ,
236- );
237- }
238- return result;
220+ Evaluation result = const Evaluation .pass ();
221+
222+ // TODO(pdblasi-google): Use view to retrieve the appropriate root semantics node when available.
223+ // ignore: unused_local_variable
224+ for (final FlutterView view in tester.platformDispatcher.views) {
225+ result += _traverse (tester.binding.pipelineOwner.semanticsOwner! .rootSemanticsNode! );
239226 }
240227
241- return traverse (root);
228+ return result;
229+ }
230+
231+ Evaluation _traverse (SemanticsNode node) {
232+ Evaluation result = const Evaluation .pass ();
233+ node.visitChildren ((SemanticsNode child) {
234+ result += _traverse (child);
235+ return true ;
236+ });
237+ if (node.isMergedIntoParent ||
238+ node.isInvisible ||
239+ node.hasFlag (ui.SemanticsFlag .isHidden) ||
240+ node.hasFlag (ui.SemanticsFlag .isTextField)) {
241+ return result;
242+ }
243+ final SemanticsData data = node.getSemanticsData ();
244+ // Skip node if it has no actions, or is marked as hidden.
245+ if (! data.hasAction (ui.SemanticsAction .longPress) &&
246+ ! data.hasAction (ui.SemanticsAction .tap)) {
247+ return result;
248+ }
249+ if ((data.label.isEmpty) && (data.tooltip.isEmpty)) {
250+ result += Evaluation .fail (
251+ '$node : expected tappable node to have semantic label, '
252+ 'but none was found.' ,
253+ );
254+ }
255+ return result;
242256 }
243257}
244258
@@ -283,29 +297,36 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
283297
284298 @override
285299 Future <Evaluation > evaluate (WidgetTester tester) async {
286- final SemanticsNode root = tester.binding.pipelineOwner.semanticsOwner! .rootSemanticsNode! ;
287- final RenderView renderView = tester.binding.renderView;
288- final OffsetLayer layer = renderView.debugLayer! as OffsetLayer ;
289-
290- late ui.Image image;
291- final ByteData ? byteData = await tester.binding.runAsync <ByteData ?>(
292- () async {
293- // Needs to be the same pixel ratio otherwise our dimensions won't match
294- // the last transform layer.
295- final double ratio = 1 / tester.binding.window.devicePixelRatio;
296- image = await layer.toImage (renderView.paintBounds, pixelRatio: ratio);
297- return image.toByteData ();
298- },
299- );
300+ Evaluation result = const Evaluation .pass ();
301+ for (final FlutterView view in tester.platformDispatcher.views) {
302+ // TODO(pdblasi): This renderView will need to be retrieved from view when available.
303+ final RenderView renderView = tester.binding.renderView;
304+ final OffsetLayer layer = renderView.debugLayer! as OffsetLayer ;
305+ final SemanticsNode root = renderView.owner! .semanticsOwner! .rootSemanticsNode! ;
306+
307+ late ui.Image image;
308+ final ByteData ? byteData = await tester.binding.runAsync <ByteData ?>(
309+ () async {
310+ // Needs to be the same pixel ratio otherwise our dimensions won't match
311+ // the last transform layer.
312+ final double ratio = 1 / view.devicePixelRatio;
313+ image = await layer.toImage (renderView.paintBounds, pixelRatio: ratio);
314+ return image.toByteData ();
315+ },
316+ );
300317
301- return _evaluateNode (root, tester, image, byteData! );
318+ result += await _evaluateNode (root, tester, image, byteData! , view);
319+ }
320+
321+ return result;
302322 }
303323
304324 Future <Evaluation > _evaluateNode (
305325 SemanticsNode node,
306326 WidgetTester tester,
307327 ui.Image image,
308328 ByteData byteData,
329+ FlutterView view,
309330 ) async {
310331 Evaluation result = const Evaluation .pass ();
311332
@@ -327,15 +348,15 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
327348 return true ;
328349 });
329350 for (final SemanticsNode child in children) {
330- result += await _evaluateNode (child, tester, image, byteData);
351+ result += await _evaluateNode (child, tester, image, byteData, view );
331352 }
332353 if (shouldSkipNode (data)) {
333354 return result;
334355 }
335356 final String text = data.label.isEmpty ? data.value : data.label;
336357 final Iterable <Element > elements = find.text (text).hitTestable ().evaluate ();
337358 for (final Element element in elements) {
338- result += await _evaluateElement (node, element, tester, image, byteData);
359+ result += await _evaluateElement (node, element, tester, image, byteData, view );
339360 }
340361 return result;
341362 }
@@ -346,6 +367,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
346367 WidgetTester tester,
347368 ui.Image image,
348369 ByteData byteData,
370+ FlutterView view,
349371 ) async {
350372 // Look up inherited text properties to determine text size and weight.
351373 late bool isBold;
@@ -401,7 +423,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
401423 throw StateError ('Unexpected widget type: ${widget .runtimeType }' );
402424 }
403425
404- if (isNodeOffScreen (paintBoundsWithOffset, tester.binding.window )) {
426+ if (isNodeOffScreen (paintBoundsWithOffset, view )) {
405427 return const Evaluation .pass ();
406428 }
407429
@@ -512,69 +534,72 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline {
512534 @override
513535 Future <Evaluation > evaluate (WidgetTester tester) async {
514536 // Compute elements to be evaluated.
515-
516537 final List <Element > elements = finder.evaluate ().toList ();
538+ final Map <FlutterView , ui.Image > images = < FlutterView , ui.Image > {};
539+ final Map <FlutterView , ByteData > byteDatas = < FlutterView , ByteData > {};
517540
518- // Obtain rendered image.
519-
520- final RenderView renderView = tester.binding.renderView;
521- final OffsetLayer layer = renderView.debugLayer! as OffsetLayer ;
522- late ui.Image image;
523- final ByteData ? byteData = await tester.binding.runAsync <ByteData ?>(
524- () async {
525- // Needs to be the same pixel ratio otherwise our dimensions won't match
526- // the last transform layer.
527- final double ratio = 1 / tester.binding.window.devicePixelRatio;
528- image = await layer.toImage (renderView.paintBounds, pixelRatio: ratio);
529- return image.toByteData ();
530- },
531- );
541+ // Collate all evaluations into a final evaluation, then return.
542+ Evaluation result = const Evaluation .pass ();
543+ for (final Element element in elements) {
544+ final FlutterView view = tester.viewOf (find.byElementPredicate ((Element e) => e == element));
545+
546+ // TODO(pdblasi): Obtain this renderView from view when possible.
547+ final RenderView renderView = tester.binding.renderView;
548+ final OffsetLayer layer = renderView.debugLayer! as OffsetLayer ;
549+
550+ late final ui.Image image;
551+ late final ByteData byteData;
552+
553+ // Obtain a previously rendered image or render one for a new view.
554+ await tester.binding.runAsync (() async {
555+ image = images[view] ?? = await layer.toImage (
556+ renderView.paintBounds,
557+ // Needs to be the same pixel ratio otherwise our dimensions
558+ // won't match the last transform layer.
559+ pixelRatio: 1 / view.devicePixelRatio,
560+ );
561+ byteData = byteDatas[view] ?? = (await image.toByteData ())! ;
562+ });
532563
533- // How to evaluate a single element.
564+ result = result + _evaluateElement (element, byteData, image);
565+ }
534566
535- Evaluation evaluateElement ( Element element) {
536- final RenderBox renderObject = element.renderObject ! as RenderBox ;
567+ return result;
568+ }
537569
538- final Rect originalPaintBounds = renderObject.paintBounds;
570+ // How to evaluate a single element.
571+ Evaluation _evaluateElement (Element element, ByteData byteData, ui.Image image) {
572+ final RenderBox renderObject = element.renderObject! as RenderBox ;
539573
540- final Rect inflatedPaintBounds = originalPaintBounds. inflate ( 4.0 ) ;
574+ final Rect originalPaintBounds = renderObject.paintBounds ;
541575
542- final Rect paintBounds = Rect .fromPoints (
543- renderObject.localToGlobal (inflatedPaintBounds.topLeft),
544- renderObject.localToGlobal (inflatedPaintBounds.bottomRight),
545- );
576+ final Rect inflatedPaintBounds = originalPaintBounds.inflate (4.0 );
546577
547- final Map <Color , int > colorHistogram = _colorsWithinRect (byteData! , paintBounds, image.width, image.height);
578+ final Rect paintBounds = Rect .fromPoints (
579+ renderObject.localToGlobal (inflatedPaintBounds.topLeft),
580+ renderObject.localToGlobal (inflatedPaintBounds.bottomRight),
581+ );
548582
549- if (colorHistogram.isEmpty) {
550- return const Evaluation .pass ();
551- }
583+ final Map <Color , int > colorHistogram = _colorsWithinRect (byteData, paintBounds, image.width, image.height);
552584
553- final _ContrastReport report = _ContrastReport (colorHistogram);
554- final double contrastRatio = report.contrastRatio ();
555-
556- if (contrastRatio >= minimumRatio - tolerance) {
557- return const Evaluation .pass ();
558- } else {
559- return Evaluation .fail (
560- '$element :\n Expected contrast ratio of at least '
561- '$minimumRatio but found ${contrastRatio .toStringAsFixed (2 )} \n '
562- 'The computed light color was: ${report .lightColor }, '
563- 'The computed dark color was: ${report .darkColor }\n '
564- '$description ' ,
565- );
566- }
585+ if (colorHistogram.isEmpty) {
586+ return const Evaluation .pass ();
567587 }
568588
569- // Collate all evaluations into a final evaluation, then return.
570-
571- Evaluation result = const Evaluation .pass ();
589+ final _ContrastReport report = _ContrastReport (colorHistogram);
590+ final double contrastRatio = report.contrastRatio ();
572591
573- for (final Element element in elements) {
574- result = result + evaluateElement (element);
592+ if (contrastRatio >= minimumRatio - tolerance) {
593+ return const Evaluation .pass ();
594+ } else {
595+ return Evaluation .fail (
596+ '$element :\n Expected contrast ratio of at least '
597+ '$minimumRatio but found ${contrastRatio .toStringAsFixed (2 )} \n '
598+ 'The computed light color was: ${report .lightColor }, '
599+ 'The computed dark color was: ${report .darkColor }\n '
600+ '$description ' ,
601+ );
575602 }
576-
577- return result;
578603 }
579604}
580605
0 commit comments