// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; import 'dart:math' as math; import 'dart:typed_data'; import 'dart:ui' as ui; import 'dart:ui'; import 'package:meta/meta.dart'; // ignore: deprecated_member_use import 'package:test_api/test_api.dart' hide TypeMatcher, isInstanceOf; // ignore: deprecated_member_use import 'package:test_api/test_api.dart' as test_package show TypeMatcher; import 'package:test_api/src/frontend/async_matcher.dart'; // ignore: implementation_imports import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage; import 'accessibility.dart'; import 'binding.dart'; import 'finders.dart'; import 'goldens.dart'; import 'widget_tester.dart' show WidgetTester; /// Asserts that the [Finder] matches no widgets in the widget tree. /// /// ## Sample code /// /// ```dart /// expect(find.text('Save'), findsNothing); /// ``` /// /// See also: /// /// * [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. const Matcher findsNothing = _FindsWidgetMatcher(null, 0); /// Asserts that the [Finder] locates at least one widget in the widget tree. /// /// ## Sample code /// /// ```dart /// expect(find.text('Save'), findsWidgets); /// ``` /// /// See also: /// /// * [findsNothing], when you want the finder to not find anything. /// * [findsOneWidget], when you want the finder to find exactly one widget. /// * [findsNWidgets], when you want the finder to find a specific number of widgets. const Matcher findsWidgets = _FindsWidgetMatcher(1, null); /// Asserts that the [Finder] locates at exactly one widget in the widget tree. /// /// ## Sample code /// /// ```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. const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1); /// Asserts that the [Finder] locates the specified number of widgets in the widget tree. /// /// ## Sample code /// /// ```dart /// expect(find.text('Save'), findsNWidgets(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. Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n); /// Asserts that the [Finder] locates a single widget that has at /// least one [Offstage] widget ancestor. /// /// It's important to use a full finder, since by default finders exclude /// offstage widgets. /// /// ## Sample code /// /// ```dart /// expect(find.text('Save', skipOffstage: false), isOffstage); /// ``` /// /// See also: /// /// * [isOnstage], the opposite. const Matcher isOffstage = _IsOffstage(); /// Asserts that the [Finder] locates a single widget that has no /// [Offstage] widget ancestors. /// /// See also: /// /// * [isOffstage], the opposite. const Matcher isOnstage = _IsOnstage(); /// Asserts that the [Finder] locates a single widget that has at /// least one [Card] widget ancestor. /// /// See also: /// /// * [isNotInCard], the opposite. const Matcher isInCard = _IsInCard(); /// Asserts that the [Finder] locates a single widget that has no /// [Card] widget ancestors. /// /// This is equivalent to `isNot(isInCard)`. /// /// See also: /// /// * [isInCard], the opposite. const Matcher isNotInCard = _IsNotInCard(); /// 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); /// Asserts that an object's toString() is a plausible one-line description. /// /// Specifically, this matcher checks that the string does not contains newline /// characters, and does not have leading or trailing whitespace, is not /// empty, and does not contain the default `Instance of ...` string. const Matcher hasOneLineDescription = _HasOneLineDescription(); /// Asserts that an object's toStringDeep() is a plausible multiline /// 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`. const Matcher hasAGoodToStringDeep = _HasGoodToStringDeep(); /// A matcher for functions that throw [FlutterError]. /// /// This is equivalent to `throwsA(isA<FlutterError>())`. /// /// If you are trying to test whether a call to [WidgetTester.pumpWidget] /// results in a [FlutterError], see [TestWidgetsFlutterBinding.takeException]. /// /// See also: /// /// * [throwsAssertionError], to test if a function throws any [AssertionError]. /// * [throwsArgumentError], to test if a functions throws an [ArgumentError]. /// * [isFlutterError], to test if any object is a [FlutterError]. final Matcher throwsFlutterError = throwsA(isFlutterError); /// A matcher for functions that throw [AssertionError]. /// /// This is equivalent to `throwsA(isA<AssertionError>())`. /// /// If you are trying to test whether a call to [WidgetTester.pumpWidget] /// results in an [AssertionError], see /// [TestWidgetsFlutterBinding.takeException]. /// /// See also: /// /// * [throwsFlutterError], to test if a function throws a [FlutterError]. /// * [throwsArgumentError], to test if a functions throws an [ArgumentError]. /// * [isAssertionError], to test if any object is any kind of [AssertionError]. final Matcher throwsAssertionError = throwsA(isAssertionError); /// A matcher for [FlutterError]. /// /// This is equivalent to `isInstanceOf<FlutterError>()`. /// /// See also: /// /// * [throwsFlutterError], to test if a function throws a [FlutterError]. /// * [isAssertionError], to test if any object is any kind of [AssertionError]. final test_package.TypeMatcher<FlutterError> isFlutterError = isA<FlutterError>(); /// A matcher for [AssertionError]. /// /// This is equivalent to `isInstanceOf<AssertionError>()`. /// /// See also: /// /// * [throwsAssertionError], to test if a function throws any [AssertionError]. /// * [isFlutterError], to test if any object is a [FlutterError]. final test_package.TypeMatcher<AssertionError> isAssertionError = isA<AssertionError>(); /// A matcher that compares the type of the actual value to the type argument T. // TODO(ianh): Remove this once https://github.com/dart-lang/matcher/issues/98 is fixed test_package.TypeMatcher<T> isInstanceOf<T>() => isA<T>(); /// Asserts that two [double]s are equal, within some tolerated error. /// /// {@template flutter.flutter_test.moreOrLessEquals.epsilon} /// Two values are considered equal if the difference between them is within /// [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. /// {@endtemplate} /// /// See also: /// /// * [closeTo], which is identical except that the epsilon argument is /// required and not named. /// * [inInclusiveRange], which matches if the argument is in a specified /// range. /// * [rectMoreOrLessEquals] and [offsetMoreOrLessEquals], which do something /// similar but for [Rect]s and [Offset]s respectively. Matcher moreOrLessEquals(double value, { double epsilon = precisionErrorTolerance }) { return _MoreOrLessEquals(value, epsilon); } /// Asserts that two [Rect]s are equal, within some tolerated error. /// /// {@macro flutter.flutter_test.moreOrLessEquals.epsilon} /// /// See also: /// /// * [moreOrLessEquals], which is for [double]s. /// * [offsetMoreOrLessEquals], which is for [Offset]s. /// * [within], which offers a generic version of this functionality that can /// be used to match [Rect]s as well as other types. Matcher rectMoreOrLessEquals(Rect value, { double epsilon = precisionErrorTolerance }) { return _IsWithinDistance<Rect>(_rectDistance, value, epsilon); } /// Asserts that two [Offset]s are equal, within some tolerated error. /// /// {@macro flutter.flutter_test.moreOrLessEquals.epsilon} /// /// 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); } /// Asserts that two [String]s are equal after normalizing likely hash codes. /// /// A `#` followed by 5 hexadecimal digits is assumed to be a short hash code /// and is normalized to #00000. /// /// See Also: /// /// * [describeIdentity], a method that generates short descriptions of objects /// with ids that match the pattern #[0-9a-f]{5}. /// * [shortHash], a method that generates a 5 character long hexadecimal /// [String] based on [Object.hashCode]. /// * [DiagnosticableTree.toStringDeep], a method that returns a [String] /// typically containing multiple hash codes. Matcher equalsIgnoringHashCodes(String value) { return _EqualsIgnoringHashCodes(value); } /// 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. Matcher isMethodCall(String name, { @required dynamic arguments }) { return _IsMethodCall(name, arguments); } /// 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. Matcher coversSameAreaAs(Path expectedPath, { @required Rect areaToCompare, int sampleSize = 20 }) => _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize); /// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches the /// golden image file identified by [key], with an optional [version] number. /// /// 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. As such, you may choose to wrap a test /// widget in a [RepaintBoundary] to specify a particular focus for the test. /// /// The [key] may be either a [Uri] or a [String] representation of a URL. /// /// The [version] is a number that can be used to differentiate historical /// golden files. This parameter is optional. /// /// This is an asynchronous matcher, meaning that callers should use /// [expectLater] when using this matcher and await the future returned by /// [expectLater]. /// /// ## 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. /// /// {@tool snippet} /// Sample invocations of [matchesGoldenFile]. /// /// ```dart /// await expectLater( /// find.text('Save'), /// matchesGoldenFile('save.png'), /// ); /// /// await expectLater( /// image, /// matchesGoldenFile('save.png'), /// ); /// /// await expectLater( /// imageFuture, /// matchesGoldenFile( /// 'save.png', /// version: 2, /// ), /// ); /// /// await expectLater( /// find.byType(MyWidget), /// matchesGoldenFile('goldens/myWidget.png'), /// ); /// ``` /// {@end-tool} /// /// See also: /// /// * [GoldenFileComparator], which acts as the backend for this matcher. /// * [LocalFileComparator], which is the default [GoldenFileComparator] /// implementation for `flutter test`. /// * [matchesReferenceImage], which should be used instead if you want to /// verify that two different code paths create identical images. /// * [flutter_test] for a discussion of test configurations, whereby callers /// may swap out the backend for this matcher. AsyncMatcher matchesGoldenFile(dynamic key, {int version}) { if (key is Uri) { return MatchesGoldenFile(key, version); } else if (key is String) { return MatchesGoldenFile.forStringPath(key, version); } throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}'); } /// 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); } /// Asserts that a [SemanticsNode] contains the specified information. /// /// 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. /// /// 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')), matchesSemanticsNode(label: 'hello')); /// handle.dispose(); /// ``` /// /// See also: /// /// * [WidgetTester.getSemantics], the tester method which retrieves semantics. Matcher matchesSemantics({ String label, String hint, String value, String increasedValue, String decreasedValue, TextDirection textDirection, Rect rect, Size size, double elevation, double thickness, int platformViewId, int maxValueLength, int currentValueLength, // Flags // bool hasCheckedState = false, bool isChecked = false, bool isSelected = false, bool isButton = false, bool isLink = false, bool isFocused = false, bool isFocusable = false, bool isTextField = false, bool isReadOnly = false, bool hasEnabledState = false, bool isEnabled = false, bool isInMutuallyExclusiveGroup = false, bool isHeader = false, bool isObscured = false, bool isMultiline = false, bool namesRoute = false, bool scopesRoute = false, bool isHidden = false, bool isImage = false, bool isLiveRegion = false, bool hasToggledState = false, bool isToggled = false, bool hasImplicitScrolling = false, // 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, bool hasMoveCursorForwardByWordAction = false, bool hasMoveCursorBackwardByWordAction = false, bool hasSetSelectionAction = false, bool hasCopyAction = false, bool hasCutAction = false, bool hasPasteAction = false, bool hasDidGainAccessibilityFocusAction = false, bool hasDidLoseAccessibilityFocusAction = false, bool hasDismissAction = false, // Custom actions and overrides String onTapHint, String onLongPressHint, List<CustomSemanticsAction> customActions, List<Matcher> children, }) { final List<SemanticsFlag> flags = <SemanticsFlag>[ if (hasCheckedState) SemanticsFlag.hasCheckedState, if (isChecked) SemanticsFlag.isChecked, if (isSelected) SemanticsFlag.isSelected, if (isButton) SemanticsFlag.isButton, if (isLink) SemanticsFlag.isLink, if (isTextField) SemanticsFlag.isTextField, if (isReadOnly) SemanticsFlag.isReadOnly, if (isFocused) SemanticsFlag.isFocused, if (isFocusable) SemanticsFlag.isFocusable, if (hasEnabledState) SemanticsFlag.hasEnabledState, if (isEnabled) SemanticsFlag.isEnabled, if (isInMutuallyExclusiveGroup) SemanticsFlag.isInMutuallyExclusiveGroup, if (isHeader) SemanticsFlag.isHeader, if (isObscured) SemanticsFlag.isObscured, if (isMultiline) SemanticsFlag.isMultiline, if (namesRoute) SemanticsFlag.namesRoute, if (scopesRoute) SemanticsFlag.scopesRoute, if (isHidden) SemanticsFlag.isHidden, if (isImage) SemanticsFlag.isImage, if (isLiveRegion) SemanticsFlag.isLiveRegion, if (hasToggledState) SemanticsFlag.hasToggledState, if (isToggled) SemanticsFlag.isToggled, if (hasImplicitScrolling) SemanticsFlag.hasImplicitScrolling, ]; final List<SemanticsAction> actions = <SemanticsAction>[ if (hasTapAction) SemanticsAction.tap, if (hasLongPressAction) SemanticsAction.longPress, if (hasScrollLeftAction) SemanticsAction.scrollLeft, if (hasScrollRightAction) SemanticsAction.scrollRight, if (hasScrollUpAction) SemanticsAction.scrollUp, if (hasScrollDownAction) SemanticsAction.scrollDown, if (hasIncreaseAction) SemanticsAction.increase, if (hasDecreaseAction) SemanticsAction.decrease, if (hasShowOnScreenAction) SemanticsAction.showOnScreen, if (hasMoveCursorForwardByCharacterAction) SemanticsAction.moveCursorForwardByCharacter, if (hasMoveCursorBackwardByCharacterAction) SemanticsAction.moveCursorBackwardByCharacter, if (hasSetSelectionAction) SemanticsAction.setSelection, if (hasCopyAction) SemanticsAction.copy, if (hasCutAction) SemanticsAction.cut, if (hasPasteAction) SemanticsAction.paste, if (hasDidGainAccessibilityFocusAction) SemanticsAction.didGainAccessibilityFocus, if (hasDidLoseAccessibilityFocusAction) SemanticsAction.didLoseAccessibilityFocus, if (customActions != null && customActions.isNotEmpty) SemanticsAction.customAction, if (hasDismissAction) SemanticsAction.dismiss, if (hasMoveCursorForwardByWordAction) SemanticsAction.moveCursorForwardByWord, if (hasMoveCursorBackwardByWordAction) SemanticsAction.moveCursorBackwardByWord, ]; SemanticsHintOverrides hintOverrides; if (onTapHint != null || onLongPressHint != null) hintOverrides = SemanticsHintOverrides( onTapHint: onTapHint, onLongPressHint: onLongPressHint, ); return _MatchesSemanticsData( label: label, hint: hint, value: value, increasedValue: increasedValue, decreasedValue: decreasedValue, actions: actions, flags: flags, textDirection: textDirection, rect: rect, size: size, elevation: elevation, thickness: thickness, platformViewId: platformViewId, customActions: customActions, hintOverrides: hintOverrides, currentValueLength: currentValueLength, maxValueLength: maxValueLength, children: children, ); } /// 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(); /// await expectLater(tester, meetsGuideline(textContrastGuideline)); /// handle.dispose(); /// ``` /// /// Supported accessibility guidelines: /// /// * [androidTapTargetGuideline], for Android minimum tappable area guidelines. /// * [iOSTapTargetGuideline], for iOS minimum tappable area guidelines. /// * [textContrastGuideline], for WCAG minimum text contrast guidelines. AsyncMatcher meetsGuideline(AccessibilityGuideline guideline) { return _MatchesAccessibilityGuideline(guideline); } /// The inverse matcher of [meetsGuideline]. /// /// This is needed because the [isNot] matcher does not compose with an /// [AsyncMatcher]. AsyncMatcher doesNotMeetGuideline(AccessibilityGuideline guideline) { return _DoesNotMatchAccessibilityGuideline(guideline); } class _FindsWidgetMatcher extends Matcher { const _FindsWidgetMatcher(this.min, this.max); final int min; final int max; @override bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) { assert(min != null || max != null); assert(min == null || max == null || min <= max); matchState[Finder] = finder; int count = 0; final Iterator<Element> iterator = finder.evaluate().iterator; if (min != null) { while (count < min && iterator.moveNext()) count += 1; if (count < min) return false; } if (max != null) { while (count <= max && iterator.moveNext()) count += 1; if (count > max) return false; } return true; } @override Description describe(Description description) { assert(min != null || max != null); if (min == max) { if (min == 1) return description.add('exactly one matching node in the widget tree'); return description.add('exactly $min matching nodes in the widget tree'); } if (min == null) { if (max == 0) return description.add('no matching nodes in the widget tree'); if (max == 1) return description.add('at most one matching node in the widget tree'); return description.add('at most $max matching nodes in the widget tree'); } if (max == null) { if (min == 1) return description.add('at least one matching node in the widget tree'); 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, bool verbose, ) { final Finder finder = matchState[Finder] as Finder; final int count = finder.evaluate().length; if (count == 0) { assert(min != null && min > 0); if (min == 1 && max == 1) return mismatchDescription.add('means none were found but one was expected'); return mismatchDescription.add('means none were found but some were expected'); } if (max == 0) { if (count == 1) return mismatchDescription.add('means one was found but none were expected'); return mismatchDescription.add('means some were found but none were expected'); } if (min != null && count < min) return mismatchDescription.add('is not enough'); assert(max != null && count > min); return mismatchDescription.add('is too many'); } } bool _hasAncestorMatching(Finder finder, bool predicate(Widget widget)) { final Iterable<Element> nodes = finder.evaluate(); if (nodes.length != 1) return false; bool result = false; nodes.single.visitAncestorElements((Element ancestor) { if (predicate(ancestor.widget)) { result = true; return false; } return true; }); return result; } bool _hasAncestorOfType(Finder finder, Type targetType) { return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType); } class _IsOffstage extends Matcher { const _IsOffstage(); @override bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) { return _hasAncestorMatching(finder, (Widget widget) { if (widget is Offstage) return widget.offstage; return false; }); } @override Description describe(Description description) => description.add('offstage'); } class _IsOnstage extends Matcher { const _IsOnstage(); @override bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) { final Iterable<Element> nodes = finder.evaluate(); if (nodes.length != 1) return false; bool result = true; nodes.single.visitAncestorElements((Element ancestor) { final Widget widget = ancestor.widget; if (widget is Offstage) { result = !widget.offstage; return false; } return true; }); return result; } @override Description describe(Description description) => description.add('onstage'); } class _IsInCard extends Matcher { const _IsInCard(); @override bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => _hasAncestorOfType(finder, Card); @override Description describe(Description description) => description.add('in card'); } class _IsNotInCard extends Matcher { const _IsNotInCard(); @override bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => !_hasAncestorOfType(finder, Card); @override Description describe(Description description) => description.add('not in card'); } class _HasOneLineDescription extends Matcher { const _HasOneLineDescription(); @override bool matches(Object object, Map<dynamic, dynamic> matchState) { final String description = object.toString(); return description.isNotEmpty && !description.contains('\n') && !description.contains('Instance of ') && description.trim() == description; } @override Description describe(Description description) => description.add('one line description'); } class _EqualsIgnoringHashCodes extends Matcher { _EqualsIgnoringHashCodes(String v) : _value = _normalize(v); final String _value; static final Object _mismatchedValueKey = Object(); static String _normalize(String s) { return s.replaceAll(RegExp(r'#[0-9a-fA-F]{5}'), '#00000'); } @override bool matches(dynamic object, Map<dynamic, dynamic> matchState) { final String description = _normalize(object as String); if (_value != description) { matchState[_mismatchedValueKey] = description; return false; } return true; } @override Description describe(Description description) { return description.add('multi line description equals $_value'); } @override Description describeMismatch( dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose, ) { if (matchState.containsKey(_mismatchedValueKey)) { final String actualValue = matchState[_mismatchedValueKey] as String; // Leading whitespace is added so that lines in the multiline // description returned by addDescriptionOf are all indented equally // which makes the output easier to read for this case. return mismatchDescription .add('expected normalized value\n ') .addDescriptionOf(_value) .add('\nbut got\n ') .addDescriptionOf(actualValue); } return mismatchDescription; } } /// Returns true if [c] represents a whitespace code unit. bool _isWhitespace(int c) => (c <= 0x000D && c >= 0x0009) || c == 0x0020; /// Returns true if [c] represents a vertical line Unicode line art code unit. /// /// 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); if (!_isWhitespace(c) && !_isVerticalLine(c)) return false; } return true; } class _HasGoodToStringDeep extends Matcher { const _HasGoodToStringDeep(); static final Object _toStringDeepErrorDescriptionKey = Object(); @override bool matches(dynamic object, Map<dynamic, dynamic> matchState) { final List<String> issues = <String>[]; String description = object.toStringDeep() as String; 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.'); } if (description.trim() != description) issues.add('Has trailing whitespace.'); final List<String> lines = description.split('\n'); if (lines.length < 2) issues.add('Does not have multiple lines.'); if (description.contains('Instance of ')) issues.add('Contains text "Instance of ".'); for (int i = 0; i < lines.length; ++i) { final String line = lines[i]; if (line.isEmpty) issues.add('Line ${i+1} is empty.'); if (line.trimRight() != line) issues.add('Line ${i+1} has trailing whitespace.'); } if (_isAllTreeConnectorCharacters(lines.last)) issues.add('Last line is all tree connector characters.'); // If a toStringDeep method doesn't properly handle nested values that // contain line breaks it can fail to add the required prefixes to all // lined when toStringDeep is called specifying prefixes. const String prefixLineOne = 'PREFIX_LINE_ONE____'; const String prefixOtherLines = 'PREFIX_OTHER_LINES_'; final List<String> prefixIssues = <String>[]; String descriptionWithPrefixes = object.toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines) as String; 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'); if (!linesWithPrefixes.first.startsWith(prefixLineOne)) prefixIssues.add('First line does not contain expected prefix.'); for (int i = 1; i < linesWithPrefixes.length; ++i) { if (!linesWithPrefixes[i].startsWith(prefixOtherLines)) prefixIssues.add('Line ${i+1} does not contain the expected prefix.'); } final StringBuffer errorDescription = StringBuffer(); if (issues.isNotEmpty) { errorDescription.writeln('Bad toStringDeep():'); errorDescription.writeln(description); errorDescription.writeAll(issues, '\n'); } if (prefixIssues.isNotEmpty) { errorDescription.writeln( 'Bad toStringDeep(prefixLineOne: "$prefixLineOne", prefixOtherLines: "$prefixOtherLines"):'); 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, bool verbose, ) { if (matchState.containsKey(_toStringDeepErrorDescriptionKey)) { return mismatchDescription.add(matchState[_toStringDeepErrorDescriptionKey] as String); } return mismatchDescription; } @override Description describe(Description description) { return description.add('multi line description'); } } /// 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. typedef DistanceFunction<T> = num Function(T a, T b); /// 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>] /// functions which have (potentially) unrelated argument types. Since the /// argument types of the functions may be unrelated, the only thing that the /// type system can statically assume about them is that they accept null (since /// all types in Dart are nullable). /// /// Calling an instance of this type must either be done dynamically, or by /// first casting it to a [DistanceFunction<T>] for some concrete T. typedef AnyDistanceFunction = num Function(Null a, Null b); const Map<Type, AnyDistanceFunction> _kStandardDistanceFunctions = <Type, AnyDistanceFunction>{ Color: _maxComponentColorDistance, HSVColor: _maxComponentHSVColorDistance, HSLColor: _maxComponentHSLColorDistance, Offset: _offsetDistance, int: _intDistance, double: _doubleDistance, Rect: _rectDistance, Size: _sizeDistance, }; 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(); } // 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()); } 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; } double _sizeDistance(Size a, Size b) { // 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; return delta.distance; } /// 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 /// `T` generic argument. Standard functions are defined for the following /// types: /// /// * [Color], whose distance is the maximum component-wise delta. /// * [Offset], whose distance is the Euclidean distance computed using the /// method [Offset.distance]. /// * [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. /// * [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. /// * [rectMoreOrLessEquals], which is similar to this function, but /// specializes in [Rect]s and has an optional `epsilon` parameter. /// * [closeTo], which specializes in numbers only. Matcher within<T>({ @required num distance, @required T from, DistanceFunction<T> distanceFunction, }) { distanceFunction ??= _kStandardDistanceFunctions[T] as DistanceFunction<T>; if (distanceFunction == null) { throw ArgumentError( 'The specified distanceFunction was null, and a standard distance ' 'function was not found for type ${from.runtimeType} of the provided ' '`from` argument.' ); } return _IsWithinDistance<T>(distanceFunction, from, distance); } class _IsWithinDistance<T> extends Matcher { const _IsWithinDistance(this.distanceFunction, this.value, this.epsilon); final DistanceFunction<T> distanceFunction; final T value; final num epsilon; @override bool matches(Object object, Map<dynamic, dynamic> matchState) { if (object is! T) return false; if (object == value) return true; final T test = object as T; final num distance = distanceFunction(test, value); if (distance < 0) { throw ArgumentError( '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.' ); } matchState['distance'] = distance; return distance <= epsilon; } @override Description describe(Description description) => description.add('$value (±$epsilon)'); @override Description describeMismatch( Object object, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose, ) { mismatchDescription.add('was ${matchState['distance']} away from the desired value.'); return mismatchDescription; } } class _MoreOrLessEquals extends Matcher { const _MoreOrLessEquals(this.value, this.epsilon) : assert(epsilon >= 0); final double value; final double epsilon; @override bool matches(Object object, Map<dynamic, dynamic> matchState) { if (object is! double) return false; if (object == value) return true; final double test = object as double; return (test - value).abs() <= epsilon; } @override Description describe(Description description) => description.add('$value (±$epsilon)'); @override Description describeMismatch(Object item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) { return super.describeMismatch(item, mismatchDescription, matchState, verbose) ..add('$item is not in the range of $value (±$epsilon).'); } } 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) { if (item is! MethodCall) return false; if (item.method != name) return false; return _deepEquals(item.arguments, arguments); } bool _deepEquals(dynamic a, dynamic b) { if (a == b) return true; if (a is List) return b is List && _deepEqualsList(a, b); if (a is Map) return b is Map && _deepEqualsMap(a, b); return false; } bool _deepEqualsList(List<dynamic> a, List<dynamic> b) { if (a.length != b.length) return false; for (int i = 0; i < a.length; i++) { if (!_deepEquals(a[i], b[i])) return false; } return true; } bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) { if (a.length != b.length) return false; for (final dynamic key in a.keys) { if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) return false; } return true; } @override Description describe(Description description) { return description .add('has method name: ').addDescriptionOf(name) .add(' with arguments: ').addDescriptionOf(arguments); } } /// Asserts that a [Finder] locates a single object whose root RenderObject /// is a [RenderClipRect] with no clipper set, or an equivalent /// [RenderClipPath]. const Matcher clipsWithBoundingRect = _ClipsWithBoundingRect(); /// Asserts that a [Finder] locates a single object whose root RenderObject is /// not a [RenderClipRect], [RenderClipRRect], [RenderClipOval], or /// [RenderClipPath]. const Matcher hasNoImmediateClip = _MatchAnythingExceptClip(); /// Asserts that a [Finder] locates a single object whose root RenderObject /// is a [RenderClipRRect] with no clipper set, and border radius equals to /// [borderRadius], or an equivalent [RenderClipPath]. Matcher clipsWithBoundingRRect({ @required BorderRadius borderRadius }) { return _ClipsWithBoundingRRect(borderRadius: borderRadius); } /// Asserts that a [Finder] locates a single object whose root RenderObject /// is a [RenderClipPath] with a [ShapeBorderClipper] that clips to /// [shape]. Matcher clipsWithShapeBorder({ @required ShapeBorder shape }) { return _ClipsWithShapeBorder(shape: shape); } /// 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]. Matcher rendersOnPhysicalModel({ BoxShape shape, BorderRadius borderRadius, double elevation, }) { return _RendersOnPhysicalModel( shape: shape, borderRadius: borderRadius, elevation: elevation, ); } /// 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({ ShapeBorder shape, double elevation, }) { return _RendersOnPhysicalShape( shape: shape, elevation: elevation, ); } abstract class _FailWithDescriptionMatcher extends Matcher { const _FailWithDescriptionMatcher(); bool failWithDescription(Map<dynamic, dynamic> matchState, String description) { matchState['failure'] = description; return false; } @override Description describeMismatch( dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose, ) { return mismatchDescription.add(matchState['failure'] as String); } } class _MatchAnythingExceptClip extends _FailWithDescriptionMatcher { const _MatchAnythingExceptClip(); @override bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) { final Iterable<Element> nodes = finder.evaluate(); if (nodes.length != 1) return failWithDescription(matchState, 'did not have a exactly one child element'); final RenderObject renderObject = nodes.single.renderObject; 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) { return description.add('does not have a clip as an immediate child'); } } abstract class _MatchRenderObject<M extends RenderObject, T extends RenderObject> extends _FailWithDescriptionMatcher { const _MatchRenderObject(); bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, T renderObject); bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, M renderObject); @override bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) { final Iterable<Element> nodes = finder.evaluate(); if (nodes.length != 1) return failWithDescription(matchState, 'did not have a exactly one child element'); final RenderObject renderObject = nodes.single.renderObject; if (renderObject.runtimeType == T) return renderObjectMatchesT(matchState, renderObject as T); if (renderObject.runtimeType == M) return renderObjectMatchesM(matchState, renderObject as M); return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}'); } } class _RendersOnPhysicalModel extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> { const _RendersOnPhysicalModel({ this.shape, this.borderRadius, this.elevation, }); final BoxShape shape; final BorderRadius borderRadius; final double elevation; @override bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) { if (shape != null && renderObject.shape != shape) return failWithDescription(matchState, 'had shape: ${renderObject.shape}'); if (borderRadius != null && renderObject.borderRadius != borderRadius) return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}'); if (elevation != null && renderObject.elevation != elevation) return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}'); return true; } @override bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) { if (renderObject.clipper.runtimeType != ShapeBorderClipper) return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}'); final ShapeBorderClipper shapeClipper = renderObject.clipper as ShapeBorderClipper; if (borderRadius != null && !assertRoundedRectangle(shapeClipper, borderRadius, matchState)) return false; if ( borderRadius == null && shape == BoxShape.rectangle && !assertRoundedRectangle(shapeClipper, BorderRadius.zero, matchState) ) { return false; } if ( borderRadius == null && shape == BoxShape.circle && !assertCircle(shapeClipper, matchState) ) { return false; } if (elevation != null && renderObject.elevation != elevation) return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}'); return true; } bool assertRoundedRectangle(ShapeBorderClipper shapeClipper, BorderRadius borderRadius, Map<dynamic, dynamic> matchState) { if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}'); final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder; if (border.borderRadius != borderRadius) return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}'); return true; } bool assertCircle(ShapeBorderClipper shapeClipper, Map<dynamic, dynamic> matchState) { if (shapeClipper.shape.runtimeType != CircleBorder) return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}'); return true; } @override Description describe(Description description) { description.add('renders on a physical model'); if (shape != null) description.add(' with shape $shape'); if (borderRadius != null) description.add(' with borderRadius $borderRadius'); if (elevation != null) description.add(' with elevation $elevation'); return description; } } class _RendersOnPhysicalShape extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> { const _RendersOnPhysicalShape({ this.shape, this.elevation, }); final ShapeBorder shape; final double elevation; @override bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) { if (renderObject.clipper.runtimeType != ShapeBorderClipper) return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}'); final ShapeBorderClipper shapeClipper = renderObject.clipper as ShapeBorderClipper; if (shapeClipper.shape != shape) return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}'); if (elevation != null && renderObject.elevation != elevation) return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}'); 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'); if (elevation != null) description.add(' with elevation $elevation'); return description; } } class _ClipsWithBoundingRect extends _MatchRenderObject<RenderClipPath, RenderClipRect> { const _ClipsWithBoundingRect(); @override bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRect renderObject) { if (renderObject.clipper != null) return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}'); return true; } @override bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) { if (renderObject.clipper.runtimeType != ShapeBorderClipper) return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}'); final ShapeBorderClipper shapeClipper = renderObject.clipper as ShapeBorderClipper; if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}'); final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder; if (border.borderRadius != BorderRadius.zero) return failWithDescription(matchState, 'borderRadius was: ${border.borderRadius}'); return true; } @override Description describe(Description description) => description.add('clips with bounding rectangle'); } class _ClipsWithBoundingRRect extends _MatchRenderObject<RenderClipPath, RenderClipRRect> { const _ClipsWithBoundingRRect({@required this.borderRadius}); final BorderRadius borderRadius; @override bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) { if (renderObject.clipper != null) return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}'); if (renderObject.borderRadius != borderRadius) return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}'); return true; } @override bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) { if (renderObject.clipper.runtimeType != ShapeBorderClipper) return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}'); final ShapeBorderClipper shapeClipper = renderObject.clipper as ShapeBorderClipper; if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}'); final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder; if (border.borderRadius != borderRadius) return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}'); return true; } @override Description describe(Description description) => description.add('clips with bounding rounded rectangle with borderRadius: $borderRadius'); } class _ClipsWithShapeBorder extends _MatchRenderObject<RenderClipPath, RenderClipRRect> { const _ClipsWithShapeBorder({@required this.shape}); final ShapeBorder shape; @override bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) { if (renderObject.clipper.runtimeType != ShapeBorderClipper) return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}'); final ShapeBorderClipper shapeClipper = renderObject.clipper as ShapeBorderClipper; if (shapeClipper.shape != shape) return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}'); return true; } @override bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) { return false; } @override Description describe(Description description) => description.add('clips with shape: $shape'); } class _CoversSameAreaAs extends Matcher { _CoversSameAreaAs( this.expectedPath, { @required this.areaToCompare, this.sampleSize = 20, }) : maxHorizontalNoise = areaToCompare.width / sampleSize, maxVerticalNoise = areaToCompare.height / sampleSize { // Use a fixed random seed to make sure tests are deterministic. random = math.Random(1); } final Path expectedPath; final Rect areaToCompare; final int sampleSize; final double maxHorizontalNoise; final double maxVerticalNoise; math.Random random; @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) { final Offset offset = Offset( i * (areaToCompare.width / sampleSize), j * (areaToCompare.height / sampleSize), ); if (!_samplePoint(matchState, actualPath, offset)) return false; final Offset noise = Offset( maxHorizontalNoise * random.nextDouble(), maxVerticalNoise * random.nextDouble(), ); if (!_samplePoint(matchState, actualPath, offset + noise)) return false; } } return true; } bool _samplePoint(Map<dynamic, dynamic> matchState, Path actualPath, Offset offset) { if (expectedPath.contains(offset) == actualPath.contains(offset)) return true; if (actualPath.contains(offset)) return failWithDescription(matchState, '$offset is contained in the actual path but not in the expected path'); else return failWithDescription(matchState, '$offset is contained in the expected path but not in the actual path'); } bool failWithDescription(Map<dynamic, dynamic> matchState, String description) { matchState['failure'] = description; return false; } @override Description describeMismatch( dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose, ) { return mismatchDescription.add(matchState['failure'] as String); } @override Description describe(Description description) => description.add('covers expected area and only expected area'); } class _ColorMatcher extends Matcher { const _ColorMatcher({ @required this.targetColor, }) : assert(targetColor != null); final Color targetColor; @override bool matches(dynamic item, Map<dynamic, dynamic> matchState) { if (item is Color) return item == targetColor || item.value == targetColor.value; return false; } @override Description describe(Description description) => description.add('matches color $targetColor'); } int _countDifferentPixels(Uint8List imageA, Uint8List imageB) { assert(imageA.length == imageB.length); int delta = 0; for (int i = 0; i < imageA.length; i+=4) { if (imageA[i] != imageB[i] || imageA[i+1] != imageB[i+1] || imageA[i+2] != imageB[i+2] || imageA[i+3] != imageB[i+3]) { delta++; } } return delta; } class _MatchesReferenceImage extends AsyncMatcher { const _MatchesReferenceImage(this.referenceImage); final ui.Image referenceImage; @override Future<String> matchAsync(dynamic item) async { 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 { final Finder finder = item as Finder; 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'; } imageFuture = captureImage(elements.single); } final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized() as TestWidgetsFlutterBinding; return binding.runAsync<String>(() async { final ui.Image image = await imageFuture; final ByteData bytes = await image.toByteData(); if (bytes == null) return 'could not be encoded.'; final ByteData referenceBytes = await referenceImage.toByteData(); if (referenceBytes == null) return 'could not have its reference image encoded.'; if (referenceImage.height != image.height || referenceImage.width != image.width) return 'does not match as width or height do not match. $image != $referenceImage'; final int countDifferentPixels = _countDifferentPixels( Uint8List.view(bytes.buffer), Uint8List.view(referenceBytes.buffer), ); return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels'; }, additionalTime: const Duration(minutes: 1)); } @override Description describe(Description description) { return description.add('rasterized image matches that of a $referenceImage reference image'); } } class _MatchesSemanticsData extends Matcher { _MatchesSemanticsData({ this.label, this.value, this.increasedValue, this.decreasedValue, this.hint, this.flags, this.actions, this.textDirection, this.rect, this.size, this.elevation, this.thickness, this.platformViewId, this.maxValueLength, this.currentValueLength, this.customActions, this.hintOverrides, this.children, }); final String label; final String value; final String hint; final String increasedValue; final String decreasedValue; final SemanticsHintOverrides hintOverrides; final List<SemanticsAction> actions; final List<CustomSemanticsAction> customActions; final List<SemanticsFlag> flags; final TextDirection textDirection; final Rect rect; final Size size; final double elevation; final double thickness; final int platformViewId; final int maxValueLength; final int currentValueLength; final List<Matcher> children; @override Description describe(Description description) { description.add('has semantics'); if (label != null) description.add(' with label: $label'); if (value != null) description.add(' with value: $value'); if (hint != null) description.add(' with hint: $hint'); if (increasedValue != null) description.add(' with increasedValue: $increasedValue '); if (decreasedValue != null) description.add(' with decreasedValue: $decreasedValue '); if (actions != null) description.add(' with actions: ').addDescriptionOf(actions); if (flags != null) description.add(' with flags: ').addDescriptionOf(flags); if (textDirection != null) description.add(' with textDirection: $textDirection '); if (rect != null) description.add(' with rect: $rect'); if (size != null) description.add(' with size: $size'); if (elevation != null) description.add(' with elevation: $elevation'); if (thickness != null) description.add(' with thickness: $thickness'); if (platformViewId != null) description.add(' with platformViewId: $platformViewId'); if (maxValueLength != null) description.add(' with maxValueLength: $maxValueLength'); if (currentValueLength != null) description.add(' with currentValueLength: $currentValueLength'); if (customActions != null) description.add(' with custom actions: $customActions'); if (hintOverrides != null) description.add(' with custom hints: $hintOverrides'); if (children != null) { description.add(' with children:\n'); for (final _MatchesSemanticsData child in children.cast<_MatchesSemanticsData>()) child.describe(description); } return description; } @override bool matches(dynamic node, Map<dynamic, dynamic> matchState) { // TODO(jonahwilliams): remove dynamic once we have removed getSemanticsData. if (node == null) return failWithDescription(matchState, 'No SemanticsData provided. ' 'Maybe you forgot to enable semantics?'); final SemanticsData data = node is SemanticsNode ? node.getSemanticsData() : (node as SemanticsData); if (label != null && label != data.label) return failWithDescription(matchState, 'label was: ${data.label}'); if (hint != null && hint != data.hint) return failWithDescription(matchState, 'hint was: ${data.hint}'); if (value != null && value != data.value) return failWithDescription(matchState, 'value was: ${data.value}'); if (increasedValue != null && increasedValue != data.increasedValue) return failWithDescription(matchState, 'increasedValue was: ${data.increasedValue}'); if (decreasedValue != null && decreasedValue != data.decreasedValue) return failWithDescription(matchState, 'decreasedValue was: ${data.decreasedValue}'); if (textDirection != null && textDirection != data.textDirection) return failWithDescription(matchState, 'textDirection was: $textDirection'); if (rect != null && rect != data.rect) return failWithDescription(matchState, 'rect was: ${data.rect}'); if (size != null && size != data.rect.size) return failWithDescription(matchState, 'size was: ${data.rect.size}'); if (elevation != null && elevation != data.elevation) return failWithDescription(matchState, 'elevation was: ${data.elevation}'); if (thickness != null && thickness != data.thickness) return failWithDescription(matchState, 'thickness was: ${data.thickness}'); if (platformViewId != null && platformViewId != data.platformViewId) return failWithDescription(matchState, 'platformViewId was: ${data.platformViewId}'); if (currentValueLength != null && currentValueLength != data.currentValueLength) return failWithDescription(matchState, 'currentValueLength was: ${data.currentValueLength}'); if (maxValueLength != null && maxValueLength != data.maxValueLength) return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}'); if (actions != null) { int actionBits = 0; for (final SemanticsAction action in actions) actionBits |= action.index; if (actionBits != data.actions) { final List<String> actionSummary = <String>[ for (final SemanticsAction action in SemanticsAction.values.values) if ((data.actions & action.index) != 0) describeEnum(action), ]; return failWithDescription(matchState, 'actions were: $actionSummary'); } } if (customActions != null || hintOverrides != null) { final List<CustomSemanticsAction> providedCustomActions = data.customSemanticsActionIds.map((int id) { return CustomSemanticsAction.getAction(id); }).toList(); final List<CustomSemanticsAction> expectedCustomActions = customActions?.toList() ?? <CustomSemanticsAction>[]; if (hintOverrides?.onTapHint != null) expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides.onTapHint, action: SemanticsAction.tap)); if (hintOverrides?.onLongPressHint != null) expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides.onLongPressHint, action: SemanticsAction.longPress)); if (expectedCustomActions.length != providedCustomActions.length) return failWithDescription(matchState, 'custom actions where: $providedCustomActions'); 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++) { if (expectedCustomActions[i] != providedCustomActions[i]) return failWithDescription(matchState, 'custom actions where: $providedCustomActions'); } } if (flags != null) { int flagBits = 0; for (final SemanticsFlag flag in flags) flagBits |= flag.index; if (flagBits != data.flags) { final List<String> flagSummary = <String>[ for (final SemanticsFlag flag in SemanticsFlag.values.values) if ((data.flags & flag.index) != 0) describeEnum(flag), ]; return failWithDescription(matchState, 'flags were: $flagSummary'); } } bool allMatched = true; if (children != null) { int i = 0; node.visitChildren((SemanticsNode child) { allMatched = children[i].matches(child, matchState) && allMatched; i += 1; return allMatched; }); } return allMatched; } bool failWithDescription(Map<dynamic, dynamic> matchState, String description) { matchState['failure'] = description; return false; } @override Description describeMismatch( dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose, ) { return mismatchDescription.add(matchState['failure'] as String); } } class _MatchesAccessibilityGuideline extends AsyncMatcher { _MatchesAccessibilityGuideline(this.guideline); final AccessibilityGuideline guideline; @override Description describe(Description description) { return description.add(guideline.description); } @override Future<String> matchAsync(covariant WidgetTester tester) async { final Evaluation result = await guideline.evaluate(tester); if (result.passed) return null; return result.reason; } } class _DoesNotMatchAccessibilityGuideline extends AsyncMatcher { _DoesNotMatchAccessibilityGuideline(this.guideline); final AccessibilityGuideline guideline; @override Description describe(Description description) { return description.add('Does not ' + guideline.description); } @override Future<String> matchAsync(covariant WidgetTester tester) async { final Evaluation result = await guideline.evaluate(tester); if (result.passed) return 'Failed'; return null; } }