matchers.dart 75.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:math' as math;
6
import 'dart:ui' as ui;
7

8
import 'package:flutter/foundation.dart';
9
import 'package:flutter/material.dart' show Card;
10
import 'package:flutter/rendering.dart';
11
import 'package:flutter/services.dart';
12
import 'package:flutter/widgets.dart';
13
import 'package:test_api/src/expect/async_matcher.dart'; // ignore: implementation_imports
14 15 16
// This import is discouraged in general, but we need it to implement flutter_test.
// ignore: deprecated_member_use
import 'package:test_api/test_api.dart';
17

18
import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage;
19
import 'accessibility.dart';
20
import 'binding.dart';
21
import 'finders.dart';
22
import 'goldens.dart';
23
import 'widget_tester.dart' show WidgetTester;
24 25 26

/// Asserts that the [Finder] matches no widgets in the widget tree.
///
27
/// ## Sample code
28
///
29 30 31 32 33 34 35
/// ```dart
/// expect(find.text('Save'), findsNothing);
/// ```
///
/// See also:
///
///  * [findsWidgets], when you want the finder to find one or more widgets.
36
///  * [findsOneWidget], when you want the finder to find exactly one widget.
37
///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
38
///  * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
39
const Matcher findsNothing = _FindsWidgetMatcher(null, 0);
40 41 42

/// Asserts that the [Finder] locates at least one widget in the widget tree.
///
43 44 45 46 47 48 49
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsWidgets);
/// ```
///
/// See also:
50
///
51
///  * [findsNothing], when you want the finder to not find anything.
52
///  * [findsOneWidget], when you want the finder to find exactly one widget.
53
///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
54
///  * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
55
const Matcher findsWidgets = _FindsWidgetMatcher(1, null);
56 57 58

/// Asserts that the [Finder] locates at exactly one widget in the widget tree.
///
59
/// ## Sample code
60
///
61 62 63 64 65 66 67 68 69
/// ```dart
/// expect(find.text('Save'), findsOneWidget);
/// ```
///
/// See also:
///
///  * [findsNothing], when you want the finder to not find anything.
///  * [findsWidgets], when you want the finder to find one or more widgets.
///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
70
///  * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
71
const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1);
72 73 74

/// Asserts that the [Finder] locates the specified number of widgets in the widget tree.
///
75 76 77 78 79 80 81
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsNWidgets(2));
/// ```
///
/// See also:
82
///
83 84
///  * [findsNothing], when you want the finder to not find anything.
///  * [findsWidgets], when you want the finder to find one or more widgets.
85
///  * [findsOneWidget], when you want the finder to find exactly one widget.
86
///  * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
87
Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n);
88

89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
/// Asserts that the [Finder] locates at least a number of widgets in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsAtLeastNWidgets(2));
/// ```
///
/// See also:
///
///  * [findsNothing], when you want the finder to not find anything.
///  * [findsWidgets], when you want the finder to find one or more widgets.
///  * [findsOneWidget], when you want the finder to find exactly one widget.
///  * [findsNWidgets], when you want the finder to find a specific number of widgets.
Matcher findsAtLeastNWidgets(int n) => _FindsWidgetMatcher(n, null);

105
/// Asserts that the [Finder] locates a single widget that has at
106 107 108 109 110
/// least one [Offstage] widget ancestor.
///
/// It's important to use a full finder, since by default finders exclude
/// offstage widgets.
///
111
/// ## Sample code
112
///
113 114 115 116 117 118
/// ```dart
/// expect(find.text('Save', skipOffstage: false), isOffstage);
/// ```
///
/// See also:
///
119
///  * [isOnstage], the opposite.
120
const Matcher isOffstage = _IsOffstage();
121

122
/// Asserts that the [Finder] locates a single widget that has no
123
/// [Offstage] widget ancestors.
124 125 126
///
/// See also:
///
127
///  * [isOffstage], the opposite.
128
const Matcher isOnstage = _IsOnstage();
129

130
/// Asserts that the [Finder] locates a single widget that has at
131
/// least one [Card] widget ancestor.
132 133 134 135
///
/// See also:
///
///  * [isNotInCard], the opposite.
136
const Matcher isInCard = _IsInCard();
137

138
/// Asserts that the [Finder] locates a single widget that has no
139
/// [Card] widget ancestors.
140 141 142 143 144 145
///
/// This is equivalent to `isNot(isInCard)`.
///
/// See also:
///
///  * [isInCard], the opposite.
146
const Matcher isNotInCard = _IsNotInCard();
147

148 149 150 151 152 153
/// Asserts that the object represents the same color as [color] when used to paint.
///
/// Specifically this matcher checks the object is of type [Color] and its [Color.value]
/// equals to that of the given [color].
Matcher isSameColorAs(Color color) => _ColorMatcher(targetColor: color);

154
/// Asserts that an object's toString() is a plausible one-line description.
155 156
///
/// Specifically, this matcher checks that the string does not contains newline
157 158
/// characters, and does not have leading or trailing whitespace, is not
/// empty, and does not contain the default `Instance of ...` string.
159
const Matcher hasOneLineDescription = _HasOneLineDescription();
160

161
/// Asserts that an object's toStringDeep() is a plausible multiline
162 163 164 165 166 167 168 169 170 171 172 173 174 175
/// description.
///
/// Specifically, this matcher checks that an object's
/// `toStringDeep(prefixLineOne, prefixOtherLines)`:
///
///  * Does not have leading or trailing whitespace.
///  * Does not contain the default `Instance of ...` string.
///  * The last line has characters other than tree connector characters and
///    whitespace. For example: the line ` │ ║ ╎` has only tree connector
///    characters and whitespace.
///  * Does not contain lines with trailing white space.
///  * Has multiple lines.
///  * The first line starts with `prefixLineOne`
///  * All subsequent lines start with `prefixOtherLines`.
176
const Matcher hasAGoodToStringDeep = _HasGoodToStringDeep();
177

178
/// A matcher for functions that throw [FlutterError].
179
///
Dan Field's avatar
Dan Field committed
180
/// This is equivalent to `throwsA(isA<FlutterError>())`.
181
///
182 183 184
/// If you are trying to test whether a call to [WidgetTester.pumpWidget]
/// results in a [FlutterError], see [TestWidgetsFlutterBinding.takeException].
///
185 186 187
/// See also:
///
///  * [throwsAssertionError], to test if a function throws any [AssertionError].
188
///  * [throwsArgumentError], to test if a functions throws an [ArgumentError].
189
///  * [isFlutterError], to test if any object is a [FlutterError].
190
final Matcher throwsFlutterError = throwsA(isFlutterError);
191 192

/// A matcher for functions that throw [AssertionError].
193
///
Dan Field's avatar
Dan Field committed
194
/// This is equivalent to `throwsA(isA<AssertionError>())`.
195
///
196 197 198 199
/// If you are trying to test whether a call to [WidgetTester.pumpWidget]
/// results in an [AssertionError], see
/// [TestWidgetsFlutterBinding.takeException].
///
200 201 202
/// See also:
///
///  * [throwsFlutterError], to test if a function throws a [FlutterError].
203
///  * [throwsArgumentError], to test if a functions throws an [ArgumentError].
204
///  * [isAssertionError], to test if any object is any kind of [AssertionError].
205
final Matcher throwsAssertionError = throwsA(isAssertionError);
206 207

/// A matcher for [FlutterError].
208
///
209
/// This is equivalent to `isInstanceOf<FlutterError>()`.
210 211 212 213 214
///
/// See also:
///
///  * [throwsFlutterError], to test if a function throws a [FlutterError].
///  * [isAssertionError], to test if any object is any kind of [AssertionError].
215
final TypeMatcher<FlutterError> isFlutterError = isA<FlutterError>();
216

217
/// A matcher for [AssertionError].
218
///
219
/// This is equivalent to `isInstanceOf<AssertionError>()`.
220 221 222 223 224
///
/// See also:
///
///  * [throwsAssertionError], to test if a function throws any [AssertionError].
///  * [isFlutterError], to test if any object is a [FlutterError].
225
final TypeMatcher<AssertionError> isAssertionError = isA<AssertionError>();
226 227

/// A matcher that compares the type of the actual value to the type argument T.
228 229 230
///
/// This is identical to [isA] and is included for backwards compatibility.
TypeMatcher<T> isInstanceOf<T>() => isA<T>();
231

232 233
/// Asserts that two [double]s are equal, within some tolerated error.
///
234
/// {@template flutter.flutter_test.moreOrLessEquals}
235
/// Two values are considered equal if the difference between them is within
236 237 238 239 240
/// [precisionErrorTolerance] of the larger one. This is an arbitrary value
/// which can be adjusted using the `epsilon` argument. This matcher is intended
/// to compare floating point numbers that are the result of different sequences
/// of operations, such that they may have accumulated slightly different
/// errors.
241
/// {@endtemplate}
242 243 244 245 246
///
/// See also:
///
///  * [closeTo], which is identical except that the epsilon argument is
///    required and not named.
247
///  * [inInclusiveRange], which matches if the argument is in a specified
248
///    range.
249 250
///  * [rectMoreOrLessEquals] and [offsetMoreOrLessEquals], which do something
///    similar but for [Rect]s and [Offset]s respectively.
251
Matcher moreOrLessEquals(double value, { double epsilon = precisionErrorTolerance }) {
252
  return _MoreOrLessEquals(value, epsilon);
253 254
}

255 256
/// Asserts that two [Rect]s are equal, within some tolerated error.
///
257
/// {@macro flutter.flutter_test.moreOrLessEquals}
258 259 260 261
///
/// See also:
///
///  * [moreOrLessEquals], which is for [double]s.
262
///  * [offsetMoreOrLessEquals], which is for [Offset]s.
263 264
///  * [within], which offers a generic version of this functionality that can
///    be used to match [Rect]s as well as other types.
265
Matcher rectMoreOrLessEquals(Rect value, { double epsilon = precisionErrorTolerance }) {
266 267 268
  return _IsWithinDistance<Rect>(_rectDistance, value, epsilon);
}

