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