4
4
5
5
import 'dart:async' ;
6
6
import 'dart:ui' as ui;
7
+ import 'dart:ui' ;
7
8
8
9
import 'package:flutter/foundation.dart' ;
9
10
import 'package:flutter/rendering.dart' ;
@@ -42,11 +43,11 @@ class Evaluation {
42
43
}
43
44
44
45
final StringBuffer buffer = StringBuffer ();
45
- if (reason != null ) {
46
+ if (reason != null && reason ! .isNotEmpty ) {
46
47
buffer.write (reason);
47
- buffer.write ( ' ' );
48
+ buffer.writeln ( );
48
49
}
49
- if (other.reason != null ) {
50
+ if (other.reason != null && other.reason ! .isNotEmpty ) {
50
51
buffer.write (other.reason);
51
52
}
52
53
return Evaluation ._(
@@ -122,16 +123,22 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
122
123
123
124
@override
124
125
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;
129
136
}
130
137
131
- Evaluation _traverse (WidgetTester tester , SemanticsNode node) {
138
+ Evaluation _traverse (FlutterView view , SemanticsNode node) {
132
139
Evaluation result = const Evaluation .pass ();
133
140
node.visitChildren ((SemanticsNode child) {
134
- result += _traverse (tester , child);
141
+ result += _traverse (view , child);
135
142
return true ;
136
143
});
137
144
if (node.isMergedIntoParent) {
@@ -152,15 +159,15 @@ class MinimumTapTargetGuideline extends AccessibilityGuideline {
152
159
// skip node if it is touching the edge of the screen, since it might
153
160
// be partially scrolled offscreen.
154
161
const double delta = 0.001 ;
155
- final Size physicalSize = tester.binding.window .physicalSize;
162
+ final Size physicalSize = view .physicalSize;
156
163
if (paintBounds.left <= delta ||
157
164
paintBounds.top <= delta ||
158
165
(paintBounds.bottom - physicalSize.height).abs () <= delta ||
159
166
(paintBounds.right - physicalSize.width).abs () <= delta) {
160
167
return result;
161
168
}
162
169
// shrink by device pixel ratio.
163
- final Size candidateSize = paintBounds.size / tester.binding.window .devicePixelRatio;
170
+ final Size candidateSize = paintBounds.size / view .devicePixelRatio;
164
171
if (candidateSize.width < size.width - delta ||
165
172
candidateSize.height < size.height - delta) {
166
173
result += Evaluation .fail (
@@ -210,35 +217,42 @@ class LabeledTapTargetGuideline extends AccessibilityGuideline {
210
217
211
218
@override
212
219
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! );
239
226
}
240
227
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;
242
256
}
243
257
}
244
258
@@ -283,29 +297,36 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
283
297
284
298
@override
285
299
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
+ );
300
317
301
- return _evaluateNode (root, tester, image, byteData! );
318
+ result += await _evaluateNode (root, tester, image, byteData! , view);
319
+ }
320
+
321
+ return result;
302
322
}
303
323
304
324
Future <Evaluation > _evaluateNode (
305
325
SemanticsNode node,
306
326
WidgetTester tester,
307
327
ui.Image image,
308
328
ByteData byteData,
329
+ FlutterView view,
309
330
) async {
310
331
Evaluation result = const Evaluation .pass ();
311
332
@@ -327,15 +348,15 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
327
348
return true ;
328
349
});
329
350
for (final SemanticsNode child in children) {
330
- result += await _evaluateNode (child, tester, image, byteData);
351
+ result += await _evaluateNode (child, tester, image, byteData, view );
331
352
}
332
353
if (shouldSkipNode (data)) {
333
354
return result;
334
355
}
335
356
final String text = data.label.isEmpty ? data.value : data.label;
336
357
final Iterable <Element > elements = find.text (text).hitTestable ().evaluate ();
337
358
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 );
339
360
}
340
361
return result;
341
362
}
@@ -346,6 +367,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
346
367
WidgetTester tester,
347
368
ui.Image image,
348
369
ByteData byteData,
370
+ FlutterView view,
349
371
) async {
350
372
// Look up inherited text properties to determine text size and weight.
351
373
late bool isBold;
@@ -401,7 +423,7 @@ class MinimumTextContrastGuideline extends AccessibilityGuideline {
401
423
throw StateError ('Unexpected widget type: ${widget .runtimeType }' );
402
424
}
403
425
404
- if (isNodeOffScreen (paintBoundsWithOffset, tester.binding.window )) {
426
+ if (isNodeOffScreen (paintBoundsWithOffset, view )) {
405
427
return const Evaluation .pass ();
406
428
}
407
429
@@ -512,69 +534,72 @@ class CustomMinimumContrastGuideline extends AccessibilityGuideline {
512
534
@override
513
535
Future <Evaluation > evaluate (WidgetTester tester) async {
514
536
// Compute elements to be evaluated.
515
-
516
537
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 > {};
517
540
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
+ });
532
563
533
- // How to evaluate a single element.
564
+ result = result + _evaluateElement (element, byteData, image);
565
+ }
534
566
535
- Evaluation evaluateElement ( Element element) {
536
- final RenderBox renderObject = element.renderObject ! as RenderBox ;
567
+ return result;
568
+ }
537
569
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 ;
539
573
540
- final Rect inflatedPaintBounds = originalPaintBounds. inflate ( 4.0 ) ;
574
+ final Rect originalPaintBounds = renderObject.paintBounds ;
541
575
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 );
546
577
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
+ );
548
582
549
- if (colorHistogram.isEmpty) {
550
- return const Evaluation .pass ();
551
- }
583
+ final Map <Color , int > colorHistogram = _colorsWithinRect (byteData, paintBounds, image.width, image.height);
552
584
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 ();
567
587
}
568
588
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 ();
572
591
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
+ );
575
602
}
576
-
577
- return result;
578
603
}
579
604
}
580
605
0 commit comments