269 270
/// Asserts that two [Offset]s are equal, within some tolerated error.
///
271
/// {@macro flutter.flutter_test.moreOrLessEquals}
272 273 274 275 276 277 278 279 280 281 282
///
/// See also:
///
///  * [moreOrLessEquals], which is for [double]s.
///  * [rectMoreOrLessEquals], which is for [Rect]s.
///  * [within], which offers a generic version of this functionality that can
///    be used to match [Offset]s as well as other types.
Matcher offsetMoreOrLessEquals(Offset value, { double epsilon = precisionErrorTolerance }) {
  return _IsWithinDistance<Offset>(_offsetDistance, value, epsilon);
}

283 284 285
/// Asserts that two [String]s are equal after normalizing likely hash codes.
///
/// A `#` followed by 5 hexadecimal digits is assumed to be a short hash code
286
/// and is normalized to `#00000`.
287 288 289 290
///
/// See Also:
///
///  * [describeIdentity], a method that generates short descriptions of objects
291
///    with ids that match the pattern `#[0-9a-f]{5}`.
292 293
///  * [shortHash], a method that generates a 5 character long hexadecimal
///    [String] based on [Object.hashCode].
294
///  * [DiagnosticableTree.toStringDeep], a method that returns a [String]
295 296
///    typically containing multiple hash codes.
Matcher equalsIgnoringHashCodes(String value) {
297
  return _EqualsIgnoringHashCodes(value);
298 299
}

300 301 302 303
/// A matcher for [MethodCall]s, asserting that it has the specified
/// method [name] and [arguments].
///
/// Arguments checking implements deep equality for [List] and [Map] types.
304
Matcher isMethodCall(String name, { required dynamic arguments }) {
305
  return _IsMethodCall(name, arguments);
306 307
}

308 309 310 311 312 313 314 315 316
/// Asserts that 2 paths cover the same area by sampling multiple points.
///
/// Samples at least [sampleSize]^2 points inside [areaToCompare], and asserts
/// that the [Path.contains] method returns the same value for each of the
/// points for both paths.
///
/// When using this matcher you typically want to use a rectangle larger than
/// the area you expect to paint in for [areaToCompare] to catch errors where
/// the path draws outside the expected area.
317
Matcher coversSameAreaAs(Path expectedPath, { required Rect areaToCompare, int sampleSize = 20 })
318
  => _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);
319

320
/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the
321
/// golden image file identified by [key], with an optional [version] number.
322 323 324
///
/// For the case of a [Finder], the [Finder] must match exactly one widget and
/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
325 326
/// treated as the image for the widget. As such, you may choose to wrap a test
/// widget in a [RepaintBoundary] to specify a particular focus for the test.
327
///
328
/// The [key] may be either a [Uri] or a [String] representation of a URL.
329
///
330
/// The [version] is a number that can be used to differentiate historical
331
/// golden files. This parameter is optional.
332
///
333 334 335 336
/// This is an asynchronous matcher, meaning that callers should use
/// [expectLater] when using this matcher and await the future returned by
/// [expectLater].
///
337 338 339 340 341 342 343 344 345
/// ## Golden File Testing
///
/// The term __golden file__ refers to a master image that is considered the true
/// rendering of a given widget, state, application, or other visual
/// representation you have chosen to capture.
///
/// The master golden image files that are tested against can be created or
/// updated by running `flutter test --update-goldens` on the test.
///
346
/// {@tool snippet}
347
/// Sample invocations of [matchesGoldenFile].
348 349
///
/// ```dart
350 351 352 353 354 355 356 357 358 359 360 361
/// await expectLater(
///   find.text('Save'),
///   matchesGoldenFile('save.png'),
/// );
///
/// await expectLater(
///   image,
///   matchesGoldenFile('save.png'),
/// );
///
/// await expectLater(
///   imageFuture,
362 363 364 365 366
///   matchesGoldenFile(
///     'save.png',
///     version: 2,
///   ),
/// );
367 368 369 370 371
///
/// await expectLater(
///   find.byType(MyWidget),
///   matchesGoldenFile('goldens/myWidget.png'),
/// );
372
/// ```
373
/// {@end-tool}
374
///
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
/// {@template flutter.flutter_test.matchesGoldenFile.custom_fonts}
/// ## Including Fonts
///
/// Custom fonts may render differently across different platforms, or
/// between different versions of Flutter. For example, a golden file generated
/// on Windows with fonts will likely differ from the one produced by another
/// operating system. Even on the same platform, if the generated golden is
/// tested with a different Flutter version, the test may fail and require an
/// updated image.
///
/// By default, the Flutter framework uses a font called 'Ahem' which shows
/// squares instead of characters, however, it is possible to render images using
/// custom fonts. For example, this is how to load the 'Roboto' font for a
/// golden test:
///
/// {@tool snippet}
/// How to load a custom font for golden images.
/// ```dart
/// testWidgets('Creating a golden image with a custom font', (tester) async {
///   // Assuming the 'Roboto.ttf' file is declared in the pubspec.yaml file
///   final font = rootBundle.load('path/to/font-file/Roboto.ttf');
///
///   final fontLoader = FontLoader('Roboto')..addFont(font);
///   await fontLoader.load();
///
///   await tester.pumpWidget(const SomeWidget());
///
///   await expectLater(
///     find.byType(SomeWidget),
///     matchesGoldenFile('someWidget.png'),
///   );
/// });
/// ```
/// {@end-tool}
///
/// The example above loads the desired font only for that specific test. To load
/// a font for all golden file tests, the `FontLoader.load()` call could be
/// moved in the `flutter_test_config.dart`. In this way, the font will always be
/// loaded before a test:
///
/// {@tool snippet}
/// Loading a custom font from the flutter_test_config.dart file.
/// ```dart
/// Future<void> testExecutable(FutureOr<void> Function() testMain) async {
///   setUpAll(() async {
///     final fontLoader = FontLoader('SomeFont')..addFont(someFont);
///     await fontLoader.load();
///   });
///
///   await testMain();
/// });
/// ```
/// {@end-tool}
/// {@endtemplate}
///
430 431
/// See also:
///
432 433 434
///  * [GoldenFileComparator], which acts as the backend for this matcher.
///  * [LocalFileComparator], which is the default [GoldenFileComparator]
///    implementation for `flutter test`.
435 436
///  * [matchesReferenceImage], which should be used instead if you want to
///    verify that two different code paths create identical images.
437 438
///  * [flutter_test] for a discussion of test configurations, whereby callers
///    may swap out the backend for this matcher.
439
AsyncMatcher matchesGoldenFile(Object key, {int? version}) {
440
  if (key is Uri) {
441
    return MatchesGoldenFile(key, version);
442
  } else if (key is String) {
443
    return MatchesGoldenFile.forStringPath(key, version);
444
  }
445
  throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
446
}
447

448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches a
/// reference image identified by [image].
///
/// For the case of a [Finder], the [Finder] must match exactly one widget and
/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
/// treated as the image for the widget.
///
/// This is an asynchronous matcher, meaning that callers should use
/// [expectLater] when using this matcher and await the future returned by
/// [expectLater].
///
/// ## Sample code
///
/// ```dart
/// final ui.Paint paint = ui.Paint()
///   ..style = ui.PaintingStyle.stroke
///   ..strokeWidth = 1.0;
/// final ui.PictureRecorder recorder = ui.PictureRecorder();
/// final ui.Canvas pictureCanvas = ui.Canvas(recorder);
/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
/// final ui.Picture picture = recorder.endRecording();
/// ui.Image referenceImage = picture.toImage(50, 50);
///
/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage));
/// await expectLater(image, matchesReferenceImage(referenceImage);
/// await expectLater(imageFuture, matchesReferenceImage(referenceImage));
/// ```
///
/// See also:
///
///  * [matchesGoldenFile], which should be used instead if you need to verify
///    that a [Finder] or [ui.Image] matches a golden image.
AsyncMatcher matchesReferenceImage(ui.Image image) {
  return _MatchesReferenceImage(image);
}

