matchers.dart 89.8 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 271 272 273 274 275 276 277 278 279 280
/// Asserts that two [Matrix4]s are equal, within some tolerated error.
///
/// {@macro flutter.flutter_test.moreOrLessEquals}
///
/// See also:
///
///  * [moreOrLessEquals], which is for [double]s.
///  * [offsetMoreOrLessEquals], which is for [Offset]s.
Matcher matrixMoreOrLessEquals(Matrix4 value, { double epsilon = precisionErrorTolerance }) {
  return _IsWithinDistance<Matrix4>(_matrixDistance, value, epsilon);
}

281 282
/// Asserts that two [Offset]s are equal, within some tolerated error.
///
283
/// {@macro flutter.flutter_test.moreOrLessEquals}
284 285 286 287 288 289 290 291 292 293 294
///
/// 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);
}

295 296
/// Asserts that two [String]s or `Iterable<String>`s are equal after
/// normalizing likely hash codes.
297 298
///
/// A `#` followed by 5 hexadecimal digits is assumed to be a short hash code
299
/// and is normalized to `#00000`.
300
///
301 302
/// Only [String] or `Iterable<String>` are allowed types for `value`.
///
303 304 305
/// See Also:
///
///  * [describeIdentity], a method that generates short descriptions of objects
306
///    with ids that match the pattern `#[0-9a-f]{5}`.
307 308
///  * [shortHash], a method that generates a 5 character long hexadecimal
///    [String] based on [Object.hashCode].
309
///  * [DiagnosticableTree.toStringDeep], a method that returns a [String]
310
///    typically containing multiple hash codes.
311 312
Matcher equalsIgnoringHashCodes(Object value) {
  assert(value is String || value is Iterable<String>, "Only String or Iterable<String> are allowed types for equalsIgnoringHashCodes, it doesn't accept ${value.runtimeType}");
313
  return _EqualsIgnoringHashCodes(value);
314 315
}

316 317 318 319
/// 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.
320
Matcher isMethodCall(String name, { required dynamic arguments }) {
321
  return _IsMethodCall(name, arguments);
322 323
}

324 325 326 327 328 329 330 331 332
/// 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.
333
Matcher coversSameAreaAs(Path expectedPath, { required Rect areaToCompare, int sampleSize = 20 })
334
  => _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);
335

336
/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the
337
/// golden image file identified by [key], with an optional [version] number.
338 339 340
///
/// 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
341 342
/// 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.
343
///
344
/// The [key] may be either a [Uri] or a [String] representation of a URL.
345
///
346
/// The [version] is a number that can be used to differentiate historical
347
/// golden files. This parameter is optional.
348
///
349 350 351 352
/// This is an asynchronous matcher, meaning that callers should use
/// [expectLater] when using this matcher and await the future returned by
/// [expectLater].
///
353 354 355 356 357 358 359 360 361
/// ## 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.
///
362
/// {@tool snippet}
363
/// Sample invocations of [matchesGoldenFile].
364 365
///
/// ```dart
366 367 368 369 370 371 372 373 374 375 376 377
/// await expectLater(
///   find.text('Save'),
///   matchesGoldenFile('save.png'),
/// );
///
/// await expectLater(
///   image,
///   matchesGoldenFile('save.png'),
/// );
///
/// await expectLater(
///   imageFuture,
378 379 380 381 382
///   matchesGoldenFile(
///     'save.png',
///     version: 2,
///   ),
/// );
383 384 385 386 387
///
/// await expectLater(
///   find.byType(MyWidget),
///   matchesGoldenFile('goldens/myWidget.png'),
/// );
388
/// ```
389
/// {@end-tool}
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 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
/// {@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}
///
446 447
/// See also:
///
448 449 450
///  * [GoldenFileComparator], which acts as the backend for this matcher.
///  * [LocalFileComparator], which is the default [GoldenFileComparator]
///    implementation for `flutter test`.
451 452
///  * [matchesReferenceImage], which should be used instead if you want to
///    verify that two different code paths create identical images.
453 454
///  * [flutter_test] for a discussion of test configurations, whereby callers
///    may swap out the backend for this matcher.
455
AsyncMatcher matchesGoldenFile(Object key, {int? version}) {
456
  if (key is Uri) {
457
    return MatchesGoldenFile(key, version);
458
  } else if (key is String) {
459
    return MatchesGoldenFile.forStringPath(key, version);
460
  }
461
  throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
462
}
463

464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
/// 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);
}

500
/// Asserts that a [SemanticsNode] contains the specified information.
501 502 503 504 505
///
/// 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.
///
506
/// To retrieve the semantics data of a widget, use [WidgetTester.getSemantics]
507 508 509 510 511 512 513
/// 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();
514
/// expect(tester.getSemantics(find.text('hello')), matchesSemantics(label: 'hello'));
515 516 517 518 519
/// handle.dispose();
/// ```
///
/// See also:
///
520
///   * [WidgetTester.getSemantics], the tester method which retrieves semantics.
521
///   * [containsSemantics], a similar matcher without default values for flags or actions.
522
Matcher matchesSemantics({
523
  String? label,
524
  AttributedString? attributedLabel,
525
  String? hint,
526
  AttributedString? attributedHint,
527
  String? value,
528
  AttributedString? attributedValue,
529
  String? increasedValue,
530
  AttributedString? attributedIncreasedValue,
531
  String? decreasedValue,
532
  AttributedString? attributedDecreasedValue,
533
  String? tooltip,
534 535 536 537 538 539 540 541
  TextDirection? textDirection,
  Rect? rect,
  Size? size,
  double? elevation,
  double? thickness,
  int? platformViewId,
  int? maxValueLength,
  int? currentValueLength,
542 543 544 545 546
  // Flags //
  bool hasCheckedState = false,
  bool isChecked = false,
  bool isSelected = false,
  bool isButton = false,
547
  bool isSlider = false,
548
  bool isKeyboardKey = false,
549
  bool isLink = false,
550
  bool isFocused = false,
551
  bool isFocusable = false,
552
  bool isTextField = false,
553
  bool isReadOnly = false,
554 555 556 557 558
  bool hasEnabledState = false,
  bool isEnabled = false,
  bool isInMutuallyExclusiveGroup = false,
  bool isHeader = false,
  bool isObscured = false,
559
  bool isMultiline = false,
560 561 562
  bool namesRoute = false,
  bool scopesRoute = false,
  bool isHidden = false,
563 564 565 566
  bool isImage = false,
  bool isLiveRegion = false,
  bool hasToggledState = false,
  bool isToggled = false,
567
  bool hasImplicitScrolling = false,
568 569 570 571 572 573 574 575 576 577 578 579
  // 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,
580 581
  bool hasMoveCursorForwardByWordAction = false,
  bool hasMoveCursorBackwardByWordAction = false,
582
  bool hasSetTextAction = false,
583 584 585 586 587 588
  bool hasSetSelectionAction = false,
  bool hasCopyAction = false,
  bool hasCutAction = false,
  bool hasPasteAction = false,
  bool hasDidGainAccessibilityFocusAction = false,
  bool hasDidLoseAccessibilityFocusAction = false,
589
  bool hasDismissAction = false,
590
  // Custom actions and overrides
591 592 593 594
  String? onTapHint,
  String? onLongPressHint,
  List<CustomSemanticsAction>? customActions,
  List<Matcher>? children,
595
}) {
596 597
  return _MatchesSemanticsData(
    label: label,
598
    attributedLabel: attributedLabel,
599
    hint: hint,
600
    attributedHint: attributedHint,
601
    value: value,
602
    attributedValue: attributedValue,
603
    increasedValue: increasedValue,
604
    attributedIncreasedValue: attributedIncreasedValue,
605
    decreasedValue: decreasedValue,
606
    attributedDecreasedValue: attributedDecreasedValue,
607
    tooltip: tooltip,
608 609 610
    textDirection: textDirection,
    rect: rect,
    size: size,
611 612
    elevation: elevation,
    thickness: thickness,
613
    platformViewId: platformViewId,
614
    customActions: customActions,
615
    maxValueLength: maxValueLength,
616
    currentValueLength: currentValueLength,
617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785
    // Flags
    hasCheckedState: hasCheckedState,
    isChecked: isChecked,
    isSelected: isSelected,
    isButton: isButton,
    isSlider: isSlider,
    isKeyboardKey: isKeyboardKey,
    isLink: isLink,
    isFocused: isFocused,
    isFocusable: isFocusable,
    isTextField: isTextField,
    isReadOnly: isReadOnly,
    hasEnabledState: hasEnabledState,
    isEnabled: isEnabled,
    isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup,
    isHeader: isHeader,
    isObscured: isObscured,
    isMultiline: isMultiline,
    namesRoute: namesRoute,
    scopesRoute: scopesRoute,
    isHidden: isHidden,
    isImage: isImage,
    isLiveRegion: isLiveRegion,
    hasToggledState: hasToggledState,
    isToggled: isToggled,
    hasImplicitScrolling: hasImplicitScrolling,
    // Actions
    hasTapAction: hasTapAction,
    hasLongPressAction: hasLongPressAction,
    hasScrollLeftAction: hasScrollLeftAction,
    hasScrollRightAction: hasScrollRightAction,
    hasScrollUpAction: hasScrollUpAction,
    hasScrollDownAction: hasScrollDownAction,
    hasIncreaseAction: hasIncreaseAction,
    hasDecreaseAction: hasDecreaseAction,
    hasShowOnScreenAction: hasShowOnScreenAction,
    hasMoveCursorForwardByCharacterAction: hasMoveCursorForwardByCharacterAction,
    hasMoveCursorBackwardByCharacterAction: hasMoveCursorBackwardByCharacterAction,
    hasMoveCursorForwardByWordAction: hasMoveCursorForwardByWordAction,
    hasMoveCursorBackwardByWordAction: hasMoveCursorBackwardByWordAction,
    hasSetTextAction: hasSetTextAction,
    hasSetSelectionAction: hasSetSelectionAction,
    hasCopyAction: hasCopyAction,
    hasCutAction: hasCutAction,
    hasPasteAction: hasPasteAction,
    hasDidGainAccessibilityFocusAction: hasDidGainAccessibilityFocusAction,
    hasDidLoseAccessibilityFocusAction: hasDidLoseAccessibilityFocusAction,
    hasDismissAction: hasDismissAction,
    // Custom actions and overrides
    children: children,
    onLongPressHint: onLongPressHint,
    onTapHint: onTapHint,
  );
}

