• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6import 'dart:math' as math;
7import 'dart:typed_data';
8import 'dart:ui' as ui;
9import 'dart:ui';
10
11import 'package:meta/meta.dart';
12import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf;
13import 'package:test_api/test_api.dart' as test_package show TypeMatcher;
14import 'package:test_api/src/frontend/async_matcher.dart'; // ignore: implementation_imports
15
16import 'package:flutter/foundation.dart';
17import 'package:flutter/material.dart';
18import 'package:flutter/rendering.dart';
19import 'package:flutter/services.dart';
20
21import 'accessibility.dart';
22import 'binding.dart';
23import 'finders.dart';
24import 'goldens.dart';
25import 'widget_tester.dart' show WidgetTester;
26
27/// Asserts that the [Finder] matches no widgets in the widget tree.
28///
29/// ## Sample code
30///
31/// ```dart
32/// expect(find.text('Save'), findsNothing);
33/// ```
34///
35/// See also:
36///
37///  * [findsWidgets], when you want the finder to find one or more widgets.
38///  * [findsOneWidget], when you want the finder to find exactly one widget.
39///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
40const Matcher findsNothing = _FindsWidgetMatcher(null, 0);
41
42/// Asserts that the [Finder] locates at least one widget in the widget tree.
43///
44/// ## Sample code
45///
46/// ```dart
47/// expect(find.text('Save'), findsWidgets);
48/// ```
49///
50/// See also:
51///
52///  * [findsNothing], when you want the finder to not find anything.
53///  * [findsOneWidget], when you want the finder to find exactly one widget.
54///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
55const Matcher findsWidgets = _FindsWidgetMatcher(1, null);
56
57/// Asserts that the [Finder] locates at exactly one widget in the widget tree.
58///
59/// ## Sample code
60///
61/// ```dart
62/// expect(find.text('Save'), findsOneWidget);
63/// ```
64///
65/// See also:
66///
67///  * [findsNothing], when you want the finder to not find anything.
68///  * [findsWidgets], when you want the finder to find one or more widgets.
69///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
70const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1);
71
72/// Asserts that the [Finder] locates the specified number of widgets in the widget tree.
73///
74/// ## Sample code
75///
76/// ```dart
77/// expect(find.text('Save'), findsNWidgets(2));
78/// ```
79///
80/// See also:
81///
82///  * [findsNothing], when you want the finder to not find anything.
83///  * [findsWidgets], when you want the finder to find one or more widgets.
84///  * [findsOneWidget], when you want the finder to find exactly one widget.
85Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n);
86
87/// Asserts that the [Finder] locates a single widget that has at
88/// least one [Offstage] widget ancestor.
89///
90/// It's important to use a full finder, since by default finders exclude
91/// offstage widgets.
92///
93/// ## Sample code
94///
95/// ```dart
96/// expect(find.text('Save', skipOffstage: false), isOffstage);
97/// ```
98///
99/// See also:
100///
101///  * [isOnstage], the opposite.
102const Matcher isOffstage = _IsOffstage();
103
104/// Asserts that the [Finder] locates a single widget that has no
105/// [Offstage] widget ancestors.
106///
107/// See also:
108///
109///  * [isOffstage], the opposite.
110const Matcher isOnstage = _IsOnstage();
111
112/// Asserts that the [Finder] locates a single widget that has at
113/// least one [Card] widget ancestor.
114///
115/// See also:
116///
117///  * [isNotInCard], the opposite.
118const Matcher isInCard = _IsInCard();
119
120/// Asserts that the [Finder] locates a single widget that has no
121/// [Card] widget ancestors.
122///
123/// This is equivalent to `isNot(isInCard)`.
124///
125/// See also:
126///
127///  * [isInCard], the opposite.
128const Matcher isNotInCard = _IsNotInCard();
129
130/// Asserts that an object's toString() is a plausible one-line description.
131///
132/// Specifically, this matcher checks that the string does not contains newline
133/// characters, and does not have leading or trailing whitespace, is not
134/// empty, and does not contain the default `Instance of ...` string.
135const Matcher hasOneLineDescription = _HasOneLineDescription();
136
137/// Asserts that an object's toStringDeep() is a plausible multi-line
138/// description.
139///
140/// Specifically, this matcher checks that an object's
141/// `toStringDeep(prefixLineOne, prefixOtherLines)`:
142///
143///  * Does not have leading or trailing whitespace.
144///  * Does not contain the default `Instance of ...` string.
145///  * The last line has characters other than tree connector characters and
146///    whitespace. For example: the line ` │ ║ ╎` has only tree connector
147///    characters and whitespace.
148///  * Does not contain lines with trailing white space.
149///  * Has multiple lines.
150///  * The first line starts with `prefixLineOne`
151///  * All subsequent lines start with `prefixOtherLines`.
152const Matcher hasAGoodToStringDeep = _HasGoodToStringDeep();
153
154/// A matcher for functions that throw [FlutterError].
155///
156/// This is equivalent to `throwsA(isInstanceOf<FlutterError>())`.
157///
158/// If you are trying to test whether a call to [WidgetTester.pumpWidget]
159/// results in a [FlutterError], see [TestWidgetsFlutterBinding.takeException].
160///
161/// See also:
162///
163///  * [throwsAssertionError], to test if a function throws any [AssertionError].
164///  * [throwsArgumentError], to test if a functions throws an [ArgumentError].
165///  * [isFlutterError], to test if any object is a [FlutterError].
166final Matcher throwsFlutterError = throwsA(isFlutterError);
167
168/// A matcher for functions that throw [AssertionError].
169///
170/// This is equivalent to `throwsA(isInstanceOf<AssertionError>())`.
171///
172/// If you are trying to test whether a call to [WidgetTester.pumpWidget]
173/// results in an [AssertionError], see
174/// [TestWidgetsFlutterBinding.takeException].
175///
176/// See also:
177///
178///  * [throwsFlutterError], to test if a function throws a [FlutterError].
179///  * [throwsArgumentError], to test if a functions throws an [ArgumentError].
180///  * [isAssertionError], to test if any object is any kind of [AssertionError].
181final Matcher throwsAssertionError = throwsA(isAssertionError);
182
183/// A matcher for [FlutterError].
184///
185/// This is equivalent to `isInstanceOf<FlutterError>()`.
186///
187/// See also:
188///
189///  * [throwsFlutterError], to test if a function throws a [FlutterError].
190///  * [isAssertionError], to test if any object is any kind of [AssertionError].
191final Matcher isFlutterError = isInstanceOf<FlutterError>();
192
193/// A matcher for [AssertionError].
194///
195/// This is equivalent to `isInstanceOf<AssertionError>()`.
196///
197/// See also:
198///
199///  * [throwsAssertionError], to test if a function throws any [AssertionError].
200///  * [isFlutterError], to test if any object is a [FlutterError].
201final Matcher isAssertionError = isInstanceOf<AssertionError>();
202
203/// A matcher that compares the type of the actual value to the type argument T.
204// TODO(ianh): Remove this once https://github.com/dart-lang/matcher/issues/98 is fixed
205Matcher isInstanceOf<T>() => test_package.TypeMatcher<T>();
206
207/// Asserts that two [double]s are equal, within some tolerated error.
208///
209/// {@template flutter.flutter_test.moreOrLessEquals.epsilon}
210/// Two values are considered equal if the difference between them is within
211/// [precisionErrorTolerance] of the larger one. This is an arbitrary value
212/// which can be adjusted using the `epsilon` argument. This matcher is intended
213/// to compare floating point numbers that are the result of different sequences
214/// of operations, such that they may have accumulated slightly different
215/// errors.
216/// {@endtemplate}
217///
218/// See also:
219///
220///  * [closeTo], which is identical except that the epsilon argument is
221///    required and not named.
222///  * [inInclusiveRange], which matches if the argument is in a specified
223///    range.
224///  * [rectMoreOrLessEquals] and [offsetMoreOrLessEquals], which do something
225///    similar but for [Rect]s and [Offset]s respectively.
226Matcher moreOrLessEquals(double value, { double epsilon = precisionErrorTolerance }) {
227  return _MoreOrLessEquals(value, epsilon);
228}
229
230/// Asserts that two [Rect]s are equal, within some tolerated error.
231///
232/// {@macro flutter.flutter_test.moreOrLessEquals.epsilon}
233///
234/// See also:
235///
236///  * [moreOrLessEquals], which is for [double]s.
237///  * [offsetMoreOrLessEquals], which is for [Offset]s.
238///  * [within], which offers a generic version of this functionality that can
239///    be used to match [Rect]s as well as other types.
240Matcher rectMoreOrLessEquals(Rect value, { double epsilon = precisionErrorTolerance }) {
241  return _IsWithinDistance<Rect>(_rectDistance, value, epsilon);
242}
243
244/// Asserts that two [Offset]s are equal, within some tolerated error.
245///
246/// {@macro flutter.flutter_test.moreOrLessEquals.epsilon}
247///
248/// See also:
249///
250///  * [moreOrLessEquals], which is for [double]s.
251///  * [rectMoreOrLessEquals], which is for [Rect]s.
252///  * [within], which offers a generic version of this functionality that can
253///    be used to match [Offset]s as well as other types.
254Matcher offsetMoreOrLessEquals(Offset value, { double epsilon = precisionErrorTolerance }) {
255  return _IsWithinDistance<Offset>(_offsetDistance, value, epsilon);
256}
257
258/// Asserts that two [String]s are equal after normalizing likely hash codes.
259///
260/// A `#` followed by 5 hexadecimal digits is assumed to be a short hash code
261/// and is normalized to #00000.
262///
263/// See Also:
264///
265///  * [describeIdentity], a method that generates short descriptions of objects
266///    with ids that match the pattern #[0-9a-f]{5}.
267///  * [shortHash], a method that generates a 5 character long hexadecimal
268///    [String] based on [Object.hashCode].
269///  * [TreeDiagnosticsMixin.toStringDeep], a method that returns a [String]
270///    typically containing multiple hash codes.
271Matcher equalsIgnoringHashCodes(String value) {
272  return _EqualsIgnoringHashCodes(value);
273}
274
275/// A matcher for [MethodCall]s, asserting that it has the specified
276/// method [name] and [arguments].
277///
278/// Arguments checking implements deep equality for [List] and [Map] types.
279Matcher isMethodCall(String name, { @required dynamic arguments }) {
280  return _IsMethodCall(name, arguments);
281}
282
283/// Asserts that 2 paths cover the same area by sampling multiple points.
284///
285/// Samples at least [sampleSize]^2 points inside [areaToCompare], and asserts
286/// that the [Path.contains] method returns the same value for each of the
287/// points for both paths.
288///
289/// When using this matcher you typically want to use a rectangle larger than
290/// the area you expect to paint in for [areaToCompare] to catch errors where
291/// the path draws outside the expected area.
292Matcher coversSameAreaAs(Path expectedPath, { @required Rect areaToCompare, int sampleSize = 20 })
293  => _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);
294
295/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the
296/// golden image file identified by [key], with an optional [version] number.
297///
298/// For the case of a [Finder], the [Finder] must match exactly one widget and
299/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
300/// treated as the image for the widget.
301///
302/// [key] may be either a [Uri] or a [String] representation of a URI.
303///
304/// [version] is a number that can be used to differentiate historical golden
305/// files. This parameter is optional. Version numbers are used in golden file
306/// tests for package:flutter. You can learn more about these tests [here]
307/// (https://github.com/flutter/flutter/wiki/Writing-a-golden-file-test-for-package:flutter).
308///
309/// This is an asynchronous matcher, meaning that callers should use
310/// [expectLater] when using this matcher and await the future returned by
311/// [expectLater].
312///
313/// ## Sample code
314///
315/// ```dart
316/// await expectLater(find.text('Save'), matchesGoldenFile('save.png'));
317/// await expectLater(image, matchesGoldenFile('save.png'));
318/// await expectLater(imageFuture, matchesGoldenFile('save.png'));
319/// ```
320///
321/// Golden image files can be created or updated by running `flutter test
322/// --update-goldens` on the test.
323///
324/// See also:
325///
326///  * [goldenFileComparator], which acts as the backend for this matcher.
327///  * [matchesReferenceImage], which should be used instead if you want to
328///    verify that two different code paths create identical images.
329///  * [flutter_test] for a discussion of test configurations, whereby callers
330///    may swap out the backend for this matcher.
331AsyncMatcher matchesGoldenFile(dynamic key, {int version}) {
332  if (key is Uri) {
333    return _MatchesGoldenFile(key, version);
334  } else if (key is String) {
335    return _MatchesGoldenFile.forStringPath(key, version);
336  }
337  throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
338}
339
340/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches a
341/// reference image identified by [image].
342///
343/// For the case of a [Finder], the [Finder] must match exactly one widget and
344/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
345/// treated as the image for the widget.
346///
347/// This is an asynchronous matcher, meaning that callers should use
348/// [expectLater] when using this matcher and await the future returned by
349/// [expectLater].
350///
351/// ## Sample code
352///
353/// ```dart
354/// final ui.Paint paint = ui.Paint()
355///   ..style = ui.PaintingStyle.stroke
356///   ..strokeWidth = 1.0;
357/// final ui.PictureRecorder recorder = ui.PictureRecorder();
358/// final ui.Canvas pictureCanvas = ui.Canvas(recorder);
359/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
360/// final ui.Picture picture = recorder.endRecording();
361/// ui.Image referenceImage = picture.toImage(50, 50);
362///
363/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage));
364/// await expectLater(image, matchesReferenceImage(referenceImage);
365/// await expectLater(imageFuture, matchesReferenceImage(referenceImage));
366/// ```
367///
368/// See also:
369///
370///  * [matchesGoldenFile], which should be used instead if you need to verify
371///    that a [Finder] or [ui.Image] matches a golden image.
372AsyncMatcher matchesReferenceImage(ui.Image image) {
373  return _MatchesReferenceImage(image);
374}
375
376/// Asserts that a [SemanticsNode] contains the specified information.
377///
378/// If either the label, hint, value, textDirection, or rect fields are not
379/// provided, then they are not part of the comparison.  All of the boolean
380/// flag and action fields must match, and default to false.
381///
382/// To retrieve the semantics data of a widget, use [tester.getSemantics]
383/// with a [Finder] that returns a single widget. Semantics must be enabled
384/// in order to use this method.
385///
386/// ## Sample code
387///
388/// ```dart
389/// final SemanticsHandle handle = tester.ensureSemantics();
390/// expect(tester.getSemantics(find.text('hello')), matchesSemanticsNode(label: 'hello'));
391/// handle.dispose();
392/// ```
393///
394/// See also:
395///
396///   * [WidgetTester.getSemantics], the tester method which retrieves semantics.
397Matcher matchesSemantics({
398  String label,
399  String hint,
400  String value,
401  String increasedValue,
402  String decreasedValue,
403  TextDirection textDirection,
404  Rect rect,
405  Size size,
406  double elevation,
407  double thickness,
408  int platformViewId,
409  // Flags //
410  bool hasCheckedState = false,
411  bool isChecked = false,
412  bool isSelected = false,
413  bool isButton = false,
414  bool isFocused = false,
415  bool isTextField = false,
416  bool isReadOnly = false,
417  bool hasEnabledState = false,
418  bool isEnabled = false,
419  bool isInMutuallyExclusiveGroup = false,
420  bool isHeader = false,
421  bool isObscured = false,
422  bool isMultiline = false,
423  bool namesRoute = false,
424  bool scopesRoute = false,
425  bool isHidden = false,
426  bool isImage = false,
427  bool isLiveRegion = false,
428  bool hasToggledState = false,
429  bool isToggled = false,
430  bool hasImplicitScrolling = false,
431  // Actions //
432  bool hasTapAction = false,
433  bool hasLongPressAction = false,
434  bool hasScrollLeftAction = false,
435  bool hasScrollRightAction = false,
436  bool hasScrollUpAction = false,
437  bool hasScrollDownAction = false,
438  bool hasIncreaseAction = false,
439  bool hasDecreaseAction = false,
440  bool hasShowOnScreenAction = false,
441  bool hasMoveCursorForwardByCharacterAction = false,
442  bool hasMoveCursorBackwardByCharacterAction = false,
443  bool hasMoveCursorForwardByWordAction = false,
444  bool hasMoveCursorBackwardByWordAction = false,
445  bool hasSetSelectionAction = false,
446  bool hasCopyAction = false,
447  bool hasCutAction = false,
448  bool hasPasteAction = false,
449  bool hasDidGainAccessibilityFocusAction = false,
450  bool hasDidLoseAccessibilityFocusAction = false,
451  bool hasDismissAction = false,
452  // Custom actions and overrides
453  String onTapHint,
454  String onLongPressHint,
455  List<CustomSemanticsAction> customActions,
456  List<Matcher> children,
457}) {
458  final List<SemanticsFlag> flags = <SemanticsFlag>[];
459  if (hasCheckedState)
460    flags.add(SemanticsFlag.hasCheckedState);
461  if (isChecked)
462    flags.add(SemanticsFlag.isChecked);
463  if (isSelected)
464    flags.add(SemanticsFlag.isSelected);
465  if (isButton)
466    flags.add(SemanticsFlag.isButton);
467  if (isTextField)
468    flags.add(SemanticsFlag.isTextField);
469  if (isReadOnly)
470    flags.add(SemanticsFlag.isReadOnly);
471  if (isFocused)
472    flags.add(SemanticsFlag.isFocused);
473  if (hasEnabledState)
474    flags.add(SemanticsFlag.hasEnabledState);
475  if (isEnabled)
476    flags.add(SemanticsFlag.isEnabled);
477  if (isInMutuallyExclusiveGroup)
478    flags.add(SemanticsFlag.isInMutuallyExclusiveGroup);
479  if (isHeader)
480    flags.add(SemanticsFlag.isHeader);
481  if (isObscured)
482    flags.add(SemanticsFlag.isObscured);
483  if (isMultiline)
484    flags.add(SemanticsFlag.isMultiline);
485  if (namesRoute)
486    flags.add(SemanticsFlag.namesRoute);
487  if (scopesRoute)
488    flags.add(SemanticsFlag.scopesRoute);
489  if (isHidden)
490    flags.add(SemanticsFlag.isHidden);
491  if (isImage)
492    flags.add(SemanticsFlag.isImage);
493  if (isLiveRegion)
494    flags.add(SemanticsFlag.isLiveRegion);
495  if (hasToggledState)
496    flags.add(SemanticsFlag.hasToggledState);
497  if (isToggled)
498    flags.add(SemanticsFlag.isToggled);
499  if (hasImplicitScrolling)
500    flags.add(SemanticsFlag.hasImplicitScrolling);
501
502  final List<SemanticsAction> actions = <SemanticsAction>[];
503  if (hasTapAction)
504    actions.add(SemanticsAction.tap);
505  if (hasLongPressAction)
506    actions.add(SemanticsAction.longPress);
507  if (hasScrollLeftAction)
508    actions.add(SemanticsAction.scrollLeft);
509  if (hasScrollRightAction)
510    actions.add(SemanticsAction.scrollRight);
511  if (hasScrollUpAction)
512    actions.add(SemanticsAction.scrollUp);
513  if (hasScrollDownAction)
514    actions.add(SemanticsAction.scrollDown);
515  if (hasIncreaseAction)
516    actions.add(SemanticsAction.increase);
517  if (hasDecreaseAction)
518    actions.add(SemanticsAction.decrease);
519  if (hasShowOnScreenAction)
520    actions.add(SemanticsAction.showOnScreen);
521  if (hasMoveCursorForwardByCharacterAction)
522    actions.add(SemanticsAction.moveCursorForwardByCharacter);
523  if (hasMoveCursorBackwardByCharacterAction)
524    actions.add(SemanticsAction.moveCursorBackwardByCharacter);
525  if (hasSetSelectionAction)
526    actions.add(SemanticsAction.setSelection);
527  if (hasCopyAction)
528    actions.add(SemanticsAction.copy);
529  if (hasCutAction)
530    actions.add(SemanticsAction.cut);
531  if (hasPasteAction)
532    actions.add(SemanticsAction.paste);
533  if (hasDidGainAccessibilityFocusAction)
534    actions.add(SemanticsAction.didGainAccessibilityFocus);
535  if (hasDidLoseAccessibilityFocusAction)
536    actions.add(SemanticsAction.didLoseAccessibilityFocus);
537  if (customActions != null && customActions.isNotEmpty)
538    actions.add(SemanticsAction.customAction);
539  if (hasDismissAction)
540    actions.add(SemanticsAction.dismiss);
541  if (hasMoveCursorForwardByWordAction)
542    actions.add(SemanticsAction.moveCursorForwardByWord);
543  if (hasMoveCursorBackwardByWordAction)
544    actions.add(SemanticsAction.moveCursorBackwardByWord);
545  SemanticsHintOverrides hintOverrides;
546  if (onTapHint != null || onLongPressHint != null)
547    hintOverrides = SemanticsHintOverrides(
548      onTapHint: onTapHint,
549      onLongPressHint: onLongPressHint,
550    );
551
552  return _MatchesSemanticsData(
553    label: label,
554    hint: hint,
555    value: value,
556    increasedValue: increasedValue,
557    decreasedValue: decreasedValue,
558    actions: actions,
559    flags: flags,
560    textDirection: textDirection,
561    rect: rect,
562    size: size,
563    elevation: elevation,
564    thickness: thickness,
565    platformViewId: platformViewId,
566    customActions: customActions,
567    hintOverrides: hintOverrides,
568    children: children,
569  );
570}
571
572/// Asserts that the currently rendered widget meets the provided accessibility
573/// `guideline`.
574///
575/// This matcher requires the result to be awaited and for semantics to be
576/// enabled first.
577///
578/// ## Sample code
579///
580/// ```dart
581/// final SemanticsHandle handle = tester.ensureSemantics();
582/// await meetsGuideline(tester, meetsGuideline(textContrastGuideline));
583/// handle.dispose();
584/// ```
585///
586/// Supported accessibility guidelines:
587///
588///   * [androidTapTargetGuideline], for Android minimum tapable area guidelines.
589///   * [iOSTapTargetGuideline], for iOS minimum tapable area guidelines.
590///   * [textContrastGuideline], for WCAG minimum text contrast guidelines.
591AsyncMatcher meetsGuideline(AccessibilityGuideline guideline) {
592  return _MatchesAccessibilityGuideline(guideline);
593}
594
595/// The inverse matcher of [meetsGuideline].
596///
597/// This is needed because the [isNot] matcher does not compose with an
598/// [AsyncMatcher].
599AsyncMatcher doesNotMeetGuideline(AccessibilityGuideline guideline) {
600  return _DoesNotMatchAccessibilityGuideline(guideline);
601}
602
603class _FindsWidgetMatcher extends Matcher {
604  const _FindsWidgetMatcher(this.min, this.max);
605
606  final int min;
607  final int max;
608
609  @override
610  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
611    assert(min != null || max != null);
612    assert(min == null || max == null || min <= max);
613    matchState[Finder] = finder;
614    int count = 0;
615    final Iterator<Element> iterator = finder.evaluate().iterator;
616    if (min != null) {
617      while (count < min && iterator.moveNext())
618        count += 1;
619      if (count < min)
620        return false;
621    }
622    if (max != null) {
623      while (count <= max && iterator.moveNext())
624        count += 1;
625      if (count > max)
626        return false;
627    }
628    return true;
629  }
630
631  @override
632  Description describe(Description description) {
633    assert(min != null || max != null);
634    if (min == max) {
635      if (min == 1)
636        return description.add('exactly one matching node in the widget tree');
637      return description.add('exactly $min matching nodes in the widget tree');
638    }
639    if (min == null) {
640      if (max == 0)
641        return description.add('no matching nodes in the widget tree');
642      if (max == 1)
643        return description.add('at most one matching node in the widget tree');
644      return description.add('at most $max matching nodes in the widget tree');
645    }
646    if (max == null) {
647      if (min == 1)
648        return description.add('at least one matching node in the widget tree');
649      return description.add('at least $min matching nodes in the widget tree');
650    }
651    return description.add('between $min and $max matching nodes in the widget tree (inclusive)');
652  }
653
654  @override
655  Description describeMismatch(
656    dynamic item,
657    Description mismatchDescription,
658    Map<dynamic, dynamic> matchState,
659    bool verbose,
660  ) {
661    final Finder finder = matchState[Finder];
662    final int count = finder.evaluate().length;
663    if (count == 0) {
664      assert(min != null && min > 0);
665      if (min == 1 && max == 1)
666        return mismatchDescription.add('means none were found but one was expected');
667      return mismatchDescription.add('means none were found but some were expected');
668    }
669    if (max == 0) {
670      if (count == 1)
671        return mismatchDescription.add('means one was found but none were expected');
672      return mismatchDescription.add('means some were found but none were expected');
673    }
674    if (min != null && count < min)
675      return mismatchDescription.add('is not enough');
676    assert(max != null && count > min);
677    return mismatchDescription.add('is too many');
678  }
679}
680
681bool _hasAncestorMatching(Finder finder, bool predicate(Widget widget)) {
682  final Iterable<Element> nodes = finder.evaluate();
683  if (nodes.length != 1)
684    return false;
685  bool result = false;
686  nodes.single.visitAncestorElements((Element ancestor) {
687    if (predicate(ancestor.widget)) {
688      result = true;
689      return false;
690    }
691    return true;
692  });
693  return result;
694}
695
696bool _hasAncestorOfType(Finder finder, Type targetType) {
697  return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType);
698}
699
700class _IsOffstage extends Matcher {
701  const _IsOffstage();
702
703  @override
704  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
705    return _hasAncestorMatching(finder, (Widget widget) {
706      if (widget is Offstage)
707        return widget.offstage;
708      return false;
709    });
710  }
711
712  @override
713  Description describe(Description description) => description.add('offstage');
714}
715
716class _IsOnstage extends Matcher {
717  const _IsOnstage();
718
719  @override
720  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
721    final Iterable<Element> nodes = finder.evaluate();
722    if (nodes.length != 1)
723      return false;
724    bool result = true;
725    nodes.single.visitAncestorElements((Element ancestor) {
726      final Widget widget = ancestor.widget;
727      if (widget is Offstage) {
728        result = !widget.offstage;
729        return false;
730      }
731      return true;
732    });
733    return result;
734  }
735
736  @override
737  Description describe(Description description) => description.add('onstage');
738}
739
740class _IsInCard extends Matcher {
741  const _IsInCard();
742
743  @override
744  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => _hasAncestorOfType(finder, Card);
745
746  @override
747  Description describe(Description description) => description.add('in card');
748}
749
750class _IsNotInCard extends Matcher {
751  const _IsNotInCard();
752
753  @override
754  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => !_hasAncestorOfType(finder, Card);
755
756  @override
757  Description describe(Description description) => description.add('not in card');
758}
759
760class _HasOneLineDescription extends Matcher {
761  const _HasOneLineDescription();
762
763  @override
764  bool matches(Object object, Map<dynamic, dynamic> matchState) {
765    final String description = object.toString();
766    return description.isNotEmpty
767        && !description.contains('\n')
768        && !description.contains('Instance of ')
769        && description.trim() == description;
770  }
771
772  @override
773  Description describe(Description description) => description.add('one line description');
774}
775
776class _EqualsIgnoringHashCodes extends Matcher {
777  _EqualsIgnoringHashCodes(String v) : _value = _normalize(v);
778
779  final String _value;
780
781  static final Object _mismatchedValueKey = Object();
782
783  static String _normalize(String s) {
784    return s.replaceAll(RegExp(r'#[0-9a-fA-F]{5}'), '#00000');
785  }
786
787  @override
788  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
789    final String description = _normalize(object);
790    if (_value != description) {
791      matchState[_mismatchedValueKey] = description;
792      return false;
793    }
794    return true;
795  }
796
797  @override
798  Description describe(Description description) {
799    return description.add('multi line description equals $_value');
800  }
801
802  @override
803  Description describeMismatch(
804    dynamic item,
805    Description mismatchDescription,
806    Map<dynamic, dynamic> matchState,
807    bool verbose,
808  ) {
809    if (matchState.containsKey(_mismatchedValueKey)) {
810      final String actualValue = matchState[_mismatchedValueKey];
811      // Leading whitespace is added so that lines in the multi-line
812      // description returned by addDescriptionOf are all indented equally
813      // which makes the output easier to read for this case.
814      return mismatchDescription
815          .add('expected normalized value\n  ')
816          .addDescriptionOf(_value)
817          .add('\nbut got\n  ')
818          .addDescriptionOf(actualValue);
819    }
820    return mismatchDescription;
821  }
822}
823
824/// Returns true if [c] represents a whitespace code unit.
825bool _isWhitespace(int c) => (c <= 0x000D && c >= 0x0009) || c == 0x0020;
826
827/// Returns true if [c] represents a vertical line Unicode line art code unit.
828///
829/// See [https://en.wikipedia.org/wiki/Box-drawing_character]. This method only
830/// specifies vertical line art code units currently used by Flutter line art.
831/// There are other line art characters that technically also represent vertical
832/// lines.
833bool _isVerticalLine(int c) {
834  return c == 0x2502 || c == 0x2503 || c == 0x2551 || c == 0x254e;
835}
836
837/// Returns whether a [line] is all vertical tree connector characters.
838///
839/// Example vertical tree connector characters: `│ ║ ╎`.
840/// The last line of a text tree contains only vertical tree connector
841/// characters indicates a poorly formatted tree.
842bool _isAllTreeConnectorCharacters(String line) {
843  for (int i = 0; i < line.length; ++i) {
844    final int c = line.codeUnitAt(i);
845    if (!_isWhitespace(c) && !_isVerticalLine(c))
846      return false;
847  }
848  return true;
849}
850
851class _HasGoodToStringDeep extends Matcher {
852  const _HasGoodToStringDeep();
853
854  static final Object _toStringDeepErrorDescriptionKey = Object();
855
856  @override
857  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
858    final List<String> issues = <String>[];
859    String description = object.toStringDeep();
860    if (description.endsWith('\n')) {
861      // Trim off trailing \n as the remaining calculations assume
862      // the description does not end with a trailing \n.
863      description = description.substring(0, description.length - 1);
864    } else {
865      issues.add('Not terminated with a line break.');
866    }
867
868    if (description.trim() != description)
869      issues.add('Has trailing whitespace.');
870
871    final List<String> lines = description.split('\n');
872    if (lines.length < 2)
873      issues.add('Does not have multiple lines.');
874
875    if (description.contains('Instance of '))
876      issues.add('Contains text "Instance of ".');
877
878    for (int i = 0; i < lines.length; ++i) {
879      final String line = lines[i];
880      if (line.isEmpty)
881        issues.add('Line ${i+1} is empty.');
882
883      if (line.trimRight() != line)
884        issues.add('Line ${i+1} has trailing whitespace.');
885    }
886
887    if (_isAllTreeConnectorCharacters(lines.last))
888      issues.add('Last line is all tree connector characters.');
889
890    // If a toStringDeep method doesn't properly handle nested values that
891    // contain line breaks it can fail to add the required prefixes to all
892    // lined when toStringDeep is called specifying prefixes.
893    const String prefixLineOne    = 'PREFIX_LINE_ONE____';
894    const String prefixOtherLines = 'PREFIX_OTHER_LINES_';
895    final List<String> prefixIssues = <String>[];
896    String descriptionWithPrefixes =
897        object.toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines);
898    if (descriptionWithPrefixes.endsWith('\n')) {
899      // Trim off trailing \n as the remaining calculations assume
900      // the description does not end with a trailing \n.
901      descriptionWithPrefixes = descriptionWithPrefixes.substring(
902          0, descriptionWithPrefixes.length - 1);
903    }
904    final List<String> linesWithPrefixes = descriptionWithPrefixes.split('\n');
905    if (!linesWithPrefixes.first.startsWith(prefixLineOne))
906      prefixIssues.add('First line does not contain expected prefix.');
907
908    for (int i = 1; i < linesWithPrefixes.length; ++i) {
909      if (!linesWithPrefixes[i].startsWith(prefixOtherLines))
910        prefixIssues.add('Line ${i+1} does not contain the expected prefix.');
911    }
912
913    final StringBuffer errorDescription = StringBuffer();
914    if (issues.isNotEmpty) {
915      errorDescription.writeln('Bad toStringDeep():');
916      errorDescription.writeln(description);
917      errorDescription.writeAll(issues, '\n');
918    }
919
920    if (prefixIssues.isNotEmpty) {
921      errorDescription.writeln(
922          'Bad toStringDeep(prefixLineOne: "$prefixLineOne", prefixOtherLines: "$prefixOtherLines"):');
923      errorDescription.writeln(descriptionWithPrefixes);
924      errorDescription.writeAll(prefixIssues, '\n');
925    }
926
927    if (errorDescription.isNotEmpty) {
928      matchState[_toStringDeepErrorDescriptionKey] =
929          errorDescription.toString();
930      return false;
931    }
932    return true;
933  }
934
935  @override
936  Description describeMismatch(
937    dynamic item,
938    Description mismatchDescription,
939    Map<dynamic, dynamic> matchState,
940    bool verbose,
941  ) {
942    if (matchState.containsKey(_toStringDeepErrorDescriptionKey)) {
943      return mismatchDescription.add(
944          matchState[_toStringDeepErrorDescriptionKey]);
945    }
946    return mismatchDescription;
947  }
948
949  @override
950  Description describe(Description description) {
951    return description.add('multi line description');
952  }
953}
954
955/// Computes the distance between two values.
956///
957/// The distance should be a metric in a metric space (see
958/// https://en.wikipedia.org/wiki/Metric_space). Specifically, if `f` is a
959/// distance function then the following conditions should hold:
960///
961/// - f(a, b) >= 0
962/// - f(a, b) == 0 if and only if a == b
963/// - f(a, b) == f(b, a)
964/// - f(a, c) <= f(a, b) + f(b, c), known as triangle inequality
965///
966/// This makes it useful for comparing numbers, [Color]s, [Offset]s and other
967/// sets of value for which a metric space is defined.
968typedef DistanceFunction<T> = num Function(T a, T b);
969
970/// The type of a union of instances of [DistanceFunction<T>] for various types
971/// T.
972///
973/// This type is used to describe a collection of [DistanceFunction<T>]
974/// functions which have (potentially) unrelated argument types. Since the
975/// argument types of the functions may be unrelated, the only thing that the
976/// type system can statically assume about them is that they accept null (since
977/// all types in Dart are nullable).
978///
979/// Calling an instance of this type must either be done dynamically, or by
980/// first casting it to a [DistanceFunction<T>] for some concrete T.
981typedef AnyDistanceFunction = num Function(Null a, Null b);
982
983const Map<Type, AnyDistanceFunction> _kStandardDistanceFunctions = <Type, AnyDistanceFunction>{
984  Color: _maxComponentColorDistance,
985  HSVColor: _maxComponentHSVColorDistance,
986  HSLColor: _maxComponentHSLColorDistance,
987  Offset: _offsetDistance,
988  int: _intDistance,
989  double: _doubleDistance,
990  Rect: _rectDistance,
991  Size: _sizeDistance,
992};
993
994int _intDistance(int a, int b) => (b - a).abs();
995double _doubleDistance(double a, double b) => (b - a).abs();
996double _offsetDistance(Offset a, Offset b) => (b - a).distance;
997
998double _maxComponentColorDistance(Color a, Color b) {
999  int delta = math.max<int>((a.red - b.red).abs(), (a.green - b.green).abs());
1000  delta = math.max<int>(delta, (a.blue - b.blue).abs());
1001  delta = math.max<int>(delta, (a.alpha - b.alpha).abs());
1002  return delta.toDouble();
1003}
1004
1005// Compares hue by converting it to a 0.0 - 1.0 range, so that the comparison
1006// can be a similar error percentage per component.
1007double _maxComponentHSVColorDistance(HSVColor a, HSVColor b) {
1008  double delta = math.max<double>((a.saturation - b.saturation).abs(), (a.value - b.value).abs());
1009  delta = math.max<double>(delta, ((a.hue - b.hue) / 360.0).abs());
1010  return math.max<double>(delta, (a.alpha - b.alpha).abs());
1011}
1012
1013// Compares hue by converting it to a 0.0 - 1.0 range, so that the comparison
1014// can be a similar error percentage per component.
1015double _maxComponentHSLColorDistance(HSLColor a, HSLColor b) {
1016  double delta = math.max<double>((a.saturation - b.saturation).abs(), (a.lightness - b.lightness).abs());
1017  delta = math.max<double>(delta, ((a.hue - b.hue) / 360.0).abs());
1018  return math.max<double>(delta, (a.alpha - b.alpha).abs());
1019}
1020
1021double _rectDistance(Rect a, Rect b) {
1022  double delta = math.max<double>((a.left - b.left).abs(), (a.top - b.top).abs());
1023  delta = math.max<double>(delta, (a.right - b.right).abs());
1024  delta = math.max<double>(delta, (a.bottom - b.bottom).abs());
1025  return delta;
1026}
1027
1028double _sizeDistance(Size a, Size b) {
1029  final Offset delta = b - a;
1030  return delta.distance;
1031}
1032
1033/// Asserts that two values are within a certain distance from each other.
1034///
1035/// The distance is computed by a [DistanceFunction].
1036///
1037/// If `distanceFunction` is null, a standard distance function is used for the
1038/// `runtimeType` of the `from` argument. Standard functions are defined for
1039/// the following types:
1040///
1041///  * [Color], whose distance is the maximum component-wise delta.
1042///  * [Offset], whose distance is the Euclidean distance computed using the
1043///    method [Offset.distance].
1044///  * [Rect], whose distance is the maximum component-wise delta.
1045///  * [Size], whose distance is the [Offset.distance] of the offset computed as
1046///    the difference between two sizes.
1047///  * [int], whose distance is the absolute difference between two integers.
1048///  * [double], whose distance is the absolute difference between two doubles.
1049///
1050/// See also:
1051///
1052///  * [moreOrLessEquals], which is similar to this function, but specializes in
1053///    [double]s and has an optional `epsilon` parameter.
1054///  * [rectMoreOrLessEquals], which is similar to this function, but
1055///    specializes in [Rect]s and has an optional `epsilon` parameter.
1056///  * [closeTo], which specializes in numbers only.
1057Matcher within<T>({
1058  @required num distance,
1059  @required T from,
1060  DistanceFunction<T> distanceFunction,
1061}) {
1062  distanceFunction ??= _kStandardDistanceFunctions[from.runtimeType];
1063
1064  if (distanceFunction == null) {
1065    throw ArgumentError(
1066      'The specified distanceFunction was null, and a standard distance '
1067      'function was not found for type ${from.runtimeType} of the provided '
1068      '`from` argument.'
1069    );
1070  }
1071
1072  return _IsWithinDistance<T>(distanceFunction, from, distance);
1073}
1074
1075class _IsWithinDistance<T> extends Matcher {
1076  const _IsWithinDistance(this.distanceFunction, this.value, this.epsilon);
1077
1078  final DistanceFunction<T> distanceFunction;
1079  final T value;
1080  final num epsilon;
1081
1082  @override
1083  bool matches(Object object, Map<dynamic, dynamic> matchState) {
1084    if (object is! T)
1085      return false;
1086    if (object == value)
1087      return true;
1088    final T test = object;
1089    final num distance = distanceFunction(test, value);
1090    if (distance < 0) {
1091      throw ArgumentError(
1092        'Invalid distance function was used to compare a ${value.runtimeType} '
1093        'to a ${object.runtimeType}. The function must return a non-negative '
1094        'double value, but it returned $distance.'
1095      );
1096    }
1097    matchState['distance'] = distance;
1098    return distance <= epsilon;
1099  }
1100
1101  @override
1102  Description describe(Description description) => description.add('$value (±$epsilon)');
1103
1104  @override
1105  Description describeMismatch(
1106    Object object,
1107    Description mismatchDescription,
1108    Map<dynamic, dynamic> matchState,
1109    bool verbose,
1110  ) {
1111    mismatchDescription.add('was ${matchState['distance']} away from the desired value.');
1112    return mismatchDescription;
1113  }
1114}
1115
1116class _MoreOrLessEquals extends Matcher {
1117  const _MoreOrLessEquals(this.value, this.epsilon)
1118    : assert(epsilon >= 0);
1119
1120  final double value;
1121  final double epsilon;
1122
1123  @override
1124  bool matches(Object object, Map<dynamic, dynamic> matchState) {
1125    if (object is! double)
1126      return false;
1127    if (object == value)
1128      return true;
1129    final double test = object;
1130    return (test - value).abs() <= epsilon;
1131  }
1132
1133  @override
1134  Description describe(Description description) => description.add('$value (±$epsilon)');
1135
1136  @override
1137  Description describeMismatch(Object item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
1138    return super.describeMismatch(item, mismatchDescription, matchState, verbose)
1139      ..add('$item is not in the range of $value (±$epsilon).');
1140  }
1141}
1142
1143class _IsMethodCall extends Matcher {
1144  const _IsMethodCall(this.name, this.arguments);
1145
1146  final String name;
1147  final dynamic arguments;
1148
1149  @override
1150  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
1151    if (item is! MethodCall)
1152      return false;
1153    if (item.method != name)
1154      return false;
1155    return _deepEquals(item.arguments, arguments);
1156  }
1157
1158  bool _deepEquals(dynamic a, dynamic b) {
1159    if (a == b)
1160      return true;
1161    if (a is List)
1162      return b is List && _deepEqualsList(a, b);
1163    if (a is Map)
1164      return b is Map && _deepEqualsMap(a, b);
1165    return false;
1166  }
1167
1168  bool _deepEqualsList(List<dynamic> a, List<dynamic> b) {
1169    if (a.length != b.length)
1170      return false;
1171    for (int i = 0; i < a.length; i++) {
1172      if (!_deepEquals(a[i], b[i]))
1173        return false;
1174    }
1175    return true;
1176  }
1177
1178  bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) {
1179    if (a.length != b.length)
1180      return false;
1181    for (dynamic key in a.keys) {
1182      if (!b.containsKey(key) || !_deepEquals(a[key], b[key]))
1183        return false;
1184    }
1185    return true;
1186  }
1187
1188  @override
1189  Description describe(Description description) {
1190    return description
1191        .add('has method name: ').addDescriptionOf(name)
1192        .add(' with arguments: ').addDescriptionOf(arguments);
1193  }
1194}
1195
1196/// Asserts that a [Finder] locates a single object whose root RenderObject
1197/// is a [RenderClipRect] with no clipper set, or an equivalent
1198/// [RenderClipPath].
1199const Matcher clipsWithBoundingRect = _ClipsWithBoundingRect();
1200
1201/// Asserts that a [Finder] locates a single object whose root RenderObject is
1202/// not a [RenderClipRect], [RenderClipRRect], [RenderClipOval], or
1203/// [RenderClipPath].
1204const Matcher hasNoImmediateClip = _MatchAnythingExceptClip();
1205
1206/// Asserts that a [Finder] locates a single object whose root RenderObject
1207/// is a [RenderClipRRect] with no clipper set, and border radius equals to
1208/// [borderRadius], or an equivalent [RenderClipPath].
1209Matcher clipsWithBoundingRRect({ @required BorderRadius borderRadius }) {
1210  return _ClipsWithBoundingRRect(borderRadius: borderRadius);
1211}
1212
1213/// Asserts that a [Finder] locates a single object whose root RenderObject
1214/// is a [RenderClipPath] with a [ShapeBorderClipper] that clips to
1215/// [shape].
1216Matcher clipsWithShapeBorder({ @required ShapeBorder shape }) {
1217  return _ClipsWithShapeBorder(shape: shape);
1218}
1219
1220/// Asserts that a [Finder] locates a single object whose root RenderObject
1221/// is a [RenderPhysicalModel] or a [RenderPhysicalShape].
1222///
1223/// - If the render object is a [RenderPhysicalModel]
1224///    - If [shape] is non null asserts that [RenderPhysicalModel.shape] is equal to
1225///   [shape].
1226///    - If [borderRadius] is non null asserts that [RenderPhysicalModel.borderRadius] is equal to
1227///   [borderRadius].
1228///     - If [elevation] is non null asserts that [RenderPhysicalModel.elevation] is equal to
1229///   [elevation].
1230/// - If the render object is a [RenderPhysicalShape]
1231///    - If [borderRadius] is non null asserts that the shape is a rounded
1232///   rectangle with this radius.
1233///    - If [borderRadius] is null, asserts that the shape is equivalent to
1234///   [shape].
1235///    - If [elevation] is non null asserts that [RenderPhysicalModel.elevation] is equal to
1236///   [elevation].
1237Matcher rendersOnPhysicalModel({
1238  BoxShape shape,
1239  BorderRadius borderRadius,
1240  double elevation,
1241}) {
1242  return _RendersOnPhysicalModel(
1243    shape: shape,
1244    borderRadius: borderRadius,
1245    elevation: elevation,
1246  );
1247}
1248
1249/// Asserts that a [Finder] locates a single object whose root RenderObject
1250/// is [RenderPhysicalShape] that uses a [ShapeBorderClipper] that clips to
1251/// [shape] as its clipper.
1252/// If [elevation] is non null asserts that [RenderPhysicalShape.elevation] is
1253/// equal to [elevation].
1254Matcher rendersOnPhysicalShape({
1255  ShapeBorder shape,
1256  double elevation,
1257}) {
1258  return _RendersOnPhysicalShape(
1259    shape: shape,
1260    elevation: elevation,
1261  );
1262}
1263
1264abstract class _FailWithDescriptionMatcher extends Matcher {
1265  const _FailWithDescriptionMatcher();
1266
1267  bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
1268    matchState['failure'] = description;
1269    return false;
1270  }
1271
1272  @override
1273  Description describeMismatch(
1274    dynamic item,
1275    Description mismatchDescription,
1276    Map<dynamic, dynamic> matchState,
1277    bool verbose,
1278  ) {
1279    return mismatchDescription.add(matchState['failure']);
1280  }
1281}
1282
1283class _MatchAnythingExceptClip extends _FailWithDescriptionMatcher {
1284  const _MatchAnythingExceptClip();
1285
1286  @override
1287  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
1288    final Iterable<Element> nodes = finder.evaluate();
1289    if (nodes.length != 1)
1290      return failWithDescription(matchState, 'did not have a exactly one child element');
1291    final RenderObject renderObject = nodes.single.renderObject;
1292
1293    switch (renderObject.runtimeType) {
1294      case RenderClipPath:
1295      case RenderClipOval:
1296      case RenderClipRect:
1297      case RenderClipRRect:
1298        return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
1299      default:
1300        return true;
1301    }
1302  }
1303
1304  @override
1305  Description describe(Description description) {
1306    return description.add('does not have a clip as an immediate child');
1307  }
1308}
1309
1310abstract class _MatchRenderObject<M extends RenderObject, T extends RenderObject> extends _FailWithDescriptionMatcher {
1311  const _MatchRenderObject();
1312
1313  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, T renderObject);
1314  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, M renderObject);
1315
1316  @override
1317  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
1318    final Iterable<Element> nodes = finder.evaluate();
1319    if (nodes.length != 1)
1320      return failWithDescription(matchState, 'did not have a exactly one child element');
1321    final RenderObject renderObject = nodes.single.renderObject;
1322
1323    if (renderObject.runtimeType == T)
1324      return renderObjectMatchesT(matchState, renderObject);
1325
1326    if (renderObject.runtimeType == M)
1327      return renderObjectMatchesM(matchState, renderObject);
1328
1329    return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
1330  }
1331}
1332
1333class _RendersOnPhysicalModel extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
1334  const _RendersOnPhysicalModel({
1335    this.shape,
1336    this.borderRadius,
1337    this.elevation,
1338  });
1339
1340  final BoxShape shape;
1341  final BorderRadius borderRadius;
1342  final double elevation;
1343
1344  @override
1345  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) {
1346    if (shape != null && renderObject.shape != shape)
1347      return failWithDescription(matchState, 'had shape: ${renderObject.shape}');
1348
1349    if (borderRadius != null && renderObject.borderRadius != borderRadius)
1350      return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');
1351
1352    if (elevation != null && renderObject.elevation != elevation)
1353      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
1354
1355    return true;
1356  }
1357
1358  @override
1359  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
1360    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
1361      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1362    final ShapeBorderClipper shapeClipper = renderObject.clipper;
1363
1364    if (borderRadius != null && !assertRoundedRectangle(shapeClipper, borderRadius, matchState))
1365      return false;
1366
1367    if (
1368      borderRadius == null &&
1369      shape == BoxShape.rectangle &&
1370      !assertRoundedRectangle(shapeClipper, BorderRadius.zero, matchState)
1371    ) {
1372      return false;
1373    }
1374
1375    if (
1376      borderRadius == null &&
1377      shape == BoxShape.circle &&
1378      !assertCircle(shapeClipper, matchState)
1379    ) {
1380      return false;
1381    }
1382
1383    if (elevation != null && renderObject.elevation != elevation)
1384      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
1385
1386    return true;
1387  }
1388
1389  bool assertRoundedRectangle(ShapeBorderClipper shapeClipper, BorderRadius borderRadius, Map<dynamic, dynamic> matchState) {
1390    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder)
1391      return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
1392    final RoundedRectangleBorder border = shapeClipper.shape;
1393    if (border.borderRadius != borderRadius)
1394      return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
1395    return true;
1396  }
1397
1398  bool assertCircle(ShapeBorderClipper shapeClipper, Map<dynamic, dynamic> matchState) {
1399    if (shapeClipper.shape.runtimeType != CircleBorder)
1400      return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
1401    return true;
1402  }
1403
1404  @override
1405  Description describe(Description description) {
1406    description.add('renders on a physical model');
1407    if (shape != null)
1408      description.add(' with shape $shape');
1409    if (borderRadius != null)
1410      description.add(' with borderRadius $borderRadius');
1411    if (elevation != null)
1412      description.add(' with elevation $elevation');
1413    return description;
1414  }
1415}
1416
1417class _RendersOnPhysicalShape extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
1418  const _RendersOnPhysicalShape({
1419    this.shape,
1420    this.elevation,
1421  });
1422
1423  final ShapeBorder shape;
1424  final double elevation;
1425
1426  @override
1427  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
1428    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
1429      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1430    final ShapeBorderClipper shapeClipper = renderObject.clipper;
1431
1432    if (shapeClipper.shape != shape)
1433      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1434
1435    if (elevation != null && renderObject.elevation != elevation)
1436      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
1437
1438    return true;
1439  }
1440
1441  @override
1442  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) {
1443    return false;
1444  }
1445
1446  @override
1447  Description describe(Description description) {
1448    description.add('renders on a physical model with shape $shape');
1449    if (elevation != null)
1450      description.add(' with elevation $elevation');
1451    return description;
1452  }
1453}
1454
1455class _ClipsWithBoundingRect extends _MatchRenderObject<RenderClipPath, RenderClipRect> {
1456  const _ClipsWithBoundingRect();
1457
1458  @override
1459  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRect renderObject) {
1460    if (renderObject.clipper != null)
1461      return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');
1462    return true;
1463  }
1464
1465  @override
1466  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
1467    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
1468      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1469    final ShapeBorderClipper shapeClipper = renderObject.clipper;
1470    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder)
1471      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1472    final RoundedRectangleBorder border = shapeClipper.shape;
1473    if (border.borderRadius != BorderRadius.zero)
1474      return failWithDescription(matchState, 'borderRadius was: ${border.borderRadius}');
1475    return true;
1476  }
1477
1478  @override
1479  Description describe(Description description) =>
1480    description.add('clips with bounding rectangle');
1481}
1482
1483class _ClipsWithBoundingRRect extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
1484  const _ClipsWithBoundingRRect({@required this.borderRadius});
1485
1486  final BorderRadius borderRadius;
1487
1488
1489  @override
1490  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) {
1491    if (renderObject.clipper != null)
1492      return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');
1493
1494    if (renderObject.borderRadius != borderRadius)
1495      return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');
1496
1497    return true;
1498  }
1499
1500  @override
1501  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
1502    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
1503      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1504    final ShapeBorderClipper shapeClipper = renderObject.clipper;
1505    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder)
1506      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1507    final RoundedRectangleBorder border = shapeClipper.shape;
1508    if (border.borderRadius != borderRadius)
1509      return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
1510    return true;
1511  }
1512
1513  @override
1514  Description describe(Description description) =>
1515    description.add('clips with bounding rounded rectangle with borderRadius: $borderRadius');
1516}
1517
1518class _ClipsWithShapeBorder extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
1519  const _ClipsWithShapeBorder({@required this.shape});
1520
1521  final ShapeBorder shape;
1522
1523  @override
1524  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
1525    if (renderObject.clipper.runtimeType != ShapeBorderClipper)
1526      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1527    final ShapeBorderClipper shapeClipper = renderObject.clipper;
1528    if (shapeClipper.shape != shape)
1529      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1530    return true;
1531  }
1532
1533  @override
1534  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) {
1535    return false;
1536  }
1537
1538
1539  @override
1540  Description describe(Description description) =>
1541    description.add('clips with shape: $shape');
1542}
1543
1544class _CoversSameAreaAs extends Matcher {
1545  _CoversSameAreaAs(
1546    this.expectedPath, {
1547    @required this.areaToCompare,
1548    this.sampleSize = 20,
1549  }) : maxHorizontalNoise = areaToCompare.width / sampleSize,
1550       maxVerticalNoise = areaToCompare.height / sampleSize {
1551    // Use a fixed random seed to make sure tests are deterministic.
1552    random = math.Random(1);
1553  }
1554
1555  final Path expectedPath;
1556  final Rect areaToCompare;
1557  final int sampleSize;
1558  final double maxHorizontalNoise;
1559  final double maxVerticalNoise;
1560  math.Random random;
1561
1562  @override
1563  bool matches(covariant Path actualPath, Map<dynamic, dynamic> matchState) {
1564    for (int i = 0; i < sampleSize; i += 1) {
1565      for (int j = 0; j < sampleSize; j += 1) {
1566        final Offset offset = Offset(
1567          i * (areaToCompare.width / sampleSize),
1568          j * (areaToCompare.height / sampleSize),
1569        );
1570
1571        if (!_samplePoint(matchState, actualPath, offset))
1572          return false;
1573
1574        final Offset noise = Offset(
1575          maxHorizontalNoise * random.nextDouble(),
1576          maxVerticalNoise * random.nextDouble(),
1577        );
1578
1579        if (!_samplePoint(matchState, actualPath, offset + noise))
1580          return false;
1581      }
1582    }
1583    return true;
1584  }
1585
1586  bool _samplePoint(Map<dynamic, dynamic> matchState, Path actualPath, Offset offset) {
1587    if (expectedPath.contains(offset) == actualPath.contains(offset))
1588      return true;
1589
1590    if (actualPath.contains(offset))
1591      return failWithDescription(matchState, '$offset is contained in the actual path but not in the expected path');
1592    else
1593      return failWithDescription(matchState, '$offset is contained in the expected path but not in the actual path');
1594  }
1595
1596  bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
1597    matchState['failure'] = description;
1598    return false;
1599  }
1600
1601  @override
1602  Description describeMismatch(
1603    dynamic item,
1604    Description mismatchDescription,
1605    Map<dynamic, dynamic> matchState,
1606    bool verbose,
1607  ) {
1608    return mismatchDescription.add(matchState['failure']);
1609  }
1610
1611  @override
1612  Description describe(Description description) =>
1613    description.add('covers expected area and only expected area');
1614}
1615
1616Future<ui.Image> _captureImage(Element element) {
1617  RenderObject renderObject = element.renderObject;
1618  while (!renderObject.isRepaintBoundary) {
1619    renderObject = renderObject.parent;
1620    assert(renderObject != null);
1621  }
1622  assert(!renderObject.debugNeedsPaint);
1623  final OffsetLayer layer = renderObject.debugLayer;
1624  return layer.toImage(renderObject.paintBounds);
1625}
1626
1627int _countDifferentPixels(Uint8List imageA, Uint8List imageB) {
1628  assert(imageA.length == imageB.length);
1629  int delta = 0;
1630  for (int i = 0; i < imageA.length; i+=4) {
1631    if (imageA[i] != imageB[i] ||
1632      imageA[i+1] != imageB[i+1] ||
1633      imageA[i+2] != imageB[i+2] ||
1634      imageA[i+3] != imageB[i+3]) {
1635      delta++;
1636    }
1637  }
1638  return delta;
1639}
1640
1641class _MatchesReferenceImage extends AsyncMatcher {
1642  const _MatchesReferenceImage(this.referenceImage);
1643
1644  final ui.Image referenceImage;
1645
1646  @override
1647  Future<String> matchAsync(dynamic item) async {
1648    Future<ui.Image> imageFuture;
1649    if (item is Future<ui.Image>) {
1650      imageFuture = item;
1651    } else if (item is ui.Image) {
1652      imageFuture = Future<ui.Image>.value(item);
1653    } else {
1654      final Finder finder = item;
1655      final Iterable<Element> elements = finder.evaluate();
1656      if (elements.isEmpty) {
1657        return 'could not be rendered because no widget was found';
1658      } else if (elements.length > 1) {
1659        return 'matched too many widgets';
1660      }
1661      imageFuture = _captureImage(elements.single);
1662    }
1663
1664    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
1665    return binding.runAsync<String>(() async {
1666      final ui.Image image = await imageFuture;
1667      final ByteData bytes = await image.toByteData();
1668      if (bytes == null)
1669        return 'could not be encoded.';
1670
1671      final ByteData referenceBytes = await referenceImage.toByteData();
1672      if (referenceBytes == null)
1673        return 'could not have its reference image encoded.';
1674
1675      if (referenceImage.height != image.height || referenceImage.width != image.width)
1676        return 'does not match as width or height do not match. $image != $referenceImage';
1677
1678      final int countDifferentPixels = _countDifferentPixels(
1679        Uint8List.view(bytes.buffer),
1680        Uint8List.view(referenceBytes.buffer),
1681      );
1682      return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
1683    }, additionalTime: const Duration(minutes: 1));
1684  }
1685
1686  @override
1687  Description describe(Description description) {
1688    return description.add('rasterized image matches that of a $referenceImage reference image');
1689  }
1690}
1691
1692class _MatchesGoldenFile extends AsyncMatcher {
1693  const _MatchesGoldenFile(this.key, this.version);
1694
1695  _MatchesGoldenFile.forStringPath(String path, this.version) : key = Uri.parse(path);
1696
1697  final Uri key;
1698  final int version;
1699
1700  @override
1701  Future<String> matchAsync(dynamic item) async {
1702    Future<ui.Image> imageFuture;
1703    if (item is Future<ui.Image>) {
1704      imageFuture = item;
1705    } else if (item is ui.Image) {
1706      imageFuture = Future<ui.Image>.value(item);
1707    } else {
1708      final Finder finder = item;
1709      final Iterable<Element> elements = finder.evaluate();
1710      if (elements.isEmpty) {
1711        return 'could not be rendered because no widget was found';
1712      } else if (elements.length > 1) {
1713        return 'matched too many widgets';
1714      }
1715      imageFuture = _captureImage(elements.single);
1716    }
1717
1718    final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
1719
1720    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
1721    return binding.runAsync<String>(() async {
1722      final ui.Image image = await imageFuture;
1723      final ByteData bytes = await image.toByteData(format: ui.ImageByteFormat.png);
1724      if (bytes == null)
1725        return 'could not encode screenshot.';
1726      if (autoUpdateGoldenFiles) {
1727        await goldenFileComparator.update(testNameUri, bytes.buffer.asUint8List());
1728        return null;
1729      }
1730      try {
1731        final bool success = await goldenFileComparator.compare(bytes.buffer.asUint8List(), testNameUri);
1732        return success ? null : 'does not match';
1733      } on TestFailure catch (ex) {
1734        return ex.message;
1735      }
1736    }, additionalTime: const Duration(minutes: 1));
1737  }
1738
1739  @override
1740  Description describe(Description description) {
1741    final Uri testNameUri = goldenFileComparator.getTestUri(key, version);
1742    return description.add('one widget whose rasterized image matches golden image "$testNameUri"');
1743  }
1744}
1745
1746class _MatchesSemanticsData extends Matcher {
1747  _MatchesSemanticsData({
1748    this.label,
1749    this.value,
1750    this.increasedValue,
1751    this.decreasedValue,
1752    this.hint,
1753    this.flags,
1754    this.actions,
1755    this.textDirection,
1756    this.rect,
1757    this.size,
1758    this.elevation,
1759    this.thickness,
1760    this.platformViewId,
1761    this.customActions,
1762    this.hintOverrides,
1763    this.children,
1764  });
1765
1766  final String label;
1767  final String value;
1768  final String hint;
1769  final String increasedValue;
1770  final String decreasedValue;
1771  final SemanticsHintOverrides hintOverrides;
1772  final List<SemanticsAction> actions;
1773  final List<CustomSemanticsAction> customActions;
1774  final List<SemanticsFlag> flags;
1775  final TextDirection textDirection;
1776  final Rect rect;
1777  final Size size;
1778  final double elevation;
1779  final double thickness;
1780  final int platformViewId;
1781  final List<Matcher> children;
1782
1783  @override
1784  Description describe(Description description) {
1785    description.add('has semantics');
1786    if (label != null)
1787      description.add(' with label: $label');
1788    if (value != null)
1789      description.add(' with value: $value');
1790    if (hint != null)
1791      description.add(' with hint: $hint');
1792    if (increasedValue != null)
1793      description.add(' with increasedValue: $increasedValue ');
1794    if (decreasedValue != null)
1795      description.add(' with decreasedValue: $decreasedValue ');
1796    if (actions != null)
1797      description.add(' with actions: ').addDescriptionOf(actions);
1798    if (flags != null)
1799      description.add(' with flags: ').addDescriptionOf(flags);
1800    if (textDirection != null)
1801      description.add(' with textDirection: $textDirection ');
1802    if (rect != null)
1803      description.add(' with rect: $rect');
1804    if (size != null)
1805      description.add(' with size: $size');
1806    if (elevation != null)
1807      description.add(' with elevation: $elevation');
1808    if (thickness != null)
1809      description.add(' with thickness: $thickness');
1810    if (platformViewId != null)
1811      description.add(' with platformViewId: $platformViewId');
1812    if (customActions != null)
1813      description.add(' with custom actions: $customActions');
1814    if (hintOverrides != null)
1815      description.add(' with custom hints: $hintOverrides');
1816    if (children != null) {
1817      description.add(' with children:\n');
1818      for (_MatchesSemanticsData child in children)
1819        child.describe(description);
1820    }
1821    return description;
1822  }
1823
1824
1825  @override
1826  bool matches(dynamic node, Map<dynamic, dynamic> matchState) {
1827    // TODO(jonahwilliams): remove dynamic once we have removed getSemanticsData.
1828    if (node == null)
1829      return failWithDescription(matchState, 'No SemanticsData provided. '
1830        'Maybe you forgot to enable semantics?');
1831    final SemanticsData data = node is SemanticsNode ? node.getSemanticsData() : node;
1832    if (label != null && label != data.label)
1833      return failWithDescription(matchState, 'label was: ${data.label}');
1834    if (hint != null && hint != data.hint)
1835      return failWithDescription(matchState, 'hint was: ${data.hint}');
1836    if (value != null && value != data.value)
1837      return failWithDescription(matchState, 'value was: ${data.value}');
1838    if (increasedValue != null && increasedValue != data.increasedValue)
1839      return failWithDescription(matchState, 'increasedValue was: ${data.increasedValue}');
1840    if (decreasedValue != null && decreasedValue != data.decreasedValue)
1841      return failWithDescription(matchState, 'decreasedValue was: ${data.decreasedValue}');
1842    if (textDirection != null && textDirection != data.textDirection)
1843      return failWithDescription(matchState, 'textDirection was: $textDirection');
1844    if (rect != null && rect != data.rect)
1845      return failWithDescription(matchState, 'rect was: ${data.rect}');
1846    if (size != null && size != data.rect.size)
1847      return failWithDescription(matchState, 'size was: ${data.rect.size}');
1848    if (elevation != null && elevation != data.elevation)
1849      return failWithDescription(matchState, 'elevation was: ${data.elevation}');
1850    if (thickness != null && thickness != data.thickness)
1851      return failWithDescription(matchState, 'thickness was: ${data.thickness}');
1852    if (platformViewId != null && platformViewId != data.platformViewId)
1853      return failWithDescription(matchState, 'platformViewId was: ${data.platformViewId}');
1854    if (actions != null) {
1855      int actionBits = 0;
1856      for (SemanticsAction action in actions)
1857        actionBits |= action.index;
1858      if (actionBits != data.actions) {
1859        final List<String> actionSummary = <String>[];
1860        for (SemanticsAction action in SemanticsAction.values.values) {
1861          if ((data.actions & action.index) != 0)
1862            actionSummary.add(describeEnum(action));
1863        }
1864        return failWithDescription(matchState, 'actions were: $actionSummary');
1865      }
1866    }
1867    if (customActions != null || hintOverrides != null) {
1868      final List<CustomSemanticsAction> providedCustomActions = data.customSemanticsActionIds.map((int id) {
1869        return CustomSemanticsAction.getAction(id);
1870      }).toList();
1871      final List<CustomSemanticsAction> expectedCustomActions = customActions?.toList() ?? <CustomSemanticsAction>[];
1872      if (hintOverrides?.onTapHint != null)
1873        expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides.onTapHint, action: SemanticsAction.tap));
1874      if (hintOverrides?.onLongPressHint != null)
1875        expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides.onLongPressHint, action: SemanticsAction.longPress));
1876      if (expectedCustomActions.length != providedCustomActions.length)
1877        return failWithDescription(matchState, 'custom actions where: $providedCustomActions');
1878      int sortActions(CustomSemanticsAction left, CustomSemanticsAction right) {
1879        return CustomSemanticsAction.getIdentifier(left) - CustomSemanticsAction.getIdentifier(right);
1880      }
1881      expectedCustomActions.sort(sortActions);
1882      providedCustomActions.sort(sortActions);
1883      for (int i = 0; i < expectedCustomActions.length; i++) {
1884        if (expectedCustomActions[i] != providedCustomActions[i])
1885          return failWithDescription(matchState, 'custom actions where: $providedCustomActions');
1886      }
1887    }
1888    if (flags != null) {
1889      int flagBits = 0;
1890      for (SemanticsFlag flag in flags)
1891        flagBits |= flag.index;
1892      if (flagBits != data.flags) {
1893        final List<String> flagSummary = <String>[];
1894        for (SemanticsFlag flag in SemanticsFlag.values.values) {
1895          if ((data.flags & flag.index) != 0)
1896            flagSummary.add(describeEnum(flag));
1897        }
1898        return failWithDescription(matchState, 'flags were: $flagSummary');
1899      }
1900    }
1901    bool allMatched = true;
1902    if (children != null) {
1903      int i = 0;
1904      node.visitChildren((SemanticsNode child) {
1905        allMatched = children[i].matches(child, matchState) && allMatched;
1906        i += 1;
1907        return allMatched;
1908      });
1909    }
1910    return allMatched;
1911  }
1912
1913  bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
1914    matchState['failure'] = description;
1915    return false;
1916  }
1917
1918  @override
1919  Description describeMismatch(
1920    dynamic item,
1921    Description mismatchDescription,
1922    Map<dynamic, dynamic> matchState,
1923    bool verbose,
1924  ) {
1925    return mismatchDescription.add(matchState['failure']);
1926  }
1927}
1928
1929class _MatchesAccessibilityGuideline extends AsyncMatcher {
1930  _MatchesAccessibilityGuideline(this.guideline);
1931
1932  final AccessibilityGuideline guideline;
1933
1934  @override
1935  Description describe(Description description) {
1936    return description.add(guideline.description);
1937  }
1938
1939  @override
1940  Future<String> matchAsync(covariant WidgetTester tester) async {
1941    final Evaluation result = await guideline.evaluate(tester);
1942    if (result.passed)
1943      return null;
1944    return result.reason;
1945  }
1946}
1947
1948class _DoesNotMatchAccessibilityGuideline extends AsyncMatcher {
1949  _DoesNotMatchAccessibilityGuideline(this.guideline);
1950
1951  final AccessibilityGuideline guideline;
1952
1953  @override
1954  Description describe(Description description) {
1955    return description.add('Does not ' + guideline.description);
1956  }
1957
1958  @override
1959  Future<String> matchAsync(covariant WidgetTester tester) async {
1960    final Evaluation result = await guideline.evaluate(tester);
1961    if (result.passed)
1962      return 'Failed';
1963    return null;
1964  }
1965}
1966