484
/// Asserts that a [SemanticsNode] contains the specified information.
485 486 487 488 489
///
/// If either the label, hint, value, textDirection, or rect fields are not
/// provided, then they are not part of the comparison.  All of the boolean
/// flag and action fields must match, and default to false.
///
490
/// To retrieve the semantics data of a widget, use [WidgetTester.getSemantics]
491 492 493 494 495 496 497
/// with a [Finder] that returns a single widget. Semantics must be enabled
/// in order to use this method.
///
/// ## Sample code
///
/// ```dart
/// final SemanticsHandle handle = tester.ensureSemantics();
498
/// expect(tester.getSemantics(find.text('hello')), matchesSemantics(label: 'hello'));
499 500 501 502 503
/// handle.dispose();
/// ```
///
/// See also:
///
504 505
///   * [WidgetTester.getSemantics], the tester method which retrieves semantics.
Matcher matchesSemantics({
506
  String? label,
507
  AttributedString? attributedLabel,
508
  String? hint,
509
  AttributedString? attributedHint,
510
  String? value,
511
  AttributedString? attributedValue,
512
  String? increasedValue,
513
  AttributedString? attributedIncreasedValue,
514
  String? decreasedValue,
515
  String? tooltip,
516
  AttributedString? attributedDecreasedValue,
517 518 519 520 521 522 523 524
  TextDirection? textDirection,
  Rect? rect,
  Size? size,
  double? elevation,
  double? thickness,
  int? platformViewId,
  int? maxValueLength,
  int? currentValueLength,
525 526 527 528 529
  // Flags //
  bool hasCheckedState = false,
  bool isChecked = false,
  bool isSelected = false,
  bool isButton = false,
530
  bool isSlider = false,
531
  bool isKeyboardKey = false,
532
  bool isLink = false,
533
  bool isFocused = false,
534
  bool isFocusable = false,
535
  bool isTextField = false,
536
  bool isReadOnly = false,
537 538 539 540 541
  bool hasEnabledState = false,
  bool isEnabled = false,
  bool isInMutuallyExclusiveGroup = false,
  bool isHeader = false,
  bool isObscured = false,
542
  bool isMultiline = false,
543 544 545
  bool namesRoute = false,
  bool scopesRoute = false,
  bool isHidden = false,
546 547 548 549
  bool isImage = false,
  bool isLiveRegion = false,
  bool hasToggledState = false,
  bool isToggled = false,
550
  bool hasImplicitScrolling = false,
551 552 553 554 555 556 557 558 559 560 561 562
  // Actions //
  bool hasTapAction = false,
  bool hasLongPressAction = false,
  bool hasScrollLeftAction = false,
  bool hasScrollRightAction = false,
  bool hasScrollUpAction = false,
  bool hasScrollDownAction = false,
  bool hasIncreaseAction = false,
  bool hasDecreaseAction = false,
  bool hasShowOnScreenAction = false,
  bool hasMoveCursorForwardByCharacterAction = false,
  bool hasMoveCursorBackwardByCharacterAction = false,
563 564
  bool hasMoveCursorForwardByWordAction = false,
  bool hasMoveCursorBackwardByWordAction = false,
565
  bool hasSetTextAction = false,
566 567 568 569 570 571
  bool hasSetSelectionAction = false,
  bool hasCopyAction = false,
  bool hasCutAction = false,
  bool hasPasteAction = false,
  bool hasDidGainAccessibilityFocusAction = false,
  bool hasDidLoseAccessibilityFocusAction = false,
572
  bool hasDismissAction = false,
573
  // Custom actions and overrides
574 575 576 577
  String? onTapHint,
  String? onLongPressHint,
  List<CustomSemanticsAction>? customActions,
  List<Matcher>? children,
578
}) {
579 580 581 582 583
  final List<SemanticsFlag> flags = <SemanticsFlag>[
    if (hasCheckedState) SemanticsFlag.hasCheckedState,
    if (isChecked) SemanticsFlag.isChecked,
    if (isSelected) SemanticsFlag.isSelected,
    if (isButton) SemanticsFlag.isButton,
584
    if (isSlider) SemanticsFlag.isSlider,
585
    if (isKeyboardKey) SemanticsFlag.isKeyboardKey,
586
    if (isLink) SemanticsFlag.isLink,
587 588 589
    if (isTextField) SemanticsFlag.isTextField,
    if (isReadOnly) SemanticsFlag.isReadOnly,
    if (isFocused) SemanticsFlag.isFocused,
590
    if (isFocusable) SemanticsFlag.isFocusable,
591 592 593 594 595 596 597 598 599 600 601 602 603 604
    if (hasEnabledState) SemanticsFlag.hasEnabledState,
    if (isEnabled) SemanticsFlag.isEnabled,
    if (isInMutuallyExclusiveGroup) SemanticsFlag.isInMutuallyExclusiveGroup,
    if (isHeader) SemanticsFlag.isHeader,
    if (isObscured) SemanticsFlag.isObscured,
    if (isMultiline) SemanticsFlag.isMultiline,
    if (namesRoute) SemanticsFlag.namesRoute,
    if (scopesRoute) SemanticsFlag.scopesRoute,
    if (isHidden) SemanticsFlag.isHidden,
    if (isImage) SemanticsFlag.isImage,
    if (isLiveRegion) SemanticsFlag.isLiveRegion,
    if (hasToggledState) SemanticsFlag.hasToggledState,
    if (isToggled) SemanticsFlag.isToggled,
    if (hasImplicitScrolling) SemanticsFlag.hasImplicitScrolling,
605
    if (isSlider) SemanticsFlag.isSlider,
606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629
  ];

  final List<SemanticsAction> actions = <SemanticsAction>[
    if (hasTapAction) SemanticsAction.tap,
    if (hasLongPressAction) SemanticsAction.longPress,
    if (hasScrollLeftAction) SemanticsAction.scrollLeft,
    if (hasScrollRightAction) SemanticsAction.scrollRight,
    if (hasScrollUpAction) SemanticsAction.scrollUp,
    if (hasScrollDownAction) SemanticsAction.scrollDown,
    if (hasIncreaseAction) SemanticsAction.increase,
    if (hasDecreaseAction) SemanticsAction.decrease,
    if (hasShowOnScreenAction) SemanticsAction.showOnScreen,
    if (hasMoveCursorForwardByCharacterAction) SemanticsAction.moveCursorForwardByCharacter,
    if (hasMoveCursorBackwardByCharacterAction) SemanticsAction.moveCursorBackwardByCharacter,
    if (hasSetSelectionAction) SemanticsAction.setSelection,
    if (hasCopyAction) SemanticsAction.copy,
    if (hasCutAction) SemanticsAction.cut,
    if (hasPasteAction) SemanticsAction.paste,
    if (hasDidGainAccessibilityFocusAction) SemanticsAction.didGainAccessibilityFocus,
    if (hasDidLoseAccessibilityFocusAction) SemanticsAction.didLoseAccessibilityFocus,
    if (customActions != null && customActions.isNotEmpty) SemanticsAction.customAction,
    if (hasDismissAction) SemanticsAction.dismiss,
    if (hasMoveCursorForwardByWordAction) SemanticsAction.moveCursorForwardByWord,
    if (hasMoveCursorBackwardByWordAction) SemanticsAction.moveCursorBackwardByWord,
630
    if (hasSetTextAction) SemanticsAction.setText,
631
  ];
632
  SemanticsHintOverrides? hintOverrides;
633
  if (onTapHint != null || onLongPressHint != null) {
634
    hintOverrides = SemanticsHintOverrides(
635 636 637
      onTapHint: onTapHint,
      onLongPressHint: onLongPressHint,
    );
638
  }
639

640 641
  return _MatchesSemanticsData(
    label: label,
642
    attributedLabel: attributedLabel,
643
    hint: hint,
644
    attributedHint: attributedHint,
645
    value: value,
646
    attributedValue: attributedValue,
647
    increasedValue: increasedValue,
648
    tooltip: tooltip,
649
    attributedIncreasedValue: attributedIncreasedValue,
650
    decreasedValue: decreasedValue,
651
    attributedDecreasedValue: attributedDecreasedValue,
652 653 654 655 656
    actions: actions,
    flags: flags,
    textDirection: textDirection,
    rect: rect,
    size: size,
657 658
    elevation: elevation,
    thickness: thickness,
659
    platformViewId: platformViewId,
660 661
    customActions: customActions,
    hintOverrides: hintOverrides,
662 663
    currentValueLength: currentValueLength,
    maxValueLength: maxValueLength,
664 665 666 667
    children: children,
  );
}

668 669 670 671 672 673 674 675 676 677
/// Asserts that the currently rendered widget meets the provided accessibility
/// `guideline`.
///
/// This matcher requires the result to be awaited and for semantics to be
/// enabled first.
///
/// ## Sample code
///
/// ```dart
/// final SemanticsHandle handle = tester.ensureSemantics();
678
/// await expectLater(tester, meetsGuideline(textContrastGuideline));
679 680 681 682 683
/// handle.dispose();
/// ```
///
/// Supported accessibility guidelines:
///
684 685
///   * [androidTapTargetGuideline], for Android minimum tappable area guidelines.
///   * [iOSTapTargetGuideline], for iOS minimum tappable area guidelines.
686
///   * [textContrastGuideline], for WCAG minimum text contrast guidelines.
687
///   * [labeledTapTargetGuideline], for enforcing labels on tappable areas.
688
AsyncMatcher meetsGuideline(AccessibilityGuideline guideline) {
689
  return _MatchesAccessibilityGuideline(guideline);
690 691 692 693 694 695 696
}

/// The inverse matcher of [meetsGuideline].
///
/// This is needed because the [isNot] matcher does not compose with an
/// [AsyncMatcher].
AsyncMatcher doesNotMeetGuideline(AccessibilityGuideline guideline) {
697
  return _DoesNotMatchAccessibilityGuideline(guideline);
698 699
}

700 701 702
class _FindsWidgetMatcher extends Matcher {
  const _FindsWidgetMatcher(this.min, this.max);

703 704
  final int? min;
  final int? max;
705 706

  @override
707
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
708
    assert(min != null || max != null);
709
    assert(min == null || max == null || min! <= max!);
710
    matchState[Finder] = finder;
711
    int count = 0;
712
    final Iterator<Element> iterator = finder.evaluate().iterator;
713
    if (min != null) {
714
      while (count < min! && iterator.moveNext()) {
715
        count += 1;
716 717
      }
      if (count < min!) {
718
        return false;
719
      }
720 721
    }
    if (max != null) {
722
      while (count <= max! && iterator.moveNext()) {
723
        count += 1;
724 725
      }
      if (count > max!) {
726
        return false;
727
      }
728 729 730 731 732 733 734 735
    }
    return true;
  }

  @override
  Description describe(Description description) {
    assert(min != null || max != null);
    if (min == max) {
736
      if (min == 1) {
737
        return description.add('exactly one matching node in the widget tree');
738
      }
739 740 741
      return description.add('exactly $min matching nodes in the widget tree');
    }
    if (min == null) {
742
      if (max == 0) {
743
        return description.add('no matching nodes in the widget tree');
744 745
      }
      if (max == 1) {
746
        return description.add('at most one matching node in the widget tree');
747
      }
748 749 750
      return description.add('at most $max matching nodes in the widget tree');
    }
    if (max == null) {
751
      if (min == 1) {
752
        return description.add('at least one matching node in the widget tree');
753
      }
754 755 756 757 758 759 760 761 762 763
      return description.add('at least $min matching nodes in the widget tree');
    }
    return description.add('between $min and $max matching nodes in the widget tree (inclusive)');
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
764
    bool verbose,
765
  ) {
766
    final Finder finder = matchState[Finder] as Finder;
767
    final int count = finder.evaluate().length;
768
    if (count == 0) {
769
      assert(min != null && min! > 0);
770
      if (min == 1 && max == 1) {
771
        return mismatchDescription.add('means none were found but one was expected');
772
      }
773 774 775
      return mismatchDescription.add('means none were found but some were expected');
    }
    if (max == 0) {
776
      if (count == 1) {
777
        return mismatchDescription.add('means one was found but none were expected');
778
      }
779 780
      return mismatchDescription.add('means some were found but none were expected');
    }
781
    if (min != null && count < min!) {
782
      return mismatchDescription.add('is not enough');
783
    }
784
    assert(max != null && count > min!);
785 786 787 788
    return mismatchDescription.add('is too many');
  }
}