/// Asserts that a [SemanticsNode] contains the specified information.
///
/// There are no default expected values, so no unspecified values will be
/// validated.
///
/// To retrieve the semantics data of a widget, use [WidgetTester.getSemantics]
/// 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();
/// expect(tester.getSemantics(find.text('hello')), hasSemantics(label: 'hello'));
/// handle.dispose();
/// ```
///
/// See also:
///
///   * [WidgetTester.getSemantics], the tester method which retrieves semantics.
///   * [matchesSemantics], a similar matcher with default values for flags and actions.
Matcher containsSemantics({
  String? label,
  AttributedString? attributedLabel,
  String? hint,
  AttributedString? attributedHint,
  String? value,
  AttributedString? attributedValue,
  String? increasedValue,
  AttributedString? attributedIncreasedValue,
  String? decreasedValue,
  AttributedString? attributedDecreasedValue,
  String? tooltip,
  TextDirection? textDirection,
  Rect? rect,
  Size? size,
  double? elevation,
  double? thickness,
  int? platformViewId,
  int? maxValueLength,
  int? currentValueLength,
  // Flags
  bool? hasCheckedState,
  bool? isChecked,
  bool? isSelected,
  bool? isButton,
  bool? isSlider,
  bool? isKeyboardKey,
  bool? isLink,
  bool? isFocused,
  bool? isFocusable,
  bool? isTextField,
  bool? isReadOnly,
  bool? hasEnabledState,
  bool? isEnabled,
  bool? isInMutuallyExclusiveGroup,
  bool? isHeader,
  bool? isObscured,
  bool? isMultiline,
  bool? namesRoute,
  bool? scopesRoute,
  bool? isHidden,
  bool? isImage,
  bool? isLiveRegion,
  bool? hasToggledState,
  bool? isToggled,
  bool? hasImplicitScrolling,
  // Actions
  bool? hasTapAction,
  bool? hasLongPressAction,
  bool? hasScrollLeftAction,
  bool? hasScrollRightAction,
  bool? hasScrollUpAction,
  bool? hasScrollDownAction,
  bool? hasIncreaseAction,
  bool? hasDecreaseAction,
  bool? hasShowOnScreenAction,
  bool? hasMoveCursorForwardByCharacterAction,
  bool? hasMoveCursorBackwardByCharacterAction,
  bool? hasMoveCursorForwardByWordAction,
  bool? hasMoveCursorBackwardByWordAction,
  bool? hasSetTextAction,
  bool? hasSetSelectionAction,
  bool? hasCopyAction,
  bool? hasCutAction,
  bool? hasPasteAction,
  bool? hasDidGainAccessibilityFocusAction,
  bool? hasDidLoseAccessibilityFocusAction,
  bool? hasDismissAction,
  // Custom actions and overrides
  String? onTapHint,
  String? onLongPressHint,
  List<CustomSemanticsAction>? customActions,
  List<Matcher>? children,
}) {
  return _MatchesSemanticsData(
    label: label,
    attributedLabel: attributedLabel,
    hint: hint,
    attributedHint: attributedHint,
    value: value,
    attributedValue: attributedValue,
    increasedValue: increasedValue,
    attributedIncreasedValue: attributedIncreasedValue,
    decreasedValue: decreasedValue,
    attributedDecreasedValue: attributedDecreasedValue,
    tooltip: tooltip,
    textDirection: textDirection,
    rect: rect,
    size: size,
    elevation: elevation,
    thickness: thickness,
    platformViewId: platformViewId,
    customActions: customActions,
786
    maxValueLength: maxValueLength,
787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836
    currentValueLength: currentValueLength,
    // Flags
    hasCheckedState: hasCheckedState,
    isChecked: isChecked,
    isSelected: isSelected,
    isButton: isButton,
    isSlider: isSlider,
    isKeyboardKey: isKeyboardKey,
    isLink: isLink,
    isFocused: isFocused,
    isFocusable: isFocusable,
    isTextField: isTextField,
    isReadOnly: isReadOnly,
    hasEnabledState: hasEnabledState,
    isEnabled: isEnabled,
    isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup,
    isHeader: isHeader,
    isObscured: isObscured,
    isMultiline: isMultiline,
    namesRoute: namesRoute,
    scopesRoute: scopesRoute,
    isHidden: isHidden,
    isImage: isImage,
    isLiveRegion: isLiveRegion,
    hasToggledState: hasToggledState,
    isToggled: isToggled,
    hasImplicitScrolling: hasImplicitScrolling,
    // Actions
    hasTapAction: hasTapAction,
    hasLongPressAction: hasLongPressAction,
    hasScrollLeftAction: hasScrollLeftAction,
    hasScrollRightAction: hasScrollRightAction,
    hasScrollUpAction: hasScrollUpAction,
    hasScrollDownAction: hasScrollDownAction,
    hasIncreaseAction: hasIncreaseAction,
    hasDecreaseAction: hasDecreaseAction,
    hasShowOnScreenAction: hasShowOnScreenAction,
    hasMoveCursorForwardByCharacterAction: hasMoveCursorForwardByCharacterAction,
    hasMoveCursorBackwardByCharacterAction: hasMoveCursorBackwardByCharacterAction,
    hasMoveCursorForwardByWordAction: hasMoveCursorForwardByWordAction,
    hasMoveCursorBackwardByWordAction: hasMoveCursorBackwardByWordAction,
    hasSetTextAction: hasSetTextAction,
    hasSetSelectionAction: hasSetSelectionAction,
    hasCopyAction: hasCopyAction,
    hasCutAction: hasCutAction,
    hasPasteAction: hasPasteAction,
    hasDidGainAccessibilityFocusAction: hasDidGainAccessibilityFocusAction,
    hasDidLoseAccessibilityFocusAction: hasDidLoseAccessibilityFocusAction,
    hasDismissAction: hasDismissAction,
    // Custom actions and overrides
837
    children: children,
838 839
    onLongPressHint: onLongPressHint,
    onTapHint: onTapHint,
840 841 842
  );
}

843 844 845 846 847 848 849 850 851 852
/// 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();
853
/// await expectLater(tester, meetsGuideline(textContrastGuideline));
854 855 856 857 858
/// handle.dispose();
/// ```
///
/// Supported accessibility guidelines:
///
859 860
///   * [androidTapTargetGuideline], for Android minimum tappable area guidelines.
///   * [iOSTapTargetGuideline], for iOS minimum tappable area guidelines.
861
///   * [textContrastGuideline], for WCAG minimum text contrast guidelines.
862
///   * [labeledTapTargetGuideline], for enforcing labels on tappable areas.
863
AsyncMatcher meetsGuideline(AccessibilityGuideline guideline) {
864
  return _MatchesAccessibilityGuideline(guideline);
865 866 867 868 869 870 871
}

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