789
bool _hasAncestorMatching(Finder finder, bool Function(Widget widget) predicate) {
790
  final Iterable<Element> nodes = finder.evaluate();
791
  if (nodes.length != 1) {
792
    return false;
793
  }
794
  bool result = false;
795
  nodes.single.visitAncestorElements((Element ancestor) {
796
    if (predicate(ancestor.widget)) {
797 798 799 800 801 802 803 804
      result = true;
      return false;
    }
    return true;
  });
  return result;
}

805 806 807 808
bool _hasAncestorOfType(Finder finder, Type targetType) {
  return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType);
}

809 810
class _IsOffstage extends Matcher {
  const _IsOffstage();
811 812

  @override
813
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
814
    return _hasAncestorMatching(finder, (Widget widget) {
815
      if (widget is Offstage) {
816
        return widget.offstage;
817
      }
818
      return false;
819 820
    });
  }
821 822 823 824 825

  @override
  Description describe(Description description) => description.add('offstage');
}

826 827
class _IsOnstage extends Matcher {
  const _IsOnstage();
828 829

  @override
830
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
831
    final Iterable<Element> nodes = finder.evaluate();
832
    if (nodes.length != 1) {
833
      return false;
834
    }
835
    bool result = true;
836
    nodes.single.visitAncestorElements((Element ancestor) {
837
      final Widget widget = ancestor.widget;
838 839
      if (widget is Offstage) {
        result = !widget.offstage;
840 841 842 843 844 845
        return false;
      }
      return true;
    });
    return result;
  }
846 847 848 849 850 851 852 853 854

  @override
  Description describe(Description description) => description.add('onstage');
}

class _IsInCard extends Matcher {
  const _IsInCard();

  @override
855
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => _hasAncestorOfType(finder, Card);
856 857 858 859 860 861 862 863 864

  @override
  Description describe(Description description) => description.add('in card');
}

class _IsNotInCard extends Matcher {
  const _IsNotInCard();

  @override
865
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => !_hasAncestorOfType(finder, Card);
866 867 868 869

  @override
  Description describe(Description description) => description.add('not in card');
}
870

871 872
class _HasOneLineDescription extends Matcher {
  const _HasOneLineDescription();
873 874

  @override
875
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
876
    final String description = object.toString();
877 878
    return description.isNotEmpty
        && !description.contains('\n')
879
        && !description.contains('Instance of ')
880
        && description.trim() == description;
881 882 883 884 885
  }

  @override
  Description describe(Description description) => description.add('one line description');
}
886

887 888 889 890 891
class _EqualsIgnoringHashCodes extends Matcher {
  _EqualsIgnoringHashCodes(String v) : _value = _normalize(v);

  final String _value;

892
  static final Object _mismatchedValueKey = Object();
893 894

  static String _normalize(String s) {
895
    return s.replaceAll(RegExp(r'#[0-9a-fA-F]{5}'), '#00000');
896 897 898 899
  }

  @override
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
900
    final String description = _normalize(object as String);
901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917
    if (_value != description) {
      matchState[_mismatchedValueKey] = description;
      return false;
    }
    return true;
  }

  @override
  Description describe(Description description) {
    return description.add('multi line description equals $_value');
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
918
    bool verbose,
919 920
  ) {
    if (matchState.containsKey(_mismatchedValueKey)) {
921
      final String actualValue = matchState[_mismatchedValueKey] as String;
922
      // Leading whitespace is added so that lines in the multiline
923 924 925 926 927 928 929 930 931 932 933 934
      // description returned by addDescriptionOf are all indented equally
      // which makes the output easier to read for this case.
      return mismatchDescription
          .add('expected normalized value\n  ')
          .addDescriptionOf(_value)
          .add('\nbut got\n  ')
          .addDescriptionOf(actualValue);
    }
    return mismatchDescription;
  }
}

935
/// Returns true if [c] represents a whitespace code unit.
936 937
bool _isWhitespace(int c) => (c <= 0x000D && c >= 0x0009) || c == 0x0020;

938
/// Returns true if [c] represents a vertical line Unicode line art code unit.
939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
///
/// See [https://en.wikipedia.org/wiki/Box-drawing_character]. This method only
/// specifies vertical line art code units currently used by Flutter line art.
/// There are other line art characters that technically also represent vertical
/// lines.
bool _isVerticalLine(int c) {
  return c == 0x2502 || c == 0x2503 || c == 0x2551 || c == 0x254e;
}

/// Returns whether a [line] is all vertical tree connector characters.
///
/// Example vertical tree connector characters: `│ ║ ╎`.
/// The last line of a text tree contains only vertical tree connector
/// characters indicates a poorly formatted tree.
bool _isAllTreeConnectorCharacters(String line) {
  for (int i = 0; i < line.length; ++i) {
    final int c = line.codeUnitAt(i);
956
    if (!_isWhitespace(c) && !_isVerticalLine(c)) {
957
      return false;
958
    }
959 960 961 962 963 964 965
  }
  return true;
}

class _HasGoodToStringDeep extends Matcher {
  const _HasGoodToStringDeep();

966
  static final Object _toStringDeepErrorDescriptionKey = Object();
967 968 969 970

  @override
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
    final List<String> issues = <String>[];
971
    String description = object.toStringDeep() as String; // ignore: avoid_dynamic_calls
972 973 974 975 976 977 978 979
    if (description.endsWith('\n')) {
      // Trim off trailing \n as the remaining calculations assume
      // the description does not end with a trailing \n.
      description = description.substring(0, description.length - 1);
    } else {
      issues.add('Not terminated with a line break.');
    }

980
    if (description.trim() != description) {
981
      issues.add('Has trailing whitespace.');
982
    }
983 984

    final List<String> lines = description.split('\n');
985
    if (lines.length < 2) {
986
      issues.add('Does not have multiple lines.');
987
    }
988

989
    if (description.contains('Instance of ')) {
990
      issues.add('Contains text "Instance of ".');
991
    }
992 993 994

    for (int i = 0; i < lines.length; ++i) {
      final String line = lines[i];
995
      if (line.isEmpty) {
996
        issues.add('Line ${i+1} is empty.');
997
      }
998

999
      if (line.trimRight() != line) {
1000
        issues.add('Line ${i+1} has trailing whitespace.');
1001
      }
1002 1003
    }

1004
    if (_isAllTreeConnectorCharacters(lines.last)) {
1005
      issues.add('Last line is all tree connector characters.');
1006
    }
1007 1008

    // If a toStringDeep method doesn't properly handle nested values that
1009
    // contain line breaks it can fail to add the required prefixes to all
1010
    // lined when toStringDeep is called specifying prefixes.
1011 1012
    const String prefixLineOne    = 'PREFIX_LINE_ONE____';
    const String prefixOtherLines = 'PREFIX_OTHER_LINES_';
1013 1014
    final List<String> prefixIssues = <String>[];
    String descriptionWithPrefixes =
1015
      object.toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines) as String; // ignore: avoid_dynamic_calls
1016 1017 1018 1019 1020 1021 1022
    if (descriptionWithPrefixes.endsWith('\n')) {
      // Trim off trailing \n as the remaining calculations assume
      // the description does not end with a trailing \n.
      descriptionWithPrefixes = descriptionWithPrefixes.substring(
          0, descriptionWithPrefixes.length - 1);
    }
    final List<String> linesWithPrefixes = descriptionWithPrefixes.split('\n');
1023
    if (!linesWithPrefixes.first.startsWith(prefixLineOne)) {
1024
      prefixIssues.add('First line does not contain expected prefix.');
1025
    }
1026 1027

    for (int i = 1; i < linesWithPrefixes.length; ++i) {
1028
      if (!linesWithPrefixes[i].startsWith(prefixOtherLines)) {
1029
        prefixIssues.add('Line ${i+1} does not contain the expected prefix.');
1030
      }
1031 1032
    }

1033
    final StringBuffer errorDescription = StringBuffer();
1034 1035 1036 1037 1038 1039 1040 1041
    if (issues.isNotEmpty) {
      errorDescription.writeln('Bad toStringDeep():');
      errorDescription.writeln(description);
      errorDescription.writeAll(issues, '\n');
    }

    if (prefixIssues.isNotEmpty) {
      errorDescription.writeln(
1042
          'Bad toStringDeep(prefixLineOne: "$prefixLineOne", prefixOtherLines: "$prefixOtherLines"):');
1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059
      errorDescription.writeln(descriptionWithPrefixes);
      errorDescription.writeAll(prefixIssues, '\n');
    }

    if (errorDescription.isNotEmpty) {
      matchState[_toStringDeepErrorDescriptionKey] =
          errorDescription.toString();
      return false;
    }
    return true;
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
1060
    bool verbose,
1061 1062
  ) {
    if (matchState.containsKey(_toStringDeepErrorDescriptionKey)) {
1063
      return mismatchDescription.add(matchState[_toStringDeepErrorDescriptionKey] as String);
1064 1065 1066 1067 1068 1069 1070 1071 1072 1073
    }
    return mismatchDescription;
  }

  @override
  Description describe(Description description) {
    return description.add('multi line description');
  }
}

1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086
/// Computes the distance between two values.
///
/// The distance should be a metric in a metric space (see
/// https://en.wikipedia.org/wiki/Metric_space). Specifically, if `f` is a
/// distance function then the following conditions should hold:
///
/// - f(a, b) >= 0
/// - f(a, b) == 0 if and only if a == b
/// - f(a, b) == f(b, a)
/// - f(a, c) <= f(a, b) + f(b, c), known as triangle inequality
///
/// This makes it useful for comparing numbers, [Color]s, [Offset]s and other
/// sets of value for which a metric space is defined.
1087
typedef DistanceFunction<T> = num Function(T a, T b);
1088

1089 1090 1091 1092
/// The type of a union of instances of [DistanceFunction<T>] for various types
/// T.
///
/// This type is used to describe a collection of [DistanceFunction<T>]
1093
/// functions which have (potentially) unrelated argument types. Since the
1094 1095 1096
/// argument types of the functions may be unrelated, their type is declared as
/// `Never`, which is the bottom type in dart to which all other types can be
/// assigned to.
1097 1098 1099
///
/// Calling an instance of this type must either be done dynamically, or by
/// first casting it to a [DistanceFunction<T>] for some concrete T.
1100
typedef AnyDistanceFunction = num Function(Never a, Never b);
1101

1102
const Map<Type, AnyDistanceFunction> _kStandardDistanceFunctions = <Type, AnyDistanceFunction>{
1103
  Color: _maxComponentColorDistance,
1104 1105
  HSVColor: _maxComponentHSVColorDistance,
  HSLColor: _maxComponentHSLColorDistance,
1106 1107 1108
  Offset: _offsetDistance,
  int: _intDistance,
  double: _doubleDistance,
1109
  Rect: _rectDistance,
1110
  Size: _sizeDistance,
1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123
};

int _intDistance(int a, int b) => (b - a).abs();
double _doubleDistance(double a, double b) => (b - a).abs();
double _offsetDistance(Offset a, Offset b) => (b - a).distance;

double _maxComponentColorDistance(Color a, Color b) {
  int delta = math.max<int>((a.red - b.red).abs(), (a.green - b.green).abs());
  delta = math.max<int>(delta, (a.blue - b.blue).abs());
  delta = math.max<int>(delta, (a.alpha - b.alpha).abs());
  return delta.toDouble();
}

1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139
// Compares hue by converting it to a 0.0 - 1.0 range, so that the comparison
// can be a similar error percentage per component.
double _maxComponentHSVColorDistance(HSVColor a, HSVColor b) {
  double delta = math.max<double>((a.saturation - b.saturation).abs(), (a.value - b.value).abs());
  delta = math.max<double>(delta, ((a.hue - b.hue) / 360.0).abs());
  return math.max<double>(delta, (a.alpha - b.alpha).abs());
}

// Compares hue by converting it to a 0.0 - 1.0 range, so that the comparison
// can be a similar error percentage per component.
double _maxComponentHSLColorDistance(HSLColor a, HSLColor b) {
  double delta = math.max<double>((a.saturation - b.saturation).abs(), (a.lightness - b.lightness).abs());
  delta = math.max<double>(delta, ((a.hue - b.hue) / 360.0).abs());
  return math.max<double>(delta, (a.alpha - b.alpha).abs());
}

1140 1141 1142 1143 1144 1145 1146
double _rectDistance(Rect a, Rect b) {
  double delta = math.max<double>((a.left - b.left).abs(), (a.top - b.top).abs());
  delta = math.max<double>(delta, (a.right - b.right).abs());
  delta = math.max<double>(delta, (a.bottom - b.bottom).abs());
  return delta;
}

1147
double _sizeDistance(Size a, Size b) {
1148 1149 1150
  // TODO(a14n): remove ignore when lint is updated, https://github.com/dart-lang/linter/issues/1843
  // ignore: unnecessary_parenthesis
  final Offset delta = (b - a) as Offset;
1151
  return delta.distance;
1152 1153
}

1154 1155 1156 1157 1158
/// Asserts that two values are within a certain distance from each other.
///
/// The distance is computed by a [DistanceFunction].
///
/// If `distanceFunction` is null, a standard distance function is used for the
1159 1160
/// `T` generic argument. Standard functions are defined for the following
/// types:
1161 1162 1163 1164
///
///  * [Color], whose distance is the maximum component-wise delta.
///  * [Offset], whose distance is the Euclidean distance computed using the
///    method [Offset.distance].
1165 1166 1167
///  * [Rect], whose distance is the maximum component-wise delta.
///  * [Size], whose distance is the [Offset.distance] of the offset computed as
///    the difference between two sizes.
1168 1169 1170 1171 1172 1173 1174
///  * [int], whose distance is the absolute difference between two integers.
///  * [double], whose distance is the absolute difference between two doubles.
///
/// See also:
///
///  * [moreOrLessEquals], which is similar to this function, but specializes in
///    [double]s and has an optional `epsilon` parameter.
1175 1176
///  * [rectMoreOrLessEquals], which is similar to this function, but
///    specializes in [Rect]s and has an optional `epsilon` parameter.
1177 1178
///  * [closeTo], which specializes in numbers only.
Matcher within<T>({
1179 1180 1181
  required num distance,
  required T from,
  DistanceFunction<T>? distanceFunction,
1182
}) {
1183
  distanceFunction ??= _kStandardDistanceFunctions[T] as DistanceFunction<T>?;
1184 1185

  if (distanceFunction == null) {
1186
    throw ArgumentError(
1187 1188 1189 1190 1191 1192
      'The specified distanceFunction was null, and a standard distance '
      'function was not found for type ${from.runtimeType} of the provided '
      '`from` argument.'
    );
  }

1193
  return _IsWithinDistance<T>(distanceFunction, from, distance);
1194 1195 1196 1197 1198 1199 1200 1201 1202 1203
}

class _IsWithinDistance<T> extends Matcher {
  const _IsWithinDistance(this.distanceFunction, this.value, this.epsilon);

  final DistanceFunction<T> distanceFunction;
  final T value;
  final num epsilon;

  @override
1204
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
1205
    if (object is! T) {
1206
      return false;
1207 1208
    }
    if (object == value) {
1209
      return true;
1210
    }
1211
    final num distance = distanceFunction(object, value);
1212
    if (distance < 0) {
1213
      throw ArgumentError(
1214 1215 1216 1217 1218
        'Invalid distance function was used to compare a ${value.runtimeType} '
        'to a ${object.runtimeType}. The function must return a non-negative '
        'double value, but it returned $distance.'
      );
    }
1219
    matchState['distance'] = distance;
1220 1221 1222 1223 1224
    return distance <= epsilon;
  }

  @override
  Description describe(Description description) => description.add('$value$epsilon)');
1225 1226 1227

  @override
  Description describeMismatch(
1228
    dynamic object,
1229 1230 1231 1232 1233 1234 1235
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    mismatchDescription.add('was ${matchState['distance']} away from the desired value.');
    return mismatchDescription;
  }
1236 1237
}

1238
class _MoreOrLessEquals extends Matcher {
1239 1240
  const _MoreOrLessEquals(this.value, this.epsilon)
    : assert(epsilon >= 0);
1241 1242 1243 1244 1245

  final double value;
  final double epsilon;

  @override
1246
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
1247
    if (object is! double) {
1248
      return false;
1249 1250
    }
    if (object == value) {
1251
      return true;
1252
    }
1253
    return (object - value).abs() <= epsilon;
1254 1255 1256 1257
  }

  @override
  Description describe(Description description) => description.add('$value$epsilon)');
1258 1259

  @override
1260
  Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
1261 1262 1263
    return super.describeMismatch(item, mismatchDescription, matchState, verbose)
      ..add('$item is not in the range of $value$epsilon).');
  }
1264
}
1265 1266 1267 1268 1269 1270 1271 1272 1273

class _IsMethodCall extends Matcher {
  const _IsMethodCall(this.name, this.arguments);

  final String name;
  final dynamic arguments;

  @override
  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
1274
    if (item is! MethodCall) {
1275
      return false;
1276 1277
    }
    if (item.method != name) {
1278
      return false;
1279
    }
1280 1281 1282 1283
    return _deepEquals(item.arguments, arguments);
  }

  bool _deepEquals(dynamic a, dynamic b) {
1284
    if (a == b) {
1285
      return true;
1286 1287
    }
    if (a is List) {
1288
      return b is List && _deepEqualsList(a, b);
1289 1290
    }
    if (a is Map) {
1291
      return b is Map && _deepEqualsMap(a, b);
1292
    }
1293 1294 1295 1296
    return false;
  }

  bool _deepEqualsList(List<dynamic> a, List<dynamic> b) {
1297
    if (a.length != b.length) {
1298
      return false;
1299
    }
1300
    for (int i = 0; i < a.length; i++) {
1301
      if (!_deepEquals(a[i], b[i])) {
1302
        return false;
1303
      }
1304 1305 1306 1307 1308
    }
    return true;
  }

  bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) {
1309
    if (a.length != b.length) {
1310
      return false;
1311
    }
1312
    for (final dynamic key in a.keys) {
1313
      if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) {
1314
        return false;
1315
      }
1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327
    }
    return true;
  }

  @override
  Description describe(Description description) {
    return description
        .add('has method name: ').addDescriptionOf(name)
        .add(' with arguments: ').addDescriptionOf(arguments);
  }
}

1328
/// Asserts that a [Finder] locates a single object whose root RenderObject
1329 1330
/// is a [RenderClipRect] with no clipper set, or an equivalent
/// [RenderClipPath].
1331
const Matcher clipsWithBoundingRect = _ClipsWithBoundingRect();
1332

1333 1334 1335 1336 1337
/// Asserts that a [Finder] locates a single object whose root RenderObject is
/// not a [RenderClipRect], [RenderClipRRect], [RenderClipOval], or
/// [RenderClipPath].
const Matcher hasNoImmediateClip = _MatchAnythingExceptClip();

1338 1339
/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderClipRRect] with no clipper set, and border radius equals to
1340
/// [borderRadius], or an equivalent [RenderClipPath].
1341
Matcher clipsWithBoundingRRect({ required BorderRadius borderRadius }) {
1342
  return _ClipsWithBoundingRRect(borderRadius: borderRadius);
1343 1344 1345
}

/// Asserts that a [Finder] locates a single object whose root RenderObject
1346
/// is a [RenderClipPath] with a [ShapeBorderClipper] that clips to
1347
/// [shape].
1348
Matcher clipsWithShapeBorder({ required ShapeBorder shape }) {
1349
  return _ClipsWithShapeBorder(shape: shape);
1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368
}

/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderPhysicalModel] or a [RenderPhysicalShape].
///
/// - If the render object is a [RenderPhysicalModel]
///    - If [shape] is non null asserts that [RenderPhysicalModel.shape] is equal to
///   [shape].
///    - If [borderRadius] is non null asserts that [RenderPhysicalModel.borderRadius] is equal to
///   [borderRadius].
///     - If [elevation] is non null asserts that [RenderPhysicalModel.elevation] is equal to
///   [elevation].
/// - If the render object is a [RenderPhysicalShape]
///    - If [borderRadius] is non null asserts that the shape is a rounded
///   rectangle with this radius.
///    - If [borderRadius] is null, asserts that the shape is equivalent to
///   [shape].
///    - If [elevation] is non null asserts that [RenderPhysicalModel.elevation] is equal to
///   [elevation].
1369
Matcher rendersOnPhysicalModel({
1370 1371 1372
  BoxShape? shape,
  BorderRadius? borderRadius,
  double? elevation,
1373
}) {
1374
  return _RendersOnPhysicalModel(
1375 1376 1377 1378 1379 1380
    shape: shape,
    borderRadius: borderRadius,
    elevation: elevation,
  );
}

1381 1382 1383 1384 1385 1386
/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is [RenderPhysicalShape] that uses a [ShapeBorderClipper] that clips to
/// [shape] as its clipper.
/// If [elevation] is non null asserts that [RenderPhysicalShape.elevation] is
/// equal to [elevation].
Matcher rendersOnPhysicalShape({
1387 1388
  required ShapeBorder shape,
  double? elevation,
1389
}) {
1390
  return _RendersOnPhysicalShape(
1391 1392 1393 1394 1395
    shape: shape,
    elevation: elevation,
  );
}

1396 1397 1398 1399 1400 1401 1402 1403 1404 1405
abstract class _FailWithDescriptionMatcher extends Matcher {
  const _FailWithDescriptionMatcher();

  bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
    matchState['failure'] = description;
    return false;
  }

  @override
  Description describeMismatch(
1406 1407 1408
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
1409
    bool verbose,
1410
  ) {
1411
    return mismatchDescription.add(matchState['failure'] as String);
1412 1413 1414 1415 1416 1417 1418 1419 1420
  }
}

class _MatchAnythingExceptClip extends _FailWithDescriptionMatcher {
  const _MatchAnythingExceptClip();

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
    final Iterable<Element> nodes = finder.evaluate();
1421
    if (nodes.length != 1) {
1422
      return failWithDescription(matchState, 'did not have a exactly one child element');
1423
    }
1424
    final RenderObject renderObject = nodes.single.renderObject!;
1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438

    switch (renderObject.runtimeType) {
      case RenderClipPath:
      case RenderClipOval:
      case RenderClipRect:
      case RenderClipRRect:
        return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
      default:
        return true;
    }
  }

  @override
  Description describe(Description description) {
1439
    return description.add('does not have a clip as an immediate child');
1440 1441 1442 1443
  }
}

abstract class _MatchRenderObject<M extends RenderObject, T extends RenderObject> extends _FailWithDescriptionMatcher {
1444 1445
  const _MatchRenderObject();

1446 1447
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, T renderObject);
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, M renderObject);
1448 1449 1450 1451

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
    final Iterable<Element> nodes = finder.evaluate();
1452
    if (nodes.length != 1) {
1453
      return failWithDescription(matchState, 'did not have a exactly one child element');
1454
    }
1455
    final RenderObject renderObject = nodes.single.renderObject!;
1456

1457
    if (renderObject.runtimeType == T) {
1458
      return renderObjectMatchesT(matchState, renderObject as T);
1459
    }
1460

1461
    if (renderObject.runtimeType == M) {
1462
      return renderObjectMatchesM(matchState, renderObject as M);
1463
    }
1464 1465

    return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
1466 1467 1468
  }
}

1469
class _RendersOnPhysicalModel extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
1470 1471 1472 1473 1474 1475
  const _RendersOnPhysicalModel({
    this.shape,
    this.borderRadius,
    this.elevation,
  });

1476 1477 1478
  final BoxShape? shape;
  final BorderRadius? borderRadius;
  final double? elevation;
1479 1480

  @override
1481
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) {
1482
    if (shape != null && renderObject.shape != shape) {
1483
      return failWithDescription(matchState, 'had shape: ${renderObject.shape}');
1484
    }
1485

1486
    if (borderRadius != null && renderObject.borderRadius != borderRadius) {
1487
      return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');
1488
    }
1489

1490
    if (elevation != null && renderObject.elevation != elevation) {
1491
      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
1492
    }
1493 1494 1495 1496

    return true;
  }

1497 1498
  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
1499
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1500
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1501
    }
1502
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1503

1504
    if (borderRadius != null && !assertRoundedRectangle(shapeClipper, borderRadius!, matchState)) {
1505
      return false;
1506
    }
1507 1508

    if (
1509 1510 1511 1512
      borderRadius == null &&
      shape == BoxShape.rectangle &&
      !assertRoundedRectangle(shapeClipper, BorderRadius.zero, matchState)
    ) {
1513
      return false;
1514
    }
1515 1516

    if (
1517 1518 1519 1520
      borderRadius == null &&
      shape == BoxShape.circle &&
      !assertCircle(shapeClipper, matchState)
    ) {
1521
      return false;
1522
    }
1523

1524
    if (elevation != null && renderObject.elevation != elevation) {
1525
      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
1526
    }
1527 1528 1529 1530 1531

    return true;
  }

  bool assertRoundedRectangle(ShapeBorderClipper shapeClipper, BorderRadius borderRadius, Map<dynamic, dynamic> matchState) {
1532
    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) {
1533
      return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
1534
    }
1535
    final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
1536
    if (border.borderRadius != borderRadius) {
1537
      return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
1538
    }
1539
    return true;
1540 1541 1542
  }

  bool assertCircle(ShapeBorderClipper shapeClipper, Map<dynamic, dynamic> matchState) {
1543
    if (shapeClipper.shape.runtimeType != CircleBorder) {
1544
      return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
1545
    }
1546
    return true;
1547 1548
  }

1549 1550 1551
  @override
  Description describe(Description description) {
    description.add('renders on a physical model');
1552
    if (shape != null) {
1553
      description.add(' with shape $shape');
1554 1555
    }
    if (borderRadius != null) {
1556
      description.add(' with borderRadius $borderRadius');
1557 1558
    }
    if (elevation != null) {
1559
      description.add(' with elevation $elevation');
1560
    }
1561 1562 1563 1564
    return description;
  }
}

1565
class _RendersOnPhysicalShape extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
1566
  const _RendersOnPhysicalShape({
1567
    required this.shape,
1568 1569 1570 1571
    this.elevation,
  });

  final ShapeBorder shape;
1572
  final double? elevation;
1573 1574 1575

  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
1576
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1577
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1578
    }
1579
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1580

1581
    if (shapeClipper.shape != shape) {
1582
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1583
    }
1584

1585
    if (elevation != null && renderObject.elevation != elevation) {
1586
      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
1587
    }
1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599

    return true;
  }

  @override
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) {
    return false;
  }

  @override
  Description describe(Description description) {
    description.add('renders on a physical model with shape $shape');
1600
    if (elevation != null) {
1601
      description.add(' with elevation $elevation');
1602
    }
1603 1604 1605 1606 1607
    return description;
  }
}

class _ClipsWithBoundingRect extends _MatchRenderObject<RenderClipPath, RenderClipRect> {
1608 1609 1610
  const _ClipsWithBoundingRect();

  @override
1611
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRect renderObject) {
1612
    if (renderObject.clipper != null) {
1613
      return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');
1614
    }
1615 1616 1617
    return true;
  }

1618 1619
  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
1620
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1621
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1622
    }
1623
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1624
    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) {
1625
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1626
    }
1627
    final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
1628
    if (border.borderRadius != BorderRadius.zero) {
1629
      return failWithDescription(matchState, 'borderRadius was: ${border.borderRadius}');
1630
    }
1631 1632 1633
    return true;
  }

1634 1635 1636 1637 1638
  @override
  Description describe(Description description) =>
    description.add('clips with bounding rectangle');
}

1639
class _ClipsWithBoundingRRect extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
1640
  const _ClipsWithBoundingRRect({required this.borderRadius});
1641 1642 1643 1644 1645

  final BorderRadius borderRadius;


  @override
1646
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) {
1647
    if (renderObject.clipper != null) {
1648
      return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');
1649
    }
1650

1651
    if (renderObject.borderRadius != borderRadius) {
1652
      return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');
1653
    }
1654 1655 1656 1657

    return true;
  }

1658 1659
  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
1660
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1661
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1662
    }
1663
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1664
    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) {
1665
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1666
    }
1667
    final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
1668
    if (border.borderRadius != borderRadius) {
1669
      return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
1670
    }
1671 1672 1673
    return true;
  }

1674 1675 1676 1677
  @override
  Description describe(Description description) =>
    description.add('clips with bounding rounded rectangle with borderRadius: $borderRadius');
}
1678

1679
class _ClipsWithShapeBorder extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
1680
  const _ClipsWithShapeBorder({required this.shape});
1681 1682 1683 1684 1685

  final ShapeBorder shape;

  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
1686
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1687
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1688
    }
1689
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1690
    if (shapeClipper.shape != shape) {
1691
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1692
    }
1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705
    return true;
  }

  @override
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) {
    return false;
  }


  @override
  Description describe(Description description) =>
    description.add('clips with shape: $shape');
}
1706 1707 1708 1709

class _CoversSameAreaAs extends Matcher {
  _CoversSameAreaAs(
    this.expectedPath, {
1710
    required this.areaToCompare,
1711 1712 1713 1714
    this.sampleSize = 20,
  }) : maxHorizontalNoise = areaToCompare.width / sampleSize,
       maxVerticalNoise = areaToCompare.height / sampleSize {
    // Use a fixed random seed to make sure tests are deterministic.
1715
    random = math.Random(1);
1716 1717 1718 1719 1720 1721 1722
  }

  final Path expectedPath;
  final Rect areaToCompare;
  final int sampleSize;
  final double maxHorizontalNoise;
  final double maxVerticalNoise;
1723
  late math.Random random;
1724 1725 1726 1727 1728

  @override
  bool matches(covariant Path actualPath, Map<dynamic, dynamic> matchState) {
    for (int i = 0; i < sampleSize; i += 1) {
      for (int j = 0; j < sampleSize; j += 1) {
1729
        final Offset offset = Offset(
1730
          i * (areaToCompare.width / sampleSize),
1731
          j * (areaToCompare.height / sampleSize),
1732 1733
        );

1734
        if (!_samplePoint(matchState, actualPath, offset)) {
1735
          return false;
1736
        }
1737

1738
        final Offset noise = Offset(
1739 1740 1741 1742
          maxHorizontalNoise * random.nextDouble(),
          maxVerticalNoise * random.nextDouble(),
        );

1743
        if (!_samplePoint(matchState, actualPath, offset + noise)) {
1744
          return false;
1745
        }
1746 1747 1748 1749 1750 1751
      }
    }
    return true;
  }

  bool _samplePoint(Map<dynamic, dynamic> matchState, Path actualPath, Offset offset) {
1752
    if (expectedPath.contains(offset) == actualPath.contains(offset)) {
1753
      return true;
1754
    }
1755

1756
    if (actualPath.contains(offset)) {
1757
      return failWithDescription(matchState, '$offset is contained in the actual path but not in the expected path');
1758
    } else {
1759
      return failWithDescription(matchState, '$offset is contained in the expected path but not in the actual path');
1760
    }
1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772
  }

  bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
    matchState['failure'] = description;
    return false;
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
1773
    bool verbose,
1774
  ) {
1775
    return mismatchDescription.add(matchState['failure'] as String);
1776 1777 1778 1779 1780 1781
  }

  @override
  Description describe(Description description) =>
    description.add('covers expected area and only expected area');
}
1782

1783 1784
class _ColorMatcher extends Matcher {
  const _ColorMatcher({
1785
    required this.targetColor,
1786 1787 1788 1789 1790 1791
  }) : assert(targetColor != null);

  final Color targetColor;

  @override
  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
1792
    if (item is Color) {
1793
      return item == targetColor || item.value == targetColor.value;
1794
    }
1795 1796 1797 1798 1799 1800 1801
    return false;
  }

  @override
  Description describe(Description description) => description.add('matches color $targetColor');
}

1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821
int _countDifferentPixels(Uint8List imageA, Uint8List imageB) {
  assert(imageA.length == imageB.length);
  int delta = 0;
  for (int i = 0; i < imageA.length; i+=4) {
    if (imageA[i] != imageB[i] ||
      imageA[i+1] != imageB[i+1] ||
      imageA[i+2] != imageB[i+2] ||
      imageA[i+3] != imageB[i+3]) {
      delta++;
    }
  }
  return delta;
}

class _MatchesReferenceImage extends AsyncMatcher {
  const _MatchesReferenceImage(this.referenceImage);

  final ui.Image referenceImage;

  @override
1822
  Future<String?> matchAsync(dynamic item) async {
1823 1824 1825 1826 1827 1828
    Future<ui.Image> imageFuture;
    if (item is Future<ui.Image>) {
      imageFuture = item;
    } else if (item is ui.Image) {
      imageFuture = Future<ui.Image>.value(item);
    } else {
1829
      final Finder finder = item as Finder;
1830 1831 1832 1833 1834 1835
      final Iterable<Element> elements = finder.evaluate();
      if (elements.isEmpty) {
        return 'could not be rendered because no widget was found';
      } else if (elements.length > 1) {
        return 'matched too many widgets';
      }
1836
      imageFuture = captureImage(elements.single);
1837 1838
    }

1839
    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
1840
    return binding.runAsync<String?>(() async {
1841
      final ui.Image image = await imageFuture;
1842
      final ByteData? bytes = await image.toByteData();
1843
      if (bytes == null) {
1844
        return 'could not be encoded.';
1845
      }
1846

1847
      final ByteData? referenceBytes = await referenceImage.toByteData();
1848
      if (referenceBytes == null) {
1849
        return 'could not have its reference image encoded.';
1850
      }
1851

1852
      if (referenceImage.height != image.height || referenceImage.width != image.width) {
1853
        return 'does not match as width or height do not match. $image != $referenceImage';
1854
      }
1855 1856 1857 1858 1859 1860

      final int countDifferentPixels = _countDifferentPixels(
        Uint8List.view(bytes.buffer),
        Uint8List.view(referenceBytes.buffer),
      );
      return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
1861
    }, additionalTime: const Duration(minutes: 1));
1862 1863 1864 1865 1866 1867 1868 1869
  }

  @override
  Description describe(Description description) {
    return description.add('rasterized image matches that of a $referenceImage reference image');
  }
}

1870 1871 1872
class _MatchesSemanticsData extends Matcher {
  _MatchesSemanticsData({
    this.label,
1873 1874 1875
    this.attributedLabel,
    this.hint,
    this.attributedHint,
1876
    this.value,
1877
    this.attributedValue,
1878
    this.increasedValue,
1879
    this.attributedIncreasedValue,
1880
    this.decreasedValue,
1881
    this.attributedDecreasedValue,
1882
    this.tooltip,
1883 1884 1885 1886
    this.flags,
    this.actions,
    this.textDirection,
    this.rect,
1887
    this.size,
1888 1889
    this.elevation,
    this.thickness,
1890
    this.platformViewId,
1891 1892
    this.maxValueLength,
    this.currentValueLength,
1893 1894
    this.customActions,
    this.hintOverrides,
1895
    this.children,
1896 1897
  });

1898
  final String? label;
1899
  final AttributedString? attributedLabel;
1900
  final String? hint;
1901 1902 1903
  final AttributedString? attributedHint;
  final String? value;
  final AttributedString? attributedValue;
1904
  final String? increasedValue;
1905
  final AttributedString? attributedIncreasedValue;
1906
  final String? decreasedValue;
1907
  final AttributedString? attributedDecreasedValue;
1908
  final String? tooltip;
1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921
  final SemanticsHintOverrides? hintOverrides;
  final List<SemanticsAction>? actions;
  final List<CustomSemanticsAction>? customActions;
  final List<SemanticsFlag>? flags;
  final TextDirection? textDirection;
  final Rect? rect;
  final Size? size;
  final double? elevation;
  final double? thickness;
  final int? platformViewId;
  final int? maxValueLength;
  final int? currentValueLength;
  final List<Matcher>? children;
1922 1923 1924 1925

  @override
  Description describe(Description description) {
    description.add('has semantics');
1926
    if (label != null) {
1927
      description.add(' with label: $label');
1928 1929
    }
    if (attributedLabel != null) {
1930
      description.add(' with attributedLabel: $attributedLabel');
1931 1932
    }
    if (value != null) {
1933
      description.add(' with value: $value');
1934 1935
    }
    if (attributedValue != null) {
1936
      description.add(' with attributedValue: $attributedValue');
1937 1938
    }
    if (hint != null) {
1939
      description.add(' with hint: $hint');
1940 1941
    }
    if (attributedHint != null) {
1942
      description.add(' with attributedHint: $attributedHint');
1943 1944
    }
    if (increasedValue != null) {
1945
      description.add(' with increasedValue: $increasedValue ');
1946 1947
    }
    if (attributedIncreasedValue != null) {
1948
      description.add(' with attributedIncreasedValue: $attributedIncreasedValue');
1949 1950
    }
    if (decreasedValue != null) {
1951
      description.add(' with decreasedValue: $decreasedValue ');
1952 1953
    }
    if (attributedDecreasedValue != null) {
1954
      description.add(' with attributedDecreasedValue: $attributedDecreasedValue');
1955 1956
    }
    if (tooltip != null) {
1957
      description.add(' with tooltip: $tooltip');
1958 1959
    }
    if (actions != null) {
1960
      description.add(' with actions: ').addDescriptionOf(actions);
1961 1962
    }
    if (flags != null) {
1963
      description.add(' with flags: ').addDescriptionOf(flags);
1964 1965
    }
    if (textDirection != null) {
1966
      description.add(' with textDirection: $textDirection ');
1967 1968
    }
    if (rect != null) {
1969
      description.add(' with rect: $rect');
1970 1971
    }
    if (size != null) {
1972
      description.add(' with size: $size');
1973 1974
    }
    if (elevation != null) {
1975
      description.add(' with elevation: $elevation');
1976 1977
    }
    if (thickness != null) {
1978
      description.add(' with thickness: $thickness');
1979 1980
    }
    if (platformViewId != null) {
1981
      description.add(' with platformViewId: $platformViewId');
1982 1983
    }
    if (maxValueLength != null) {
1984
      description.add(' with maxValueLength: $maxValueLength');
1985 1986
    }
    if (currentValueLength != null) {
1987
      description.add(' with currentValueLength: $currentValueLength');
1988 1989
    }
    if (customActions != null) {
1990
      description.add(' with custom actions: $customActions');
1991 1992
    }
    if (hintOverrides != null) {
1993
      description.add(' with custom hints: $hintOverrides');
1994
    }
1995 1996
    if (children != null) {
      description.add(' with children:\n');
1997
      for (final _MatchesSemanticsData child in children!.cast<_MatchesSemanticsData>()) {
1998
        child.describe(description);
1999
      }
2000
    }
2001 2002 2003
    return description;
  }

2004
  bool _stringAttributesEqual(List<StringAttribute> first, List<StringAttribute> second) {
2005
    if (first.length != second.length) {
2006
      return false;
2007
    }
2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022
    for (int i = 0; i < first.length; i++) {
      if (first[i] is SpellOutStringAttribute &&
          (second[i] is! SpellOutStringAttribute ||
           second[i].range != first[i].range)) {
        return false;
      }
      if (first[i] is LocaleStringAttribute &&
          (second[i] is! LocaleStringAttribute ||
           second[i].range != first[i].range ||
           (second[i] as LocaleStringAttribute).locale != (second[i] as LocaleStringAttribute).locale)) {
        return false;
      }
    }
    return true;
  }
2023 2024

  @override
2025
  bool matches(dynamic node, Map<dynamic, dynamic> matchState) {
2026
    if (node == null) {
2027
      return failWithDescription(matchState, 'No SemanticsData provided. '
2028
        'Maybe you forgot to enable semantics?');
2029
    }
2030
    final SemanticsData data = node is SemanticsNode ? node.getSemanticsData() : (node as SemanticsData);
2031
    if (label != null && label != data.label) {
2032
      return failWithDescription(matchState, 'label was: ${data.label}');
2033
    }
2034 2035 2036 2037 2038 2039
    if (attributedLabel != null &&
        (attributedLabel!.string != data.attributedLabel.string ||
         !_stringAttributesEqual(attributedLabel!.attributes, data.attributedLabel.attributes))) {
      return failWithDescription(
          matchState, 'attributedLabel was: ${data.attributedLabel}');
    }
2040
    if (hint != null && hint != data.hint) {
2041
      return failWithDescription(matchState, 'hint was: ${data.hint}');
2042
    }
2043 2044 2045 2046 2047 2048
    if (attributedHint != null &&
        (attributedHint!.string != data.attributedHint.string ||
         !_stringAttributesEqual(attributedHint!.attributes, data.attributedHint.attributes))) {
      return failWithDescription(
          matchState, 'attributedHint was: ${data.attributedHint}');
    }
2049
    if (value != null && value != data.value) {
2050
      return failWithDescription(matchState, 'value was: ${data.value}');
2051
    }
2052 2053 2054 2055 2056 2057
    if (attributedValue != null &&
        (attributedValue!.string != data.attributedValue.string ||
         !_stringAttributesEqual(attributedValue!.attributes, data.attributedValue.attributes))) {
      return failWithDescription(
          matchState, 'attributedValue was: ${data.attributedValue}');
    }
2058
    if (increasedValue != null && increasedValue != data.increasedValue) {
2059
      return failWithDescription(matchState, 'increasedValue was: ${data.increasedValue}');
2060
    }
2061 2062 2063 2064 2065 2066
    if (attributedIncreasedValue != null &&
        (attributedIncreasedValue!.string != data.attributedIncreasedValue.string ||
         !_stringAttributesEqual(attributedIncreasedValue!.attributes, data.attributedIncreasedValue.attributes))) {
      return failWithDescription(
          matchState, 'attributedIncreasedValue was: ${data.attributedIncreasedValue}');
    }
2067
    if (decreasedValue != null && decreasedValue != data.decreasedValue) {
2068
      return failWithDescription(matchState, 'decreasedValue was: ${data.decreasedValue}');
2069
    }
2070 2071 2072 2073 2074 2075
    if (attributedDecreasedValue != null &&
        (attributedDecreasedValue!.string != data.attributedDecreasedValue.string ||
         !_stringAttributesEqual(attributedDecreasedValue!.attributes, data.attributedDecreasedValue.attributes))) {
      return failWithDescription(
          matchState, 'attributedDecreasedValue was: ${data.attributedDecreasedValue}');
    }
2076
    if (tooltip != null && tooltip != data.tooltip) {
2077
      return failWithDescription(matchState, 'tooltip was: ${data.tooltip}');
2078 2079
    }
    if (textDirection != null && textDirection != data.textDirection) {
2080
      return failWithDescription(matchState, 'textDirection was: $textDirection');
2081 2082
    }
    if (rect != null && rect != data.rect) {
2083
      return failWithDescription(matchState, 'rect was: ${data.rect}');
2084 2085
    }
    if (size != null && size != data.rect.size) {
2086
      return failWithDescription(matchState, 'size was: ${data.rect.size}');
2087 2088
    }
    if (elevation != null && elevation != data.elevation) {
2089
      return failWithDescription(matchState, 'elevation was: ${data.elevation}');
2090 2091
    }
    if (thickness != null && thickness != data.thickness) {
2092
      return failWithDescription(matchState, 'thickness was: ${data.thickness}');
2093 2094
    }
    if (platformViewId != null && platformViewId != data.platformViewId) {
2095
      return failWithDescription(matchState, 'platformViewId was: ${data.platformViewId}');
2096 2097
    }
    if (currentValueLength != null && currentValueLength != data.currentValueLength) {
2098
      return failWithDescription(matchState, 'currentValueLength was: ${data.currentValueLength}');
2099 2100
    }
    if (maxValueLength != null && maxValueLength != data.maxValueLength) {
2101
      return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}');
2102
    }
2103 2104
    if (actions != null) {
      int actionBits = 0;
2105
      for (final SemanticsAction action in actions!) {
2106
        actionBits |= action.index;
2107
      }
2108
      if (actionBits != data.actions) {
2109
        final List<String> actionSummary = <String>[
2110
          for (final SemanticsAction action in SemanticsAction.values.values)
2111 2112 2113
            if ((data.actions & action.index) != 0)
              describeEnum(action),
        ];
2114 2115 2116
        return failWithDescription(matchState, 'actions were: $actionSummary');
      }
    }
2117
    if (customActions != null || hintOverrides != null) {
2118 2119 2120
      final List<CustomSemanticsAction> providedCustomActions = data.customSemanticsActionIds?.map<CustomSemanticsAction>((int id) {
        return CustomSemanticsAction.getAction(id)!;
      }).toList() ?? <CustomSemanticsAction>[];
2121
      final List<CustomSemanticsAction> expectedCustomActions = customActions?.toList() ?? <CustomSemanticsAction>[];
2122
      if (hintOverrides?.onTapHint != null) {
2123
        expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides!.onTapHint!, action: SemanticsAction.tap));
2124 2125
      }
      if (hintOverrides?.onLongPressHint != null) {
2126
        expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides!.onLongPressHint!, action: SemanticsAction.longPress));
2127 2128
      }
      if (expectedCustomActions.length != providedCustomActions.length) {
2129
        return failWithDescription(matchState, 'custom actions where: $providedCustomActions');
2130
      }
2131 2132 2133 2134 2135 2136
      int sortActions(CustomSemanticsAction left, CustomSemanticsAction right) {
        return CustomSemanticsAction.getIdentifier(left) - CustomSemanticsAction.getIdentifier(right);
      }
      expectedCustomActions.sort(sortActions);
      providedCustomActions.sort(sortActions);
      for (int i = 0; i < expectedCustomActions.length; i++) {
2137
        if (expectedCustomActions[i] != providedCustomActions[i]) {
2138
          return failWithDescription(matchState, 'custom actions where: $providedCustomActions');
2139
        }
2140 2141
      }
    }
2142 2143
    if (flags != null) {
      int flagBits = 0;
2144
      for (final SemanticsFlag flag in flags!) {
2145
        flagBits |= flag.index;
2146
      }
2147
      if (flagBits != data.flags) {
2148
        final List<String> flagSummary = <String>[
2149
          for (final SemanticsFlag flag in SemanticsFlag.values.values)
2150 2151 2152
            if ((data.flags & flag.index) != 0)
              describeEnum(flag),
        ];
2153 2154 2155
        return failWithDescription(matchState, 'flags were: $flagSummary');
      }
    }
2156 2157 2158
    bool allMatched = true;
    if (children != null) {
      int i = 0;
2159
      (node as SemanticsNode).visitChildren((SemanticsNode child) {
2160
        allMatched = children![i].matches(child, matchState) && allMatched;
2161 2162 2163 2164 2165
        i += 1;
        return allMatched;
      });
    }
    return allMatched;
2166 2167 2168 2169 2170 2171 2172 2173 2174
  }

  bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
    matchState['failure'] = description;
    return false;
  }

  @override
  Description describeMismatch(
2175 2176 2177
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
2178
    bool verbose,
2179
  ) {
2180
    return mismatchDescription.add(matchState['failure'] as String);
2181
  }
2182
}
2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194

class _MatchesAccessibilityGuideline extends AsyncMatcher {
  _MatchesAccessibilityGuideline(this.guideline);

  final AccessibilityGuideline guideline;

  @override
  Description describe(Description description) {
    return description.add(guideline.description);
  }

  @override
2195
  Future<String?> matchAsync(covariant WidgetTester tester) async {
2196
    final Evaluation result = await guideline.evaluate(tester);
2197
    if (result.passed) {
2198
      return null;
2199
    }
2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210
    return result.reason;
  }
}

class _DoesNotMatchAccessibilityGuideline extends AsyncMatcher {
  _DoesNotMatchAccessibilityGuideline(this.guideline);

  final AccessibilityGuideline guideline;

  @override
  Description describe(Description description) {
2211
    return description.add('Does not ${guideline.description}');
2212 2213 2214
  }

  @override
2215
  Future<String?> matchAsync(covariant WidgetTester tester) async {
2216
    final Evaluation result = await guideline.evaluate(tester);
2217
    if (result.passed) {
2218
      return 'Failed';
2219
    }
2220 2221
    return null;
  }
2222
}