875 876 877
class _FindsWidgetMatcher extends Matcher {
  const _FindsWidgetMatcher(this.min, this.max);

878 879
  final int? min;
  final int? max;
880 881

  @override
882
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
883
    assert(min != null || max != null);
884
    assert(min == null || max == null || min! <= max!);
885
    matchState[Finder] = finder;
886
    int count = 0;
887
    final Iterator<Element> iterator = finder.evaluate().iterator;
888
    if (min != null) {
889
      while (count < min! && iterator.moveNext()) {
890
        count += 1;
891 892
      }
      if (count < min!) {
893
        return false;
894
      }
895 896
    }
    if (max != null) {
897
      while (count <= max! && iterator.moveNext()) {
898
        count += 1;
899 900
      }
      if (count > max!) {
901
        return false;
902
      }
903 904 905 906 907 908 909 910
    }
    return true;
  }

  @override
  Description describe(Description description) {
    assert(min != null || max != null);
    if (min == max) {
911
      if (min == 1) {
912
        return description.add('exactly one matching node in the widget tree');
913
      }
914 915 916
      return description.add('exactly $min matching nodes in the widget tree');
    }
    if (min == null) {
917
      if (max == 0) {
918
        return description.add('no matching nodes in the widget tree');
919 920
      }
      if (max == 1) {
921
        return description.add('at most one matching node in the widget tree');
922
      }
923 924 925
      return description.add('at most $max matching nodes in the widget tree');
    }
    if (max == null) {
926
      if (min == 1) {
927
        return description.add('at least one matching node in the widget tree');
928
      }
929 930 931 932 933 934 935 936 937 938
      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,
939
    bool verbose,
940
  ) {
941
    final Finder finder = matchState[Finder] as Finder;
942
    final int count = finder.evaluate().length;
943
    if (count == 0) {
944
      assert(min != null && min! > 0);
945
      if (min == 1 && max == 1) {
946
        return mismatchDescription.add('means none were found but one was expected');
947
      }
948 949 950
      return mismatchDescription.add('means none were found but some were expected');
    }
    if (max == 0) {
951
      if (count == 1) {
952
        return mismatchDescription.add('means one was found but none were expected');
953
      }
954 955
      return mismatchDescription.add('means some were found but none were expected');
    }
956
    if (min != null && count < min!) {
957
      return mismatchDescription.add('is not enough');
958
    }
959
    assert(max != null && count > min!);
960 961 962 963
    return mismatchDescription.add('is too many');
  }
}

964
bool _hasAncestorMatching(Finder finder, bool Function(Widget widget) predicate) {
965
  final Iterable<Element> nodes = finder.evaluate();
966
  if (nodes.length != 1) {
967
    return false;
968
  }
969
  bool result = false;
970
  nodes.single.visitAncestorElements((Element ancestor) {
971
    if (predicate(ancestor.widget)) {
972 973 974 975 976 977 978 979
      result = true;
      return false;
    }
    return true;
  });
  return result;
}

980 981 982 983
bool _hasAncestorOfType(Finder finder, Type targetType) {
  return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType);
}

984 985
class _IsOffstage extends Matcher {
  const _IsOffstage();
986 987

  @override
988
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
989
    return _hasAncestorMatching(finder, (Widget widget) {
990
      if (widget is Offstage) {
991
        return widget.offstage;
992
      }
993
      return false;
994 995
    });
  }
996 997 998 999 1000

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

1001 1002
class _IsOnstage extends Matcher {
  const _IsOnstage();
1003 1004

  @override
1005
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
1006
    final Iterable<Element> nodes = finder.evaluate();
1007
    if (nodes.length != 1) {
1008
      return false;
1009
    }
1010
    bool result = true;
1011
    nodes.single.visitAncestorElements((Element ancestor) {
1012
      final Widget widget = ancestor.widget;
1013 1014
      if (widget is Offstage) {
        result = !widget.offstage;
1015 1016 1017 1018 1019 1020
        return false;
      }
      return true;
    });
    return result;
  }
1021 1022 1023 1024 1025 1026 1027 1028 1029

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

class _IsInCard extends Matcher {
  const _IsInCard();

  @override
1030
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => _hasAncestorOfType(finder, Card);
1031 1032 1033 1034 1035 1036 1037 1038 1039

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

class _IsNotInCard extends Matcher {
  const _IsNotInCard();

  @override
1040
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => !_hasAncestorOfType(finder, Card);
1041 1042 1043 1044

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

1046 1047
class _HasOneLineDescription extends Matcher {
  const _HasOneLineDescription();
1048 1049

  @override
1050
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
1051
    final String description = object.toString();
1052 1053
    return description.isNotEmpty
        && !description.contains('\n')
1054
        && !description.contains('Instance of ')
1055
        && description.trim() == description;
1056 1057 1058 1059 1060
  }

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

1062
class _EqualsIgnoringHashCodes extends Matcher {
1063
  _EqualsIgnoringHashCodes(Object v) : _value = _normalize(v);
1064

1065
  final Object _value;
1066

1067
  static final Object _mismatchedValueKey = Object();
1068

1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082
  static String _normalizeString(String value) {
    return value.replaceAll(RegExp(r'#[\da-fA-F]{5}'), '#00000');
  }

  static Object _normalize(Object value, {bool expected = true}) {
    if (value is String) {
      return _normalizeString(value);
    }
    if (value is Iterable<String>) {
      return value.map<String>((dynamic item) => _normalizeString(item.toString()));
    }
    throw ArgumentError('The specified ${expected ? 'expected' : 'comparison'} value for '
        'equalsIgnoringHashCodes must be a String or an Iterable<String>, '
        'not a ${value.runtimeType}');
1083 1084 1085 1086
  }

  @override
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
1087 1088 1089
    final Object normalized = _normalize(object as Object, expected: false);
    if (!equals(_value).matches(normalized, matchState)) {
      matchState[_mismatchedValueKey] = normalized;
1090 1091 1092 1093 1094 1095 1096
      return false;
    }
    return true;
  }

  @override
  Description describe(Description description) {
1097 1098 1099 1100
    if (_value is String) {
      return description.add('normalized value matches $_value');
    }
    return description.add('normalized value matches\n').addDescriptionOf(_value);
1101 1102 1103 1104 1105 1106 1107
  }

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
1108
    bool verbose,
1109 1110
  ) {
    if (matchState.containsKey(_mismatchedValueKey)) {
1111
      final Object actualValue = matchState[_mismatchedValueKey] as Object;
1112
      // Leading whitespace is added so that lines in the multiline
1113 1114 1115
      // description returned by addDescriptionOf are all indented equally
      // which makes the output easier to read for this case.
      return mismatchDescription
1116
          .add('was expected to be normalized value\n')
1117
          .addDescriptionOf(_value)
1118
          .add('\nbut got\n')
1119 1120 1121 1122 1123 1124
          .addDescriptionOf(actualValue);
    }
    return mismatchDescription;
  }
}

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

1128
/// Returns true if [c] represents a vertical line Unicode line art code unit.
1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
///
/// 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);
1146
    if (!_isWhitespace(c) && !_isVerticalLine(c)) {
1147
      return false;
1148
    }
1149 1150 1151 1152 1153 1154 1155
  }
  return true;
}

class _HasGoodToStringDeep extends Matcher {
  const _HasGoodToStringDeep();

1156
  static final Object _toStringDeepErrorDescriptionKey = Object();
1157 1158 1159 1160

  @override
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
    final List<String> issues = <String>[];
1161
    String description = object.toStringDeep() as String; // ignore: avoid_dynamic_calls
1162 1163 1164 1165 1166 1167 1168 1169
    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.');
    }

1170
    if (description.trim() != description) {
1171
      issues.add('Has trailing whitespace.');
1172
    }
1173 1174

    final List<String> lines = description.split('\n');
1175
    if (lines.length < 2) {
1176
      issues.add('Does not have multiple lines.');
1177
    }
1178

1179
    if (description.contains('Instance of ')) {
1180
      issues.add('Contains text "Instance of ".');
1181
    }
1182 1183 1184

    for (int i = 0; i < lines.length; ++i) {
      final String line = lines[i];
1185
      if (line.isEmpty) {
1186
        issues.add('Line ${i + 1} is empty.');
1187
      }
1188

1189
      if (line.trimRight() != line) {
1190
        issues.add('Line ${i + 1} has trailing whitespace.');
1191
      }
1192 1193
    }

1194
    if (_isAllTreeConnectorCharacters(lines.last)) {
1195
      issues.add('Last line is all tree connector characters.');
1196
    }
1197 1198

    // If a toStringDeep method doesn't properly handle nested values that
1199
    // contain line breaks it can fail to add the required prefixes to all
1200
    // lined when toStringDeep is called specifying prefixes.
1201
    const String prefixLineOne = 'PREFIX_LINE_ONE____';
1202
    const String prefixOtherLines = 'PREFIX_OTHER_LINES_';
1203
    final List<String> prefixIssues = <String>[];
1204 1205
    // ignore: avoid_dynamic_calls
    String descriptionWithPrefixes = object.toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines) as String;
1206 1207 1208 1209 1210 1211 1212
    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');
1213
    if (!linesWithPrefixes.first.startsWith(prefixLineOne)) {
1214
      prefixIssues.add('First line does not contain expected prefix.');
1215
    }
1216 1217

    for (int i = 1; i < linesWithPrefixes.length; ++i) {
1218
      if (!linesWithPrefixes[i].startsWith(prefixOtherLines)) {
1219
        prefixIssues.add('Line ${i + 1} does not contain the expected prefix.');
1220
      }
1221 1222
    }

1223
    final StringBuffer errorDescription = StringBuffer();
1224 1225 1226 1227 1228 1229 1230 1231
    if (issues.isNotEmpty) {
      errorDescription.writeln('Bad toStringDeep():');
      errorDescription.writeln(description);
      errorDescription.writeAll(issues, '\n');
    }

    if (prefixIssues.isNotEmpty) {
      errorDescription.writeln(
1232
          'Bad toStringDeep(prefixLineOne: "$prefixLineOne", prefixOtherLines: "$prefixOtherLines"):');
1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249
      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,
1250
    bool verbose,
1251 1252
  ) {
    if (matchState.containsKey(_toStringDeepErrorDescriptionKey)) {
1253
      return mismatchDescription.add(matchState[_toStringDeepErrorDescriptionKey] as String);
1254 1255 1256 1257 1258 1259 1260 1261 1262 1263
    }
    return mismatchDescription;
  }

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

1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276
/// 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.
1277
typedef DistanceFunction<T> = num Function(T a, T b);
1278

1279 1280 1281 1282
/// 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>]
1283
/// functions which have (potentially) unrelated argument types. Since the
1284 1285 1286
/// 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.
1287 1288 1289
///
/// Calling an instance of this type must either be done dynamically, or by
/// first casting it to a [DistanceFunction<T>] for some concrete T.
1290
typedef AnyDistanceFunction = num Function(Never a, Never b);
1291

1292
const Map<Type, AnyDistanceFunction> _kStandardDistanceFunctions = <Type, AnyDistanceFunction>{
1293
  Color: _maxComponentColorDistance,
1294 1295
  HSVColor: _maxComponentHSVColorDistance,
  HSLColor: _maxComponentHSLColorDistance,
1296 1297 1298
  Offset: _offsetDistance,
  int: _intDistance,
  double: _doubleDistance,
1299
  Rect: _rectDistance,
1300
  Size: _sizeDistance,
1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313
};

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();
}

1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329
// 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());
}

1330 1331 1332 1333 1334 1335 1336
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;
}

1337 1338 1339 1340 1341 1342 1343 1344
double _matrixDistance(Matrix4 a, Matrix4 b) {
  double delta = 0.0;
  for (int i = 0; i < 16; i += 1) {
    delta = math.max<double>((a[i] - b[i]).abs(), delta);
  }
  return delta;
}

1345
double _sizeDistance(Size a, Size b) {
1346 1347 1348
  // 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;
1349
  return delta.distance;
1350 1351
}

1352 1353 1354 1355 1356
/// 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
1357 1358
/// `T` generic argument. Standard functions are defined for the following
/// types:
1359 1360 1361 1362
///
///  * [Color], whose distance is the maximum component-wise delta.
///  * [Offset], whose distance is the Euclidean distance computed using the
///    method [Offset.distance].
1363 1364 1365
///  * [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.
1366 1367 1368 1369 1370 1371 1372
///  * [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.
1373 1374
///  * [rectMoreOrLessEquals], which is similar to this function, but
///    specializes in [Rect]s and has an optional `epsilon` parameter.
1375 1376
///  * [closeTo], which specializes in numbers only.
Matcher within<T>({
1377 1378 1379
  required num distance,
  required T from,
  DistanceFunction<T>? distanceFunction,
1380
}) {
1381
  distanceFunction ??= _kStandardDistanceFunctions[T] as DistanceFunction<T>?;
1382 1383

  if (distanceFunction == null) {
1384
    throw ArgumentError(
1385 1386 1387 1388 1389 1390
      'The specified distanceFunction was null, and a standard distance '
      'function was not found for type ${from.runtimeType} of the provided '
      '`from` argument.'
    );
  }

1391
  return _IsWithinDistance<T>(distanceFunction, from, distance);
1392 1393 1394 1395 1396 1397 1398 1399 1400 1401
}

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

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

  @override
1402
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
1403
    if (object is! T) {
1404
      return false;
1405 1406
    }
    if (object == value) {
1407
      return true;
1408
    }
1409
    final num distance = distanceFunction(object, value);
1410
    if (distance < 0) {
1411
      throw ArgumentError(
1412 1413 1414 1415 1416
        '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.'
      );
    }
1417
    matchState['distance'] = distance;
1418 1419 1420 1421 1422
    return distance <= epsilon;
  }

  @override
  Description describe(Description description) => description.add('$value$epsilon)');
1423 1424 1425

  @override
  Description describeMismatch(
1426
    dynamic object,
1427 1428 1429 1430 1431 1432 1433
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
    bool verbose,
  ) {
    mismatchDescription.add('was ${matchState['distance']} away from the desired value.');
    return mismatchDescription;
  }
1434 1435
}

1436
class _MoreOrLessEquals extends Matcher {
1437 1438
  const _MoreOrLessEquals(this.value, this.epsilon)
    : assert(epsilon >= 0);
1439 1440 1441 1442 1443

  final double value;
  final double epsilon;

  @override
1444
  bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
1445
    if (object is! double) {
1446
      return false;
1447 1448
    }
    if (object == value) {
1449
      return true;
1450
    }
1451
    return (object - value).abs() <= epsilon;
1452 1453 1454 1455
  }

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

  @override
1458
  Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
1459 1460 1461
    return super.describeMismatch(item, mismatchDescription, matchState, verbose)
      ..add('$item is not in the range of $value$epsilon).');
  }
1462
}
1463 1464 1465 1466 1467 1468 1469 1470 1471

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) {
1472
    if (item is! MethodCall) {
1473
      return false;
1474 1475
    }
    if (item.method != name) {
1476
      return false;
1477
    }
1478 1479 1480 1481
    return _deepEquals(item.arguments, arguments);
  }

  bool _deepEquals(dynamic a, dynamic b) {
1482
    if (a == b) {
1483
      return true;
1484 1485
    }
    if (a is List) {
1486
      return b is List && _deepEqualsList(a, b);
1487 1488
    }
    if (a is Map) {
1489
      return b is Map && _deepEqualsMap(a, b);
1490
    }
1491 1492 1493 1494
    return false;
  }

  bool _deepEqualsList(List<dynamic> a, List<dynamic> b) {
1495
    if (a.length != b.length) {
1496
      return false;
1497
    }
1498
    for (int i = 0; i < a.length; i++) {
1499
      if (!_deepEquals(a[i], b[i])) {
1500
        return false;
1501
      }
1502 1503 1504 1505 1506
    }
    return true;
  }

  bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) {
1507
    if (a.length != b.length) {
1508
      return false;
1509
    }
1510
    for (final dynamic key in a.keys) {
1511
      if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) {
1512
        return false;
1513
      }
1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525
    }
    return true;
  }

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

1526
/// Asserts that a [Finder] locates a single object whose root RenderObject
1527 1528
/// is a [RenderClipRect] with no clipper set, or an equivalent
/// [RenderClipPath].
1529
const Matcher clipsWithBoundingRect = _ClipsWithBoundingRect();
1530

1531 1532 1533 1534 1535
/// Asserts that a [Finder] locates a single object whose root RenderObject is
/// not a [RenderClipRect], [RenderClipRRect], [RenderClipOval], or
/// [RenderClipPath].
const Matcher hasNoImmediateClip = _MatchAnythingExceptClip();

1536 1537
/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderClipRRect] with no clipper set, and border radius equals to
1538
/// [borderRadius], or an equivalent [RenderClipPath].
1539
Matcher clipsWithBoundingRRect({ required BorderRadius borderRadius }) {
1540
  return _ClipsWithBoundingRRect(borderRadius: borderRadius);
1541 1542 1543
}

/// Asserts that a [Finder] locates a single object whose root RenderObject
1544
/// is a [RenderClipPath] with a [ShapeBorderClipper] that clips to
1545
/// [shape].
1546
Matcher clipsWithShapeBorder({ required ShapeBorder shape }) {
1547
  return _ClipsWithShapeBorder(shape: shape);
1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566
}

/// 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].
1567
Matcher rendersOnPhysicalModel({
1568 1569 1570
  BoxShape? shape,
  BorderRadius? borderRadius,
  double? elevation,
1571
}) {
1572
  return _RendersOnPhysicalModel(
1573 1574 1575 1576 1577 1578
    shape: shape,
    borderRadius: borderRadius,
    elevation: elevation,
  );
}

1579 1580 1581 1582 1583 1584
/// 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({
1585 1586
  required ShapeBorder shape,
  double? elevation,
1587
}) {
1588
  return _RendersOnPhysicalShape(
1589 1590 1591 1592 1593
    shape: shape,
    elevation: elevation,
  );
}

1594 1595 1596 1597 1598 1599 1600 1601 1602 1603
abstract class _FailWithDescriptionMatcher extends Matcher {
  const _FailWithDescriptionMatcher();

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

  @override
  Description describeMismatch(
1604 1605 1606
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
1607
    bool verbose,
1608
  ) {
1609
    return mismatchDescription.add(matchState['failure'] as String);
1610 1611 1612 1613 1614 1615 1616 1617 1618
  }
}

class _MatchAnythingExceptClip extends _FailWithDescriptionMatcher {
  const _MatchAnythingExceptClip();

  @override
  bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
    final Iterable<Element> nodes = finder.evaluate();
1619
    if (nodes.length != 1) {
1620
      return failWithDescription(matchState, 'did not have a exactly one child element');
1621
    }
1622
    final RenderObject renderObject = nodes.single.renderObject!;
1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636

    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) {
1637
    return description.add('does not have a clip as an immediate child');
1638 1639 1640 1641
  }
}

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

1644 1645
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, T renderObject);
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, M renderObject);
1646 1647 1648 1649

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

1655
    if (renderObject.runtimeType == T) {
1656
      return renderObjectMatchesT(matchState, renderObject as T);
1657
    }
1658

1659
    if (renderObject.runtimeType == M) {
1660
      return renderObjectMatchesM(matchState, renderObject as M);
1661
    }
1662 1663

    return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
1664 1665 1666
  }
}

1667
class _RendersOnPhysicalModel extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
1668 1669 1670 1671 1672 1673
  const _RendersOnPhysicalModel({
    this.shape,
    this.borderRadius,
    this.elevation,
  });

1674 1675 1676
  final BoxShape? shape;
  final BorderRadius? borderRadius;
  final double? elevation;
1677 1678

  @override
1679
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) {
1680
    if (shape != null && renderObject.shape != shape) {
1681
      return failWithDescription(matchState, 'had shape: ${renderObject.shape}');
1682
    }
1683

1684
    if (borderRadius != null && renderObject.borderRadius != borderRadius) {
1685
      return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');
1686
    }
1687

1688
    if (elevation != null && renderObject.elevation != elevation) {
1689
      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
1690
    }
1691 1692 1693 1694

    return true;
  }

1695 1696
  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
1697
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1698
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1699
    }
1700
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1701

1702
    if (borderRadius != null && !assertRoundedRectangle(shapeClipper, borderRadius!, matchState)) {
1703
      return false;
1704
    }
1705

1706
    if (borderRadius == null &&
1707
      shape == BoxShape.rectangle &&
1708
      !assertRoundedRectangle(shapeClipper, BorderRadius.zero, matchState)) {
1709
      return false;
1710
    }
1711

1712
    if (borderRadius == null &&
1713
      shape == BoxShape.circle &&
1714
      !assertCircle(shapeClipper, matchState)) {
1715
      return false;
1716
    }
1717

1718
    if (elevation != null && renderObject.elevation != elevation) {
1719
      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
1720
    }
1721 1722 1723 1724 1725

    return true;
  }

  bool assertRoundedRectangle(ShapeBorderClipper shapeClipper, BorderRadius borderRadius, Map<dynamic, dynamic> matchState) {
1726
    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) {
1727
      return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
1728
    }
1729
    final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
1730
    if (border.borderRadius != borderRadius) {
1731
      return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
1732
    }
1733
    return true;
1734 1735 1736
  }

  bool assertCircle(ShapeBorderClipper shapeClipper, Map<dynamic, dynamic> matchState) {
1737
    if (shapeClipper.shape.runtimeType != CircleBorder) {
1738
      return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
1739
    }
1740
    return true;
1741 1742
  }

1743 1744 1745
  @override
  Description describe(Description description) {
    description.add('renders on a physical model');
1746
    if (shape != null) {
1747
      description.add(' with shape $shape');
1748 1749
    }
    if (borderRadius != null) {
1750
      description.add(' with borderRadius $borderRadius');
1751 1752
    }
    if (elevation != null) {
1753
      description.add(' with elevation $elevation');
1754
    }
1755 1756 1757 1758
    return description;
  }
}

1759
class _RendersOnPhysicalShape extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
1760
  const _RendersOnPhysicalShape({
1761
    required this.shape,
1762 1763 1764 1765
    this.elevation,
  });

  final ShapeBorder shape;
1766
  final double? elevation;
1767 1768 1769

  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
1770
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1771
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1772
    }
1773
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1774

1775
    if (shapeClipper.shape != shape) {
1776
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1777
    }
1778

1779
    if (elevation != null && renderObject.elevation != elevation) {
1780
      return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
1781
    }
1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793

    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');
1794
    if (elevation != null) {
1795
      description.add(' with elevation $elevation');
1796
    }
1797 1798 1799 1800 1801
    return description;
  }
}

class _ClipsWithBoundingRect extends _MatchRenderObject<RenderClipPath, RenderClipRect> {
1802 1803 1804
  const _ClipsWithBoundingRect();

  @override
1805
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRect renderObject) {
1806
    if (renderObject.clipper != null) {
1807
      return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');
1808
    }
1809 1810 1811
    return true;
  }

1812 1813
  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
1814
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1815
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1816
    }
1817
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1818
    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) {
1819
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1820
    }
1821
    final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
1822
    if (border.borderRadius != BorderRadius.zero) {
1823
      return failWithDescription(matchState, 'borderRadius was: ${border.borderRadius}');
1824
    }
1825 1826 1827
    return true;
  }

1828 1829 1830 1831 1832
  @override
  Description describe(Description description) =>
    description.add('clips with bounding rectangle');
}

1833
class _ClipsWithBoundingRRect extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
1834
  const _ClipsWithBoundingRRect({required this.borderRadius});
1835 1836 1837 1838 1839

  final BorderRadius borderRadius;


  @override
1840
  bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) {
1841
    if (renderObject.clipper != null) {
1842
      return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');
1843
    }
1844

1845
    if (renderObject.borderRadius != borderRadius) {
1846
      return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');
1847
    }
1848 1849 1850 1851

    return true;
  }

1852 1853
  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
1854
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1855
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1856
    }
1857
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1858
    if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) {
1859
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1860
    }
1861
    final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
1862
    if (border.borderRadius != borderRadius) {
1863
      return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
1864
    }
1865 1866 1867
    return true;
  }

1868 1869 1870 1871
  @override
  Description describe(Description description) =>
    description.add('clips with bounding rounded rectangle with borderRadius: $borderRadius');
}
1872

1873
class _ClipsWithShapeBorder extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
1874
  const _ClipsWithShapeBorder({required this.shape});
1875 1876 1877 1878 1879

  final ShapeBorder shape;

  @override
  bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
1880
    if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
1881
      return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
1882
    }
1883
    final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
1884
    if (shapeClipper.shape != shape) {
1885
      return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
1886
    }
1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899
    return true;
  }

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


  @override
  Description describe(Description description) =>
    description.add('clips with shape: $shape');
}
1900 1901 1902 1903

class _CoversSameAreaAs extends Matcher {
  _CoversSameAreaAs(
    this.expectedPath, {
1904
    required this.areaToCompare,
1905 1906 1907 1908
    this.sampleSize = 20,
  }) : maxHorizontalNoise = areaToCompare.width / sampleSize,
       maxVerticalNoise = areaToCompare.height / sampleSize {
    // Use a fixed random seed to make sure tests are deterministic.
1909
    random = math.Random(1);
1910 1911 1912 1913 1914 1915 1916
  }

  final Path expectedPath;
  final Rect areaToCompare;
  final int sampleSize;
  final double maxHorizontalNoise;
  final double maxVerticalNoise;
1917
  late math.Random random;
1918 1919 1920 1921 1922

  @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) {
1923
        final Offset offset = Offset(
1924
          i * (areaToCompare.width / sampleSize),
1925
          j * (areaToCompare.height / sampleSize),
1926 1927
        );

1928
        if (!_samplePoint(matchState, actualPath, offset)) {
1929
          return false;
1930
        }
1931

1932
        final Offset noise = Offset(
1933 1934 1935 1936
          maxHorizontalNoise * random.nextDouble(),
          maxVerticalNoise * random.nextDouble(),
        );

1937
        if (!_samplePoint(matchState, actualPath, offset + noise)) {
1938
          return false;
1939
        }
1940 1941 1942 1943 1944 1945
      }
    }
    return true;
  }

  bool _samplePoint(Map<dynamic, dynamic> matchState, Path actualPath, Offset offset) {
1946
    if (expectedPath.contains(offset) == actualPath.contains(offset)) {
1947
      return true;
1948
    }
1949

1950
    if (actualPath.contains(offset)) {
1951
      return failWithDescription(matchState, '$offset is contained in the actual path but not in the expected path');
1952
    } else {
1953
      return failWithDescription(matchState, '$offset is contained in the expected path but not in the actual path');
1954
    }
1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966
  }

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

  @override
  Description describeMismatch(
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
1967
    bool verbose,
1968
  ) {
1969
    return mismatchDescription.add(matchState['failure'] as String);
1970 1971 1972 1973 1974 1975
  }

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

1977 1978
class _ColorMatcher extends Matcher {
  const _ColorMatcher({
1979
    required this.targetColor,
1980 1981 1982 1983 1984 1985
  }) : assert(targetColor != null);

  final Color targetColor;

  @override
  bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
1986
    if (item is Color) {
1987
      return item == targetColor || item.value == targetColor.value;
1988
    }
1989 1990 1991 1992 1993 1994 1995
    return false;
  }

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

1996 1997 1998 1999 2000
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] ||
2001 2002 2003
        imageA[i + 1] != imageB[i + 1] ||
        imageA[i + 2] != imageB[i + 2] ||
        imageA[i + 3] != imageB[i + 3]) {
2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015
      delta++;
    }
  }
  return delta;
}

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

  final ui.Image referenceImage;

  @override
2016
  Future<String?> matchAsync(dynamic item) async {
2017 2018 2019 2020 2021 2022
    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 {
2023
      final Finder finder = item as Finder;
2024 2025 2026 2027 2028 2029
      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';
      }
2030
      imageFuture = captureImage(elements.single);
2031 2032
    }

2033
    final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
2034
    return binding.runAsync<String?>(() async {
2035
      final ui.Image image = await imageFuture;
2036
      final ByteData? bytes = await image.toByteData();
2037
      if (bytes == null) {
2038
        return 'could not be encoded.';
2039
      }
2040

2041
      final ByteData? referenceBytes = await referenceImage.toByteData();
2042
      if (referenceBytes == null) {
2043
        return 'could not have its reference image encoded.';
2044
      }
2045

2046
      if (referenceImage.height != image.height || referenceImage.width != image.width) {
2047
        return 'does not match as width or height do not match. $image != $referenceImage';
2048
      }
2049 2050 2051 2052 2053 2054

      final int countDifferentPixels = _countDifferentPixels(
        Uint8List.view(bytes.buffer),
        Uint8List.view(referenceBytes.buffer),
      );
      return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
2055
    }, additionalTime: const Duration(minutes: 1));
2056 2057 2058 2059 2060 2061 2062 2063
  }

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

2064 2065
class _MatchesSemanticsData extends Matcher {
  _MatchesSemanticsData({
2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195
    required this.label,
    required this.attributedLabel,
    required this.hint,
    required this.attributedHint,
    required this.value,
    required this.attributedValue,
    required this.increasedValue,
    required this.attributedIncreasedValue,
    required this.decreasedValue,
    required this.attributedDecreasedValue,
    required this.tooltip,
    required this.textDirection,
    required this.rect,
    required this.size,
    required this.elevation,
    required this.thickness,
    required this.platformViewId,
    required this.maxValueLength,
    required this.currentValueLength,
    // Flags
    required bool? hasCheckedState,
    required bool? isChecked,
    required bool? isSelected,
    required bool? isButton,
    required bool? isSlider,
    required bool? isKeyboardKey,
    required bool? isLink,
    required bool? isFocused,
    required bool? isFocusable,
    required bool? isTextField,
    required bool? isReadOnly,
    required bool? hasEnabledState,
    required bool? isEnabled,
    required bool? isInMutuallyExclusiveGroup,
    required bool? isHeader,
    required bool? isObscured,
    required bool? isMultiline,
    required bool? namesRoute,
    required bool? scopesRoute,
    required bool? isHidden,
    required bool? isImage,
    required bool? isLiveRegion,
    required bool? hasToggledState,
    required bool? isToggled,
    required bool? hasImplicitScrolling,
    // Actions
    required bool? hasTapAction,
    required bool? hasLongPressAction,
    required bool? hasScrollLeftAction,
    required bool? hasScrollRightAction,
    required bool? hasScrollUpAction,
    required bool? hasScrollDownAction,
    required bool? hasIncreaseAction,
    required bool? hasDecreaseAction,
    required bool? hasShowOnScreenAction,
    required bool? hasMoveCursorForwardByCharacterAction,
    required bool? hasMoveCursorBackwardByCharacterAction,
    required bool? hasMoveCursorForwardByWordAction,
    required bool? hasMoveCursorBackwardByWordAction,
    required bool? hasSetTextAction,
    required bool? hasSetSelectionAction,
    required bool? hasCopyAction,
    required bool? hasCutAction,
    required bool? hasPasteAction,
    required bool? hasDidGainAccessibilityFocusAction,
    required bool? hasDidLoseAccessibilityFocusAction,
    required bool? hasDismissAction,
    // Custom actions and overrides
    required String? onTapHint,
    required String? onLongPressHint,
    required this.customActions,
    required this.children,
  })  : flags = <SemanticsFlag, bool>{
          if (hasCheckedState != null) SemanticsFlag.hasCheckedState: hasCheckedState,
          if (isChecked != null) SemanticsFlag.isChecked: isChecked,
          if (isSelected != null) SemanticsFlag.isSelected: isSelected,
          if (isButton != null) SemanticsFlag.isButton: isButton,
          if (isSlider != null) SemanticsFlag.isSlider: isSlider,
          if (isKeyboardKey != null) SemanticsFlag.isKeyboardKey: isKeyboardKey,
          if (isLink != null) SemanticsFlag.isLink: isLink,
          if (isTextField != null) SemanticsFlag.isTextField: isTextField,
          if (isReadOnly != null) SemanticsFlag.isReadOnly: isReadOnly,
          if (isFocused != null) SemanticsFlag.isFocused: isFocused,
          if (isFocusable != null) SemanticsFlag.isFocusable: isFocusable,
          if (hasEnabledState != null) SemanticsFlag.hasEnabledState: hasEnabledState,
          if (isEnabled != null) SemanticsFlag.isEnabled: isEnabled,
          if (isInMutuallyExclusiveGroup != null) SemanticsFlag.isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup,
          if (isHeader != null) SemanticsFlag.isHeader: isHeader,
          if (isObscured != null) SemanticsFlag.isObscured: isObscured,
          if (isMultiline != null) SemanticsFlag.isMultiline: isMultiline,
          if (namesRoute != null) SemanticsFlag.namesRoute: namesRoute,
          if (scopesRoute != null) SemanticsFlag.scopesRoute: scopesRoute,
          if (isHidden != null) SemanticsFlag.isHidden: isHidden,
          if (isImage != null) SemanticsFlag.isImage: isImage,
          if (isLiveRegion != null) SemanticsFlag.isLiveRegion: isLiveRegion,
          if (hasToggledState != null) SemanticsFlag.hasToggledState: hasToggledState,
          if (isToggled != null) SemanticsFlag.isToggled: isToggled,
          if (hasImplicitScrolling != null) SemanticsFlag.hasImplicitScrolling: hasImplicitScrolling,
          if (isSlider != null) SemanticsFlag.isSlider: isSlider,
        },
        actions = <SemanticsAction, bool>{
          if (hasTapAction != null) SemanticsAction.tap: hasTapAction,
          if (hasLongPressAction != null) SemanticsAction.longPress: hasLongPressAction,
          if (hasScrollLeftAction != null) SemanticsAction.scrollLeft: hasScrollLeftAction,
          if (hasScrollRightAction != null) SemanticsAction.scrollRight: hasScrollRightAction,
          if (hasScrollUpAction != null) SemanticsAction.scrollUp: hasScrollUpAction,
          if (hasScrollDownAction != null) SemanticsAction.scrollDown: hasScrollDownAction,
          if (hasIncreaseAction != null) SemanticsAction.increase: hasIncreaseAction,
          if (hasDecreaseAction != null) SemanticsAction.decrease: hasDecreaseAction,
          if (hasShowOnScreenAction != null) SemanticsAction.showOnScreen: hasShowOnScreenAction,
          if (hasMoveCursorForwardByCharacterAction != null) SemanticsAction.moveCursorForwardByCharacter: hasMoveCursorForwardByCharacterAction,
          if (hasMoveCursorBackwardByCharacterAction != null) SemanticsAction.moveCursorBackwardByCharacter: hasMoveCursorBackwardByCharacterAction,
          if (hasSetSelectionAction != null) SemanticsAction.setSelection: hasSetSelectionAction,
          if (hasCopyAction != null) SemanticsAction.copy: hasCopyAction,
          if (hasCutAction != null) SemanticsAction.cut: hasCutAction,
          if (hasPasteAction != null) SemanticsAction.paste: hasPasteAction,
          if (hasDidGainAccessibilityFocusAction != null) SemanticsAction.didGainAccessibilityFocus: hasDidGainAccessibilityFocusAction,
          if (hasDidLoseAccessibilityFocusAction != null) SemanticsAction.didLoseAccessibilityFocus: hasDidLoseAccessibilityFocusAction,
          if (customActions != null) SemanticsAction.customAction: customActions.isNotEmpty,
          if (hasDismissAction != null) SemanticsAction.dismiss: hasDismissAction,
          if (hasMoveCursorForwardByWordAction != null) SemanticsAction.moveCursorForwardByWord: hasMoveCursorForwardByWordAction,
          if (hasMoveCursorBackwardByWordAction != null) SemanticsAction.moveCursorBackwardByWord: hasMoveCursorBackwardByWordAction,
          if (hasSetTextAction != null) SemanticsAction.setText: hasSetTextAction,
        },
        hintOverrides = onTapHint == null && onLongPressHint == null
            ? null
            : SemanticsHintOverrides(
                onTapHint: onTapHint,
                onLongPressHint: onLongPressHint,
              );
2196

2197
  final String? label;
2198
  final AttributedString? attributedLabel;
2199
  final String? hint;
2200 2201 2202
  final AttributedString? attributedHint;
  final String? value;
  final AttributedString? attributedValue;
2203
  final String? increasedValue;
2204
  final AttributedString? attributedIncreasedValue;
2205
  final String? decreasedValue;
2206
  final AttributedString? attributedDecreasedValue;
2207
  final String? tooltip;
2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218
  final SemanticsHintOverrides? hintOverrides;
  final List<CustomSemanticsAction>? customActions;
  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;
2219

2220 2221 2222 2223 2224 2225 2226 2227
  /// There are three possible states for these two maps:
  ///
  ///  1. If the flag/action maps to `true`, then it must be present in the SemanticData
  ///  2. If the flag/action maps to `false`, then it must not be present in the SemanticData
  ///  3. If the flag/action is not in the map, then it will not be validated against
  final Map<SemanticsAction, bool> actions;
  final Map<SemanticsFlag, bool> flags;

2228 2229 2230
  @override
  Description describe(Description description) {
    description.add('has semantics');
2231
    if (label != null) {
2232
      description.add(' with label: $label');
2233 2234
    }
    if (attributedLabel != null) {
2235
      description.add(' with attributedLabel: $attributedLabel');
2236 2237
    }
    if (value != null) {
2238
      description.add(' with value: $value');
2239 2240
    }
    if (attributedValue != null) {
2241
      description.add(' with attributedValue: $attributedValue');
2242 2243
    }
    if (hint != null) {
2244
      description.add(' with hint: $hint');
2245 2246
    }
    if (attributedHint != null) {
2247
      description.add(' with attributedHint: $attributedHint');
2248 2249
    }
    if (increasedValue != null) {
2250
      description.add(' with increasedValue: $increasedValue ');
2251 2252
    }
    if (attributedIncreasedValue != null) {
2253
      description.add(' with attributedIncreasedValue: $attributedIncreasedValue');
2254 2255
    }
    if (decreasedValue != null) {
2256
      description.add(' with decreasedValue: $decreasedValue ');
2257 2258
    }
    if (attributedDecreasedValue != null) {
2259
      description.add(' with attributedDecreasedValue: $attributedDecreasedValue');
2260 2261
    }
    if (tooltip != null) {
2262
      description.add(' with tooltip: $tooltip');
2263
    }
2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279
    if (actions.isNotEmpty) {
      final List<SemanticsAction> expectedActions = actions.entries
        .where((MapEntry<ui.SemanticsAction, bool> e) => e.value)
        .map((MapEntry<ui.SemanticsAction, bool> e) => e.key)
        .toList();
      final List<SemanticsAction> notExpectedActions = actions.entries
        .where((MapEntry<ui.SemanticsAction, bool> e) => !e.value)
        .map((MapEntry<ui.SemanticsAction, bool> e) => e.key)
        .toList();

      if (expectedActions.isNotEmpty) {
        description.add(' with actions: ').addDescriptionOf(expectedActions);
      }
      if (notExpectedActions.isNotEmpty) {
        description.add(' without actions: ').addDescriptionOf(notExpectedActions);
      }
2280
    }
2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296
    if (flags.isNotEmpty) {
      final List<SemanticsFlag> expectedFlags = flags.entries
        .where((MapEntry<ui.SemanticsFlag, bool> e) => e.value)
        .map((MapEntry<ui.SemanticsFlag, bool> e) => e.key)
        .toList();
      final List<SemanticsFlag> notExpectedFlags = flags.entries
        .where((MapEntry<ui.SemanticsFlag, bool> e) => !e.value)
        .map((MapEntry<ui.SemanticsFlag, bool> e) => e.key)
        .toList();

      if (expectedFlags.isNotEmpty) {
        description.add(' with flags: ').addDescriptionOf(expectedFlags);
      }
      if (notExpectedFlags.isNotEmpty) {
        description.add(' without flags: ').addDescriptionOf(notExpectedFlags);
      }
2297 2298
    }
    if (textDirection != null) {
2299
      description.add(' with textDirection: $textDirection ');
2300 2301
    }
    if (rect != null) {
2302
      description.add(' with rect: $rect');
2303 2304
    }
    if (size != null) {
2305
      description.add(' with size: $size');
2306 2307
    }
    if (elevation != null) {
2308
      description.add(' with elevation: $elevation');
2309 2310
    }
    if (thickness != null) {
2311
      description.add(' with thickness: $thickness');
2312 2313
    }
    if (platformViewId != null) {
2314
      description.add(' with platformViewId: $platformViewId');
2315 2316
    }
    if (maxValueLength != null) {
2317
      description.add(' with maxValueLength: $maxValueLength');
2318 2319
    }
    if (currentValueLength != null) {
2320
      description.add(' with currentValueLength: $currentValueLength');
2321 2322
    }
    if (customActions != null) {
2323
      description.add(' with custom actions: $customActions');
2324 2325
    }
    if (hintOverrides != null) {
2326
      description.add(' with custom hints: $hintOverrides');
2327
    }
2328 2329
    if (children != null) {
      description.add(' with children:\n');
2330
      for (final _MatchesSemanticsData child in children!.cast<_MatchesSemanticsData>()) {
2331
        child.describe(description);
2332
      }
2333
    }
2334 2335 2336
    return description;
  }

2337
  bool _stringAttributesEqual(List<StringAttribute> first, List<StringAttribute> second) {
2338
    if (first.length != second.length) {
2339
      return false;
2340
    }
2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355
    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;
  }
2356 2357

  @override
2358
  bool matches(dynamic node, Map<dynamic, dynamic> matchState) {
2359
    if (node == null) {
2360
      return failWithDescription(matchState, 'No SemanticsData provided. '
2361
        'Maybe you forgot to enable semantics?');
2362
    }
2363
    final SemanticsData data = node is SemanticsNode ? node.getSemanticsData() : (node as SemanticsData);
2364
    if (label != null && label != data.label) {
2365
      return failWithDescription(matchState, 'label was: ${data.label}');
2366
    }
2367 2368 2369 2370 2371 2372
    if (attributedLabel != null &&
        (attributedLabel!.string != data.attributedLabel.string ||
         !_stringAttributesEqual(attributedLabel!.attributes, data.attributedLabel.attributes))) {
      return failWithDescription(
          matchState, 'attributedLabel was: ${data.attributedLabel}');
    }
2373
    if (hint != null && hint != data.hint) {
2374
      return failWithDescription(matchState, 'hint was: ${data.hint}');
2375
    }
2376 2377 2378 2379 2380 2381
    if (attributedHint != null &&
        (attributedHint!.string != data.attributedHint.string ||
         !_stringAttributesEqual(attributedHint!.attributes, data.attributedHint.attributes))) {
      return failWithDescription(
          matchState, 'attributedHint was: ${data.attributedHint}');
    }
2382
    if (value != null && value != data.value) {
2383
      return failWithDescription(matchState, 'value was: ${data.value}');
2384
    }
2385 2386 2387 2388 2389 2390
    if (attributedValue != null &&
        (attributedValue!.string != data.attributedValue.string ||
         !_stringAttributesEqual(attributedValue!.attributes, data.attributedValue.attributes))) {
      return failWithDescription(
          matchState, 'attributedValue was: ${data.attributedValue}');
    }
2391
    if (increasedValue != null && increasedValue != data.increasedValue) {
2392
      return failWithDescription(matchState, 'increasedValue was: ${data.increasedValue}');
2393
    }
2394 2395 2396 2397 2398 2399
    if (attributedIncreasedValue != null &&
        (attributedIncreasedValue!.string != data.attributedIncreasedValue.string ||
         !_stringAttributesEqual(attributedIncreasedValue!.attributes, data.attributedIncreasedValue.attributes))) {
      return failWithDescription(
          matchState, 'attributedIncreasedValue was: ${data.attributedIncreasedValue}');
    }
2400
    if (decreasedValue != null && decreasedValue != data.decreasedValue) {
2401
      return failWithDescription(matchState, 'decreasedValue was: ${data.decreasedValue}');
2402
    }
2403 2404 2405 2406 2407 2408
    if (attributedDecreasedValue != null &&
        (attributedDecreasedValue!.string != data.attributedDecreasedValue.string ||
         !_stringAttributesEqual(attributedDecreasedValue!.attributes, data.attributedDecreasedValue.attributes))) {
      return failWithDescription(
          matchState, 'attributedDecreasedValue was: ${data.attributedDecreasedValue}');
    }
2409
    if (tooltip != null && tooltip != data.tooltip) {
2410
      return failWithDescription(matchState, 'tooltip was: ${data.tooltip}');
2411 2412
    }
    if (textDirection != null && textDirection != data.textDirection) {
2413
      return failWithDescription(matchState, 'textDirection was: $textDirection');
2414 2415
    }
    if (rect != null && rect != data.rect) {
2416
      return failWithDescription(matchState, 'rect was: ${data.rect}');
2417 2418
    }
    if (size != null && size != data.rect.size) {
2419
      return failWithDescription(matchState, 'size was: ${data.rect.size}');
2420 2421
    }
    if (elevation != null && elevation != data.elevation) {
2422
      return failWithDescription(matchState, 'elevation was: ${data.elevation}');
2423 2424
    }
    if (thickness != null && thickness != data.thickness) {
2425
      return failWithDescription(matchState, 'thickness was: ${data.thickness}');
2426 2427
    }
    if (platformViewId != null && platformViewId != data.platformViewId) {
2428
      return failWithDescription(matchState, 'platformViewId was: ${data.platformViewId}');
2429 2430
    }
    if (currentValueLength != null && currentValueLength != data.currentValueLength) {
2431
      return failWithDescription(matchState, 'currentValueLength was: ${data.currentValueLength}');
2432 2433
    }
    if (maxValueLength != null && maxValueLength != data.maxValueLength) {
2434
      return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}');
2435
    }
2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447
    if (actions.isNotEmpty) {
      for (final MapEntry<ui.SemanticsAction, bool> actionEntry in actions.entries) {
        final ui.SemanticsAction action = actionEntry.key;
        final bool actionExpected = actionEntry.value;
        final bool actionPresent = (action.index & data.actions) == action.index;
        if (actionPresent != actionExpected) {
          final List<String> actionSummary = <String>[
            for (final int action in SemanticsAction.values.keys)
              if ((data.actions & action) != 0) describeEnum(action),
          ];
          return failWithDescription(matchState, 'actions were: $actionSummary');
        }
2448 2449
      }
    }
2450
    if (customActions != null || hintOverrides != null) {
2451 2452 2453
      final List<CustomSemanticsAction> providedCustomActions = data.customSemanticsActionIds?.map<CustomSemanticsAction>((int id) {
        return CustomSemanticsAction.getAction(id)!;
      }).toList() ?? <CustomSemanticsAction>[];
2454
      final List<CustomSemanticsAction> expectedCustomActions = customActions?.toList() ?? <CustomSemanticsAction>[];
2455
      if (hintOverrides?.onTapHint != null) {
2456
        expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides!.onTapHint!, action: SemanticsAction.tap));
2457 2458
      }
      if (hintOverrides?.onLongPressHint != null) {
2459
        expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides!.onLongPressHint!, action: SemanticsAction.longPress));
2460 2461
      }
      if (expectedCustomActions.length != providedCustomActions.length) {
2462
        return failWithDescription(matchState, 'custom actions were: $providedCustomActions');
2463
      }
2464 2465 2466 2467 2468 2469
      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++) {
2470
        if (expectedCustomActions[i] != providedCustomActions[i]) {
2471
          return failWithDescription(matchState, 'custom actions were: $providedCustomActions');
2472
        }
2473 2474
      }
    }
2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486
    if (flags.isNotEmpty) {
      for (final MapEntry<ui.SemanticsFlag, bool> flagEntry in flags.entries) {
        final ui.SemanticsFlag flag = flagEntry.key;
        final bool flagExpected = flagEntry.value;
        final bool flagPresent = flag.index & data.flags == flag.index;
        if (flagPresent != flagExpected) {
          final List<String> flagSummary = <String>[
            for (final int flag in SemanticsFlag.values.keys)
              if ((data.flags & flag) != 0) describeEnum(flag),
          ];
          return failWithDescription(matchState, 'flags were: $flagSummary');
        }
2487 2488
      }
    }
2489 2490 2491
    bool allMatched = true;
    if (children != null) {
      int i = 0;
2492
      (node as SemanticsNode).visitChildren((SemanticsNode child) {
2493
        allMatched = children![i].matches(child, matchState) && allMatched;
2494 2495 2496 2497 2498
        i += 1;
        return allMatched;
      });
    }
    return allMatched;
2499 2500 2501 2502 2503 2504 2505 2506 2507
  }

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

  @override
  Description describeMismatch(
2508 2509 2510
    dynamic item,
    Description mismatchDescription,
    Map<dynamic, dynamic> matchState,
2511
    bool verbose,
2512
  ) {
2513
    return mismatchDescription.add(matchState['failure'] as String);
2514
  }
2515
}
2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527

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

  final AccessibilityGuideline guideline;

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

  @override
2528
  Future<String?> matchAsync(covariant WidgetTester tester) async {
2529
    final Evaluation result = await guideline.evaluate(tester);
2530
    if (result.passed) {
2531
      return null;
2532
    }
2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543
    return result.reason;
  }
}

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

  final AccessibilityGuideline guideline;

  @override
  Description describe(Description description) {
2544
    return description.add('Does not ${guideline.description}');
2545 2546 2547
  }

  @override
2548
  Future<String?> matchAsync(covariant WidgetTester tester) async {
2549
    final Evaluation result = await guideline.evaluate(tester);
2550
    if (result.passed) {
2551
      return 'Failed';
2552
    }
2553 2554
    return null;
  }
2555
}