| // 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. | |
| /// @docImport '_goldens_io.dart'; | |
| /// @docImport 'window.dart'; | |
| library; | |
| import 'dart:convert' show LineSplitter; | |
| import 'dart:math' as math; | |
| import 'dart:ui' as ui; | |
| import 'package:flutter/foundation.dart'; | |
| import 'package:flutter/material.dart' show Card; | |
| import 'package:flutter/rendering.dart'; | |
| import 'package:flutter/services.dart'; | |
| import 'package:flutter/widgets.dart'; | |
| import 'package:matcher/expect.dart'; | |
| import 'package:matcher/src/expect/async_matcher.dart'; // ignore: implementation_imports | |
| import 'package:vector_math/vector_math_64.dart' show Matrix3; | |
| import '_matchers_io.dart' | |
| if (dart.library.js_interop) '_matchers_web.dart' | |
| show MatchesGoldenFile, captureImage; | |
| import 'accessibility.dart'; | |
| import 'binding.dart'; | |
| import 'controller.dart'; | |
| import 'finders.dart'; | |
| import 'goldens.dart'; | |
| import 'widget_tester.dart' show WidgetTester; | |
| /// Asserts that the [FinderBase] matches nothing in the available candidates. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// expect(find.text('Save'), findsNothing); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [findsAny], when you want the finder to find one or more candidates. | |
| /// * [findsOne], when you want the finder to find exactly one candidate. | |
| /// * [findsExactly], when you want the finder to find a specific number of candidates. | |
| /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. | |
| const Matcher findsNothing = _FindsCountMatcher(null, 0); | |
| /// Asserts that the [FinderBase] locates at least one widget in the widget tree. | |
| /// | |
| /// This is equivalent to the preferred [findsAny] method. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// expect(find.text('Save'), findsWidgets); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [findsNothing], when you want the finder to not find anything. | |
| /// * [findsOne], when you want the finder to find exactly one candidate. | |
| /// * [findsExactly], when you want the finder to find a specific number of candidates. | |
| /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. | |
| const Matcher findsWidgets = _FindsCountMatcher(1, null); | |
| /// Asserts that the [FinderBase] locates at least one matching candidate. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// expect(find.text('Save'), findsAny); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [findsNothing], when you want the finder to not find anything. | |
| /// * [findsOne], when you want the finder to find exactly one candidate. | |
| /// * [findsExactly], when you want the finder to find a specific number of candidates. | |
| /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. | |
| const Matcher findsAny = _FindsCountMatcher(1, null); | |
| /// Asserts that the [FinderBase] locates at exactly one widget in the widget tree. | |
| /// | |
| /// This is equivalent to the preferred [findsOne] method. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// expect(find.text('Save'), findsOneWidget); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [findsNothing], when you want the finder to not find anything. | |
| /// * [findsAny], when you want the finder to find one or more candidates. | |
| /// * [findsExactly], when you want the finder to find a specific number of candidates. | |
| /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. | |
| const Matcher findsOneWidget = _FindsCountMatcher(1, 1); | |
| /// Asserts that the [FinderBase] finds exactly one matching candidate. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// expect(find.text('Save'), findsOne); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [findsNothing], when you want the finder to not find anything. | |
| /// * [findsAny], when you want the finder to find one or more candidates. | |
| /// * [findsExactly], when you want the finder to find a specific number candidates. | |
| /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. | |
| const Matcher findsOne = _FindsCountMatcher(1, 1); | |
| /// Asserts that the [FinderBase] locates the specified number of widgets in the widget tree. | |
| /// | |
| /// This is equivalent to the preferred [findsExactly] method. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// expect(find.text('Save'), findsNWidgets(2)); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [findsNothing], when you want the finder to not find anything. | |
| /// * [findsAny], when you want the finder to find one or more candidates. | |
| /// * [findsOne], when you want the finder to find exactly one candidate. | |
| /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. | |
| Matcher findsNWidgets(int n) => _FindsCountMatcher(n, n); | |
| /// Asserts that the [FinderBase] locates the specified number of candidates. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// expect(find.text('Save'), findsExactly(2)); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [findsNothing], when you want the finder to not find anything. | |
| /// * [findsAny], when you want the finder to find one or more candidates. | |
| /// * [findsOne], when you want the finder to find exactly one candidates. | |
| /// * [findsAtLeast], when you want the finder to find at least a specific number of candidates. | |
| Matcher findsExactly(int n) => _FindsCountMatcher(n, n); | |
| /// Asserts that the [FinderBase] locates at least a number of widgets in the widget tree. | |
| /// | |
| /// This is equivalent to the preferred [findsAtLeast] method. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// expect(find.text('Save'), findsAtLeastNWidgets(2)); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [findsNothing], when you want the finder to not find anything. | |
| /// * [findsAny], when you want the finder to find one or more candidates. | |
| /// * [findsOne], when you want the finder to find exactly one candidate. | |
| /// * [findsExactly], when you want the finder to find a specific number of candidates. | |
| Matcher findsAtLeastNWidgets(int n) => _FindsCountMatcher(n, null); | |
| /// Asserts that the [FinderBase] locates at least the given number of candidates. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// expect(find.text('Save'), findsAtLeast(2)); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [findsNothing], when you want the finder to not find anything. | |
| /// * [findsAny], when you want the finder to find one or more candidates. | |
| /// * [findsOne], when you want the finder to find exactly one candidates. | |
| /// * [findsExactly], when you want the finder to find a specific number of candidates. | |
| Matcher findsAtLeast(int n) => _FindsCountMatcher(n, null); | |
| /// 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(); | |
| /// Default threshold for [isSameColorAs] and [isSameColorSwatchAs]. | |
| const double colorEpsilon = 0.004; | |
| /// Asserts that the object represents the same color swatch as [color] when | |
| /// used to paint. | |
| /// | |
| /// Specifically this matcher checks the object is of type [ColorSwatch] and its | |
| /// color components fall below the delta specified by [threshold]. | |
| /// | |
| /// Note: This doesn't recurse into the swatches [Color] type, instead treating | |
| /// them as [Color]s. | |
| Matcher isSameColorSwatchAs<T>(ColorSwatch<T> color, {double threshold = colorEpsilon}) { | |
| return _ColorSwatchMatcher<T>(color, threshold); | |
| } | |
| /// 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 | |
| /// components fall below the delta specified by [threshold]. | |
| Matcher isSameColorAs(Color color, {double threshold = colorEpsilon}) { | |
| return _ColorMatcher(color, threshold); | |
| } | |
| /// Asserts that the object is a [TextScaler] that reflects the user's font | |
| /// scale preferences from the platform's accessibility settings. | |
| /// | |
| /// This matcher is useful for verifying the text scaling within a widget subtree | |
| /// respects the user accessibility preferences, and not accidentally being | |
| /// shadowed by a [MediaQuery] with a different type of [TextScaler]. | |
| /// | |
| /// In widget tests, the value of the system font scale preference can be | |
| /// changed via [TestPlatformDispatcher.textScaleFactorTestValue]. | |
| /// | |
| /// If `withScaleFactor` is specified and non-null, this matcher also asserts | |
| /// that the [TextScaler]'s' `textScaleFactor` equals `withScaleFactor`. | |
| Matcher isSystemTextScaler({double? withScaleFactor}) => _IsSystemTextScaler(withScaleFactor); | |
| /// 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 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 TypeMatcher<AssertionError> isAssertionError = isA<AssertionError>(); | |
| /// A matcher that compares the type of the actual value to the type argument T. | |
| /// | |
| /// This is identical to [isA] and is included for backwards compatibility. | |
| TypeMatcher<T> isInstanceOf<T>() => isA<T>(); | |
| /// Asserts that two [double]s are equal, within some tolerated error. | |
| /// | |
| /// {@template flutter.flutter_test.moreOrLessEquals} | |
| /// 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} | |
| /// | |
| /// 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 [Matrix4]s are equal, within some tolerated error. | |
| /// | |
| /// {@macro flutter.flutter_test.moreOrLessEquals} | |
| /// | |
| /// See also: | |
| /// | |
| /// * [moreOrLessEquals], which is for [double]s. | |
| /// * [offsetMoreOrLessEquals], which is for [Offset]s. | |
| /// * [matrix3MoreOrLessEquals], which is for [Matrix3]s. | |
| Matcher matrixMoreOrLessEquals(Matrix4 value, {double epsilon = precisionErrorTolerance}) { | |
| return _IsWithinDistance<Matrix4>(_matrixDistance, value, epsilon); | |
| } | |
| /// Asserts that two [Matrix3]s are equal, within some tolerated error. | |
| /// | |
| /// {@macro flutter.flutter_test.moreOrLessEquals} | |
| /// | |
| /// See also: | |
| /// | |
| /// * [moreOrLessEquals], which is for [double]s. | |
| /// * [offsetMoreOrLessEquals], which is for [Offset]s. | |
| /// * [matrixMoreOrLessEquals], which is for [Matrix4]s. | |
| Matcher matrix3MoreOrLessEquals(Matrix3 value, {double epsilon = precisionErrorTolerance}) { | |
| return _IsWithinDistance<Matrix3>(_matrix3Distance, value, epsilon); | |
| } | |
| /// Asserts that two [Offset]s are equal, within some tolerated error. | |
| /// | |
| /// {@macro flutter.flutter_test.moreOrLessEquals} | |
| /// | |
| /// 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 or `Iterable<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`. | |
| /// | |
| /// Only [String] or `Iterable<String>` are allowed types for `value`. | |
| /// | |
| /// 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(Object value) { | |
| assert( | |
| value is String || value is Iterable<String>, | |
| "Only String or Iterable<String> are allowed types for equalsIgnoringHashCodes, it doesn't accept ${value.runtimeType}", | |
| ); | |
| 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); | |
| // Examples can assume: | |
| // late Image image; | |
| // late Future<Image> imageFuture; | |
| // typedef MyWidget = Placeholder; | |
| // late Future<ByteData> someFont; | |
| // late WidgetTester tester; | |
| /// 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} | |
| /// | |
| /// {@template flutter.flutter_test.matchesGoldenFile.custom_fonts} | |
| /// ## Including Fonts | |
| /// | |
| /// Custom fonts may render differently across different platforms, or | |
| /// between different versions of Flutter. For example, a golden file generated | |
| /// on Windows with fonts will likely differ from the one produced by another | |
| /// operating system. Even on the same platform, if the generated golden is | |
| /// tested with a different Flutter version, the test may fail and require an | |
| /// updated image. | |
| /// | |
| /// By default, the Flutter framework uses a font called 'Ahem' which shows | |
| /// squares instead of characters, however, it is possible to render images using | |
| /// custom fonts. For example, this is how to load the 'Roboto' font for a | |
| /// golden test: | |
| /// | |
| /// {@tool snippet} | |
| /// How to load a custom font for golden images. | |
| /// ```dart | |
| /// testWidgets('Creating a golden image with a custom font', (WidgetTester tester) async { | |
| /// // Assuming the 'Roboto.ttf' file is declared in the pubspec.yaml file | |
| /// final Future<ByteData> font = rootBundle.load('path/to/font-file/Roboto.ttf'); | |
| /// | |
| /// final FontLoader fontLoader = FontLoader('Roboto')..addFont(font); | |
| /// await fontLoader.load(); | |
| /// | |
| /// await tester.pumpWidget(const MyWidget()); | |
| /// | |
| /// await expectLater( | |
| /// find.byType(MyWidget), | |
| /// matchesGoldenFile('myWidget.png'), | |
| /// ); | |
| /// }); | |
| /// ``` | |
| /// {@end-tool} | |
| /// | |
| /// The example above loads the desired font only for that specific test. To load | |
| /// a font for all golden file tests, the `FontLoader.load()` call could be | |
| /// moved in the `flutter_test_config.dart`. In this way, the font will always be | |
| /// loaded before a test: | |
| /// | |
| /// {@tool snippet} | |
| /// Loading a custom font from the flutter_test_config.dart file. | |
| /// ```dart | |
| /// Future<void> testExecutable(FutureOr<void> Function() testMain) async { | |
| /// setUpAll(() async { | |
| /// final FontLoader fontLoader = FontLoader('SomeFont')..addFont(someFont); | |
| /// await fontLoader.load(); | |
| /// }); | |
| /// | |
| /// await testMain(); | |
| /// } | |
| /// ``` | |
| /// {@end-tool} | |
| /// {@endtemplate} | |
| /// | |
| /// 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(Object key, {int? version}) { | |
| return switch (key) { | |
| Uri() => MatchesGoldenFile(key, version), | |
| String() => 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 | |
| /// testWidgets('matchesReferenceImage', (WidgetTester tester) async { | |
| /// 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(); | |
| /// addTearDown(picture.dispose); | |
| /// ui.Image referenceImage = await picture.toImage(50, 50); | |
| /// addTearDown(referenceImage.dispose); | |
| /// | |
| /// 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 find a [SemanticsNode] directly, use [CommonFinders.semantics]. | |
| /// These methods will search the semantics tree directly and avoid the edge | |
| /// cases that [SemanticsController.find] sometimes runs into. | |
| /// | |
| /// To retrieve the semantics data of a widget, use [SemanticsController.find] | |
| /// with a [FinderBase] that returns a single widget. Semantics must be enabled | |
| /// in order to use this method. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// testWidgets('matchesSemantics', (WidgetTester tester) async { | |
| /// final SemanticsHandle handle = tester.ensureSemantics(); | |
| /// // ... | |
| /// expect(tester.getSemantics(find.text('hello')), matchesSemantics(label: 'hello')); | |
| /// handle.dispose(); | |
| /// }); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics. | |
| /// * [isSemantics], a similar matcher without default values for flags or actions. | |
| Matcher matchesSemantics({ | |
| String? identifier, | |
| String? traversalParentIdentifier, | |
| String? traversalChildIdentifier, | |
| String? label, | |
| AttributedString? attributedLabel, | |
| String? hint, | |
| AttributedString? attributedHint, | |
| String? value, | |
| AttributedString? attributedValue, | |
| String? increasedValue, | |
| AttributedString? attributedIncreasedValue, | |
| String? decreasedValue, | |
| AttributedString? attributedDecreasedValue, | |
| String? tooltip, | |
| TextDirection? textDirection, | |
| Rect? rect, | |
| Size? size, | |
| double? elevation, | |
| double? thickness, | |
| int? platformViewId, | |
| int? maxValueLength, | |
| int? currentValueLength, | |
| SemanticsValidationResult validationResult = SemanticsValidationResult.none, | |
| ui.SemanticsInputType? inputType, | |
| String? maxValue, | |
| String? minValue, | |
| ui.SemanticsRole? role, | |
| // Flags // | |
| bool hasCheckedState = false, | |
| bool isChecked = false, | |
| bool isCheckStateMixed = false, | |
| bool isSelected = false, | |
| bool hasSelectedState = false, | |
| bool isButton = false, | |
| bool isSlider = false, | |
| bool isKeyboardKey = 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, | |
| bool hasExpandedState = false, | |
| bool isExpanded = false, | |
| bool hasRequiredState = false, | |
| bool isRequired = false, | |
| // Actions // | |
| bool hasTapAction = false, | |
| bool hasFocusAction = 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 hasSetTextAction = 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, | |
| }) { | |
| return _MatchesSemanticsData( | |
| identifier: identifier, | |
| traversalParentIdentifier: traversalParentIdentifier, | |
| traversalChildIdentifier: traversalChildIdentifier, | |
| label: label, | |
| attributedLabel: attributedLabel, | |
| hint: hint, | |
| attributedHint: attributedHint, | |
| value: value, | |
| attributedValue: attributedValue, | |
| increasedValue: increasedValue, | |
| attributedIncreasedValue: attributedIncreasedValue, | |
| decreasedValue: decreasedValue, | |
| attributedDecreasedValue: attributedDecreasedValue, | |
| tooltip: tooltip, | |
| textDirection: textDirection, | |
| rect: rect, | |
| size: size, | |
| platformViewId: platformViewId, | |
| customActions: customActions, | |
| maxValueLength: maxValueLength, | |
| currentValueLength: currentValueLength, | |
| validationResult: validationResult, | |
| inputType: inputType, | |
| minValue: minValue, | |
| maxValue: maxValue, | |
| role: role, | |
| // Flags | |
| hasCheckedState: hasCheckedState, | |
| isChecked: isChecked, | |
| isCheckStateMixed: isCheckStateMixed, | |
| isSelected: isSelected, | |
| hasSelectedState: hasSelectedState, | |
| isButton: isButton, | |
| isSlider: isSlider, | |
| isKeyboardKey: isKeyboardKey, | |
| isLink: isLink, | |
| isFocused: isFocused, | |
| isFocusable: isFocusable, | |
| isTextField: isTextField, | |
| isReadOnly: isReadOnly, | |
| hasEnabledState: hasEnabledState, | |
| isEnabled: isEnabled, | |
| isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup, | |
| isHeader: isHeader, | |
| isObscured: isObscured, | |
| isMultiline: isMultiline, | |
| namesRoute: namesRoute, | |
| scopesRoute: scopesRoute, | |
| isHidden: isHidden, | |
| isImage: isImage, | |
| isLiveRegion: isLiveRegion, | |
| hasToggledState: hasToggledState, | |
| isToggled: isToggled, | |
| hasImplicitScrolling: hasImplicitScrolling, | |
| hasExpandedState: hasExpandedState, | |
| isExpanded: isExpanded, | |
| hasRequiredState: hasRequiredState, | |
| isRequired: isRequired, | |
| // Actions | |
| hasTapAction: hasTapAction, | |
| hasFocusAction: hasFocusAction, | |
| hasLongPressAction: hasLongPressAction, | |
| hasScrollLeftAction: hasScrollLeftAction, | |
| hasScrollRightAction: hasScrollRightAction, | |
| hasScrollUpAction: hasScrollUpAction, | |
| hasScrollDownAction: hasScrollDownAction, | |
| hasIncreaseAction: hasIncreaseAction, | |
| hasDecreaseAction: hasDecreaseAction, | |
| hasShowOnScreenAction: hasShowOnScreenAction, | |
| hasMoveCursorForwardByCharacterAction: hasMoveCursorForwardByCharacterAction, | |
| hasMoveCursorBackwardByCharacterAction: hasMoveCursorBackwardByCharacterAction, | |
| hasMoveCursorForwardByWordAction: hasMoveCursorForwardByWordAction, | |
| hasMoveCursorBackwardByWordAction: hasMoveCursorBackwardByWordAction, | |
| hasSetTextAction: hasSetTextAction, | |
| hasSetSelectionAction: hasSetSelectionAction, | |
| hasCopyAction: hasCopyAction, | |
| hasCutAction: hasCutAction, | |
| hasPasteAction: hasPasteAction, | |
| hasDidGainAccessibilityFocusAction: hasDidGainAccessibilityFocusAction, | |
| hasDidLoseAccessibilityFocusAction: hasDidLoseAccessibilityFocusAction, | |
| hasDismissAction: hasDismissAction, | |
| // Custom actions and overrides | |
| children: children, | |
| onLongPressHint: onLongPressHint, | |
| onTapHint: onTapHint, | |
| ); | |
| } | |
| /// Asserts that a [SemanticsNode] contains the specified information. | |
| /// | |
| /// There are no default expected values, so no unspecified values will be | |
| /// validated. | |
| /// | |
| /// To find a [SemanticsNode] directly, use [CommonFinders.semantics]. | |
| /// These methods will search the semantics tree directly and avoid the edge | |
| /// cases that [SemanticsController.find] sometimes runs into. | |
| /// | |
| /// To retrieve the semantics data of a widget, use [SemanticsController.find] | |
| /// with a [FinderBase] that returns a single widget. Semantics must be enabled | |
| /// in order to use this method. | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics. | |
| /// * [matchesSemantics], a similar matcher with default values for flags and actions. | |
| @Deprecated( | |
| 'Migrate to isSemantics instead. ' | |
| 'This feature was deprecated after v3.40.0-1.0.pre.', | |
| ) | |
| Matcher containsSemantics({ | |
| String? identifier, | |
| String? traversalParentIdentifier, | |
| String? traversalChildIdentifier, | |
| String? label, | |
| AttributedString? attributedLabel, | |
| String? hint, | |
| AttributedString? attributedHint, | |
| String? value, | |
| AttributedString? attributedValue, | |
| String? increasedValue, | |
| AttributedString? attributedIncreasedValue, | |
| String? decreasedValue, | |
| AttributedString? attributedDecreasedValue, | |
| String? tooltip, | |
| TextDirection? textDirection, | |
| Rect? rect, | |
| Size? size, | |
| double? elevation, | |
| double? thickness, | |
| int? platformViewId, | |
| int? maxValueLength, | |
| int? currentValueLength, | |
| SemanticsValidationResult? validationResult, | |
| ui.SemanticsInputType? inputType, | |
| String? maxValue, | |
| String? minValue, | |
| // Flags | |
| bool? hasCheckedState, | |
| bool? isChecked, | |
| bool? isCheckStateMixed, | |
| bool? isSelected, | |
| bool? hasSelectedState, | |
| bool? isButton, | |
| bool? isSlider, | |
| bool? isKeyboardKey, | |
| bool? isLink, | |
| bool? isFocused, | |
| bool? isFocusable, | |
| bool? isTextField, | |
| bool? isReadOnly, | |
| bool? hasEnabledState, | |
| bool? isEnabled, | |
| bool? isInMutuallyExclusiveGroup, | |
| bool? isHeader, | |
| bool? isObscured, | |
| bool? isMultiline, | |
| bool? namesRoute, | |
| bool? scopesRoute, | |
| bool? isHidden, | |
| bool? isImage, | |
| bool? isLiveRegion, | |
| bool? hasToggledState, | |
| bool? isToggled, | |
| bool? hasImplicitScrolling, | |
| bool? hasExpandedState, | |
| bool? isExpanded, | |
| bool? hasRequiredState, | |
| bool? isRequired, | |
| // Actions | |
| bool? hasTapAction, | |
| bool? hasFocusAction, | |
| bool? hasLongPressAction, | |
| bool? hasScrollLeftAction, | |
| bool? hasScrollRightAction, | |
| bool? hasScrollUpAction, | |
| bool? hasScrollDownAction, | |
| bool? hasIncreaseAction, | |
| bool? hasDecreaseAction, | |
| bool? hasShowOnScreenAction, | |
| bool? hasMoveCursorForwardByCharacterAction, | |
| bool? hasMoveCursorBackwardByCharacterAction, | |
| bool? hasMoveCursorForwardByWordAction, | |
| bool? hasMoveCursorBackwardByWordAction, | |
| bool? hasSetTextAction, | |
| bool? hasSetSelectionAction, | |
| bool? hasCopyAction, | |
| bool? hasCutAction, | |
| bool? hasPasteAction, | |
| bool? hasDidGainAccessibilityFocusAction, | |
| bool? hasDidLoseAccessibilityFocusAction, | |
| bool? hasDismissAction, | |
| // Custom actions and overrides | |
| String? onTapHint, | |
| String? onLongPressHint, | |
| List<CustomSemanticsAction>? customActions, | |
| List<Matcher>? children, | |
| }) => isSemantics( | |
| identifier: identifier, | |
| traversalParentIdentifier: traversalParentIdentifier, | |
| traversalChildIdentifier: traversalChildIdentifier, | |
| label: label, | |
| attributedLabel: attributedLabel, | |
| hint: hint, | |
| attributedHint: attributedHint, | |
| value: value, | |
| attributedValue: attributedValue, | |
| increasedValue: increasedValue, | |
| attributedIncreasedValue: attributedIncreasedValue, | |
| decreasedValue: decreasedValue, | |
| attributedDecreasedValue: attributedDecreasedValue, | |
| tooltip: tooltip, | |
| textDirection: textDirection, | |
| rect: rect, | |
| size: size, | |
| elevation: elevation, | |
| thickness: thickness, | |
| platformViewId: platformViewId, | |
| maxValueLength: maxValueLength, | |
| currentValueLength: currentValueLength, | |
| validationResult: validationResult, | |
| inputType: inputType, | |
| maxValue: maxValue, | |
| minValue: minValue, | |
| // Flags | |
| hasCheckedState: hasCheckedState, | |
| isChecked: isChecked, | |
| isCheckStateMixed: isCheckStateMixed, | |
| isSelected: isSelected, | |
| hasSelectedState: hasSelectedState, | |
| isButton: isButton, | |
| isSlider: isSlider, | |
| isKeyboardKey: isKeyboardKey, | |
| isLink: isLink, | |
| isFocused: isFocused, | |
| isFocusable: isFocusable, | |
| isTextField: isTextField, | |
| isReadOnly: isReadOnly, | |
| hasEnabledState: hasEnabledState, | |
| isEnabled: isEnabled, | |
| isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup, | |
| isHeader: isHeader, | |
| isObscured: isObscured, | |
| isMultiline: isMultiline, | |
| namesRoute: namesRoute, | |
| scopesRoute: scopesRoute, | |
| isHidden: isHidden, | |
| isImage: isImage, | |
| isLiveRegion: isLiveRegion, | |
| hasToggledState: hasToggledState, | |
| isToggled: isToggled, | |
| hasImplicitScrolling: hasImplicitScrolling, | |
| hasExpandedState: hasExpandedState, | |
| isExpanded: isExpanded, | |
| hasRequiredState: hasRequiredState, | |
| isRequired: isRequired, | |
| // Actions | |
| hasTapAction: hasTapAction, | |
| hasFocusAction: hasFocusAction, | |
| hasLongPressAction: hasLongPressAction, | |
| hasScrollLeftAction: hasScrollLeftAction, | |
| hasScrollRightAction: hasScrollRightAction, | |
| hasScrollUpAction: hasScrollUpAction, | |
| hasScrollDownAction: hasScrollDownAction, | |
| hasIncreaseAction: hasIncreaseAction, | |
| hasDecreaseAction: hasDecreaseAction, | |
| hasShowOnScreenAction: hasShowOnScreenAction, | |
| hasMoveCursorForwardByCharacterAction: hasMoveCursorForwardByCharacterAction, | |
| hasMoveCursorBackwardByCharacterAction: hasMoveCursorBackwardByCharacterAction, | |
| hasMoveCursorForwardByWordAction: hasMoveCursorForwardByWordAction, | |
| hasMoveCursorBackwardByWordAction: hasMoveCursorBackwardByWordAction, | |
| hasSetTextAction: hasSetTextAction, | |
| hasSetSelectionAction: hasSetSelectionAction, | |
| hasCopyAction: hasCopyAction, | |
| hasCutAction: hasCutAction, | |
| hasPasteAction: hasPasteAction, | |
| hasDidGainAccessibilityFocusAction: hasDidGainAccessibilityFocusAction, | |
| hasDidLoseAccessibilityFocusAction: hasDidLoseAccessibilityFocusAction, | |
| hasDismissAction: hasDismissAction, | |
| // Custom actions and overrides | |
| onTapHint: onTapHint, | |
| onLongPressHint: onLongPressHint, | |
| customActions: customActions, | |
| children: children, | |
| ); | |
| /// Asserts that a [SemanticsNode] contains the specified information. | |
| /// | |
| /// There are no default expected values, so no unspecified values will be | |
| /// validated. | |
| /// | |
| /// To find a [SemanticsNode] directly, use [CommonFinders.semantics]. | |
| /// These methods will search the semantics tree directly and avoid the edge | |
| /// cases that [SemanticsController.find] sometimes runs into. | |
| /// | |
| /// To retrieve the semantics data of a widget, use [SemanticsController.find] | |
| /// with a [FinderBase] that returns a single widget. Semantics must be enabled | |
| /// in order to use this method. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// testWidgets('isSemantics', (WidgetTester tester) async { | |
| /// final SemanticsHandle handle = tester.ensureSemantics(); | |
| /// // ... | |
| /// expect(tester.getSemantics(find.text('hello')), isSemantics(label: 'hello')); | |
| /// handle.dispose(); | |
| /// }); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [SemanticsController.find] under [WidgetTester.semantics], the tester method which retrieves semantics. | |
| /// * [matchesSemantics], a similar matcher with default values for flags and actions. | |
| Matcher isSemantics({ | |
| String? identifier, | |
| String? traversalParentIdentifier, | |
| String? traversalChildIdentifier, | |
| String? label, | |
| AttributedString? attributedLabel, | |
| String? hint, | |
| AttributedString? attributedHint, | |
| String? value, | |
| AttributedString? attributedValue, | |
| String? increasedValue, | |
| AttributedString? attributedIncreasedValue, | |
| String? decreasedValue, | |
| AttributedString? attributedDecreasedValue, | |
| String? tooltip, | |
| TextDirection? textDirection, | |
| Rect? rect, | |
| Size? size, | |
| double? elevation, | |
| double? thickness, | |
| int? platformViewId, | |
| int? maxValueLength, | |
| int? currentValueLength, | |
| SemanticsValidationResult? validationResult, | |
| ui.SemanticsInputType? inputType, | |
| String? maxValue, | |
| String? minValue, | |
| ui.SemanticsRole? role, | |
| // Flags | |
| bool? hasCheckedState, | |
| bool? isChecked, | |
| bool? isCheckStateMixed, | |
| bool? isSelected, | |
| bool? hasSelectedState, | |
| bool? isButton, | |
| bool? isSlider, | |
| bool? isKeyboardKey, | |
| bool? isLink, | |
| bool? isFocused, | |
| bool? isFocusable, | |
| bool? isTextField, | |
| bool? isReadOnly, | |
| bool? hasEnabledState, | |
| bool? isEnabled, | |
| bool? isInMutuallyExclusiveGroup, | |
| bool? isHeader, | |
| bool? isObscured, | |
| bool? isMultiline, | |
| bool? namesRoute, | |
| bool? scopesRoute, | |
| bool? isHidden, | |
| bool? isImage, | |
| bool? isLiveRegion, | |
| bool? hasToggledState, | |
| bool? isToggled, | |
| bool? hasImplicitScrolling, | |
| bool? hasExpandedState, | |
| bool? isExpanded, | |
| bool? hasRequiredState, | |
| bool? isRequired, | |
| // Actions | |
| bool? hasTapAction, | |
| bool? hasFocusAction, | |
| bool? hasLongPressAction, | |
| bool? hasScrollLeftAction, | |
| bool? hasScrollRightAction, | |
| bool? hasScrollUpAction, | |
| bool? hasScrollDownAction, | |
| bool? hasIncreaseAction, | |
| bool? hasDecreaseAction, | |
| bool? hasShowOnScreenAction, | |
| bool? hasMoveCursorForwardByCharacterAction, | |
| bool? hasMoveCursorBackwardByCharacterAction, | |
| bool? hasMoveCursorForwardByWordAction, | |
| bool? hasMoveCursorBackwardByWordAction, | |
| bool? hasSetTextAction, | |
| bool? hasSetSelectionAction, | |
| bool? hasCopyAction, | |
| bool? hasCutAction, | |
| bool? hasPasteAction, | |
| bool? hasDidGainAccessibilityFocusAction, | |
| bool? hasDidLoseAccessibilityFocusAction, | |
| bool? hasDismissAction, | |
| // Custom actions and overrides | |
| String? onTapHint, | |
| String? onLongPressHint, | |
| List<CustomSemanticsAction>? customActions, | |
| List<Matcher>? children, | |
| }) { | |
| return _MatchesSemanticsData( | |
| identifier: identifier, | |
| traversalChildIdentifier: traversalChildIdentifier, | |
| traversalParentIdentifier: traversalParentIdentifier, | |
| label: label, | |
| attributedLabel: attributedLabel, | |
| hint: hint, | |
| attributedHint: attributedHint, | |
| value: value, | |
| attributedValue: attributedValue, | |
| increasedValue: increasedValue, | |
| attributedIncreasedValue: attributedIncreasedValue, | |
| decreasedValue: decreasedValue, | |
| attributedDecreasedValue: attributedDecreasedValue, | |
| tooltip: tooltip, | |
| textDirection: textDirection, | |
| rect: rect, | |
| size: size, | |
| platformViewId: platformViewId, | |
| customActions: customActions, | |
| maxValueLength: maxValueLength, | |
| currentValueLength: currentValueLength, | |
| validationResult: validationResult, | |
| inputType: inputType, | |
| minValue: minValue, | |
| maxValue: maxValue, | |
| role: role, | |
| // Flags | |
| hasCheckedState: hasCheckedState, | |
| isChecked: isChecked, | |
| isCheckStateMixed: isCheckStateMixed, | |
| isSelected: isSelected, | |
| hasSelectedState: hasSelectedState, | |
| isButton: isButton, | |
| isSlider: isSlider, | |
| isKeyboardKey: isKeyboardKey, | |
| isLink: isLink, | |
| isFocused: isFocused, | |
| isFocusable: isFocusable, | |
| isTextField: isTextField, | |
| isReadOnly: isReadOnly, | |
| hasEnabledState: hasEnabledState, | |
| isEnabled: isEnabled, | |
| isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup, | |
| isHeader: isHeader, | |
| isObscured: isObscured, | |
| isMultiline: isMultiline, | |
| namesRoute: namesRoute, | |
| scopesRoute: scopesRoute, | |
| isHidden: isHidden, | |
| isImage: isImage, | |
| isLiveRegion: isLiveRegion, | |
| hasToggledState: hasToggledState, | |
| isToggled: isToggled, | |
| hasImplicitScrolling: hasImplicitScrolling, | |
| hasExpandedState: hasExpandedState, | |
| isExpanded: isExpanded, | |
| hasRequiredState: hasRequiredState, | |
| isRequired: isRequired, | |
| // Actions | |
| hasTapAction: hasTapAction, | |
| hasFocusAction: hasFocusAction, | |
| hasLongPressAction: hasLongPressAction, | |
| hasScrollLeftAction: hasScrollLeftAction, | |
| hasScrollRightAction: hasScrollRightAction, | |
| hasScrollUpAction: hasScrollUpAction, | |
| hasScrollDownAction: hasScrollDownAction, | |
| hasIncreaseAction: hasIncreaseAction, | |
| hasDecreaseAction: hasDecreaseAction, | |
| hasShowOnScreenAction: hasShowOnScreenAction, | |
| hasMoveCursorForwardByCharacterAction: hasMoveCursorForwardByCharacterAction, | |
| hasMoveCursorBackwardByCharacterAction: hasMoveCursorBackwardByCharacterAction, | |
| hasMoveCursorForwardByWordAction: hasMoveCursorForwardByWordAction, | |
| hasMoveCursorBackwardByWordAction: hasMoveCursorBackwardByWordAction, | |
| hasSetTextAction: hasSetTextAction, | |
| hasSetSelectionAction: hasSetSelectionAction, | |
| hasCopyAction: hasCopyAction, | |
| hasCutAction: hasCutAction, | |
| hasPasteAction: hasPasteAction, | |
| hasDidGainAccessibilityFocusAction: hasDidGainAccessibilityFocusAction, | |
| hasDidLoseAccessibilityFocusAction: hasDidLoseAccessibilityFocusAction, | |
| hasDismissAction: hasDismissAction, | |
| // Custom actions and overrides | |
| children: children, | |
| onLongPressHint: onLongPressHint, | |
| onTapHint: onTapHint, | |
| ); | |
| } | |
| /// Asserts that a [CapturedAccessibilityAnnouncement] matches the expected message. | |
| /// | |
| /// The [message] argument matches the [CapturedAccessibilityAnnouncement.message]. | |
| /// The [textDirection] argument, if non-null, matches the [CapturedAccessibilityAnnouncement.textDirection]. | |
| /// The [assertiveness] argument, if non-null, matches the [CapturedAccessibilityAnnouncement.assertiveness]. | |
| /// | |
| /// ## Sample code | |
| /// | |
| /// ```dart | |
| /// await SemanticsService.sendAnnouncement(tester.view, 'Hello', TextDirection.ltr); | |
| /// expect(tester.takeAnnouncements(), contains(isAccessibilityAnnouncement('Hello'))); | |
| /// ``` | |
| /// | |
| /// See also: | |
| /// | |
| /// * [WidgetTester.takeAnnouncements], which retrieves the announcements in unit tests. | |
| /// * [SemanticsService.sendAnnouncement], which sends an announcement. | |
| Matcher isAccessibilityAnnouncement( | |
| String message, { | |
| TextDirection? textDirection, | |
| Assertiveness? assertiveness, | |
| int? viewId, | |
| }) { | |
| return _MatchesAccessibilityAnnouncement( | |
| expectedMessage: message, | |
| expectedTextDirection: textDirection, | |
| expectedAssertiveness: assertiveness, | |
| expectedViewId: viewId, | |
| ); | |
| } | |
| /// 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 | |
| /// testWidgets('isSemantics', (WidgetTester tester) async { | |
| /// 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. | |
| /// * [labeledTapTargetGuideline], for enforcing labels on tappable areas. | |
| 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 _FindsCountMatcher extends Matcher { | |
| const _FindsCountMatcher(this.min, this.max); | |
| final int? min; | |
| final int? max; | |
| @override | |
| bool matches(covariant FinderBase<dynamic> finder, Map<dynamic, dynamic> matchState) { | |
| assert(min != null || max != null); | |
| assert(min == null || max == null || min! <= max!); | |
| matchState[FinderBase] = finder; | |
| var count = 0; | |
| final Iterator<dynamic> 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 candidate'); | |
| } | |
| return description.add('exactly $min matching candidates'); | |
| } | |
| if (min == null) { | |
| if (max == 0) { | |
| return description.add('no matching candidates'); | |
| } | |
| if (max == 1) { | |
| return description.add('at most one matching candidate'); | |
| } | |
| return description.add('at most $max matching candidates'); | |
| } | |
| if (max == null) { | |
| if (min == 1) { | |
| return description.add('at least one matching candidate'); | |
| } | |
| return description.add('at least $min matching candidates'); | |
| } | |
| return description.add('between $min and $max matching candidates (inclusive)'); | |
| } | |
| @override | |
| Description describeMismatch( | |
| dynamic item, | |
| Description mismatchDescription, | |
| Map<dynamic, dynamic> matchState, | |
| bool verbose, | |
| ) { | |
| final finder = matchState[FinderBase] as FinderBase<dynamic>; | |
| final int count = finder.found.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 Function(Widget widget) predicate) { | |
| final Iterable<Element> nodes = finder.evaluate(); | |
| if (nodes.length != 1) { | |
| return false; | |
| } | |
| var 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; | |
| } | |
| var 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 _IsSystemTextScaler extends Matcher { | |
| const _IsSystemTextScaler(this.expectedUserTextScaleFactor); | |
| final double? expectedUserTextScaleFactor; | |
| @override | |
| bool matches(dynamic item, Map<dynamic, dynamic> matchState) { | |
| return switch (item) { | |
| SystemTextScaler(:final double textScaleFactor) | |
| when expectedUserTextScaleFactor != null && | |
| expectedUserTextScaleFactor != textScaleFactor => | |
| failWithDescription( | |
| matchState, | |
| 'expecting a scale factor of $expectedUserTextScaleFactor, but got $textScaleFactor', | |
| ), | |
| SystemTextScaler() => true, | |
| _ => failWithDescription(matchState, '${item.runtimeType} is not a SystemTextScaler'), | |
| }; | |
| } | |
| @override | |
| Description describe(Description description) { | |
| final scaleFactorExpectation = expectedUserTextScaleFactor == null | |
| ? '' | |
| : '(${expectedUserTextScaleFactor}x)'; | |
| return description.add( | |
| 'A SystemTextScaler that reflects the font scale settings in the system user preference $scaleFactorExpectation', | |
| ); | |
| } | |
| 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 _HasOneLineDescription extends Matcher { | |
| const _HasOneLineDescription(); | |
| @override | |
| bool matches(dynamic object, Map<dynamic, dynamic> matchState) { | |
| final 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(Object v) | |
| : _value = _normalize(v), | |
| _stringValue = v is String ? _normalizeString(v) : null; | |
| final Iterable<String> _value; | |
| final String? _stringValue; | |
| static final Object _lineNumberValueKey = Object(); | |
| static final Object _expectedLineValueKey = Object(); | |
| static final Object _seenLineValueKey = Object(); | |
| static String _normalizeString(String value) { | |
| return value.replaceAll(RegExp(r'#[\da-fA-F]{5}'), '#00000'); | |
| } | |
| static Iterable<String> _normalize(Object value, {bool expected = true}) { | |
| if (value is String) { | |
| return LineSplitter.split(value) | |
| .map<String>((dynamic item) => _normalizeString(item.toString())); | |
| } | |
| if (value is Iterable<String>) { | |
| return value.map<String>((dynamic item) => _normalizeString(item.toString())); | |
| } | |
| throw ArgumentError( | |
| 'The specified ${expected ? 'expected' : 'comparison'} value for ' | |
| 'equalsIgnoringHashCodes must be a String or an Iterable<String>, ' | |
| 'not a ${value.runtimeType}', | |
| ); | |
| } | |
| @override | |
| bool matches(dynamic object, Map<dynamic, dynamic> matchState) { | |
| final Iterable<String> normalized = _normalize(object as Object, expected: false); | |
| final Iterator<String> expectedIt = _value.iterator; | |
| final Iterator<String> seenIt = normalized.iterator; | |
| var lineNumber = 1; | |
| bool hasExpected = expectedIt.moveNext(); | |
| bool hasSeen = seenIt.moveNext(); | |
| while (hasExpected && hasSeen) { | |
| if (!equals(expectedIt.current).matches(seenIt.current, matchState)) { | |
| matchState[_lineNumberValueKey] = lineNumber; | |
| matchState[_expectedLineValueKey] = expectedIt.current; | |
| matchState[_seenLineValueKey] = seenIt.current; | |
| return false; | |
| } | |
| lineNumber += 1; | |
| hasExpected = expectedIt.moveNext(); | |
| hasSeen = seenIt.moveNext(); | |
| } | |
| return !hasExpected && !hasSeen; | |
| } | |
| @override | |
| Description describe(Description description) { | |
| return description.add('normalized value matches\n').addDescriptionOf(_stringValue ?? _value); | |
| } | |
| @override | |
| Description describeMismatch( | |
| dynamic item, | |
| Description mismatchDescription, | |
| Map<dynamic, dynamic> matchState, | |
| bool verbose, | |
| ) { | |
| if (matchState.containsKey(_lineNumberValueKey) && | |
| matchState.containsKey(_expectedLineValueKey) && | |
| matchState.containsKey(_seenLineValueKey)) { | |
| final lineNumber = matchState[_lineNumberValueKey] as int; | |
| if (lineNumber > 1) { | |
| mismatchDescription = mismatchDescription | |
| .add('Lines $lineNumber differed, expected: \n') | |
| .addDescriptionOf(matchState[_expectedLineValueKey]) | |
| .add('\nbut got\n') | |
| .addDescriptionOf(matchState[_seenLineValueKey]); | |
| } | |
| } | |
| 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 (var 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 issues = <String>[]; | |
| var description = object.toStringDeep() as String; // ignore: avoid_dynamic_calls | |
| 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 (var 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 prefixLineOne = 'PREFIX_LINE_ONE____'; | |
| const prefixOtherLines = 'PREFIX_OTHER_LINES_'; | |
| final prefixIssues = <String>[]; | |
| var descriptionWithPrefixes = | |
| // ignore: avoid_dynamic_calls | |
| 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 (var i = 1; i < linesWithPrefixes.length; ++i) { | |
| if (!linesWithPrefixes[i].startsWith(prefixOtherLines)) { | |
| prefixIssues.add('Line ${i + 1} does not contain the expected prefix.'); | |
| } | |
| } | |
| final 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, their type is declared as | |
| /// `Never`, which is the bottom type in dart to which all other types can be | |
| /// assigned to. | |
| /// | |
| /// 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(Never a, Never 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 _matrixDistance(Matrix4 a, Matrix4 b) { | |
| var delta = 0.0; | |
| for (var i = 0; i < 16; i += 1) { | |
| delta = math.max<double>((a[i] - b[i]).abs(), delta); | |
| } | |
| return delta; | |
| } | |
| double _matrix3Distance(Matrix3 a, Matrix3 b) { | |
| var delta = 0.0; | |
| for (var i = 0; i < 9; i += 1) { | |
| delta = math.max<double>((a[i] - b[i]).abs(), delta); | |
| } | |
| return delta; | |
| } | |
| double _sizeDistance(Size a, Size b) { | |
| final 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(dynamic object, Map<dynamic, dynamic> matchState) { | |
| if (object is! T) { | |
| return false; | |
| } | |
| if (object == value) { | |
| return true; | |
| } | |
| final num distance = distanceFunction(object, 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( | |
| dynamic 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(dynamic object, Map<dynamic, dynamic> matchState) { | |
| if (object is! num) { | |
| return false; | |
| } | |
| if (object == value) { | |
| return true; | |
| } | |
| return (object - value).abs() <= epsilon; | |
| } | |
| @override | |
| Description describe(Description description) => description.add('$value (±$epsilon)'); | |
| @override | |
| Description describeMismatch( | |
| dynamic 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 (var 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 Object? 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({required 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 const (RenderClipPath): | |
| case const (RenderClipOval): | |
| case const (RenderClipRect): | |
| case const (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 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 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({required 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 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 shapeClipper = renderObject.clipper! as ShapeBorderClipper; | |
| if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) { | |
| return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}'); | |
| } | |
| final 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 shapeClipper = renderObject.clipper! as ShapeBorderClipper; | |
| if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) { | |
| return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}'); | |
| } | |
| final 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 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; | |
| late math.Random random; | |
| @override | |
| bool matches(covariant Path actualPath, Map<dynamic, dynamic> matchState) { | |
| for (var i = 0; i < sampleSize; i += 1) { | |
| for (var j = 0; j < sampleSize; j += 1) { | |
| final offset = Offset( | |
| i * (areaToCompare.width / sampleSize), | |
| j * (areaToCompare.height / sampleSize), | |
| ); | |
| if (!_samplePoint(matchState, actualPath, offset)) { | |
| return false; | |
| } | |
| final 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 _ColorSwatchMatcher<T> extends Matcher { | |
| _ColorSwatchMatcher(this._target, this._threshold); | |
| final ColorSwatch<T> _target; | |
| final double _threshold; | |
| @override | |
| Description describe(Description description) { | |
| return description.add('matches color swatch "$_target" with threshold "$_threshold".'); | |
| } | |
| @override | |
| bool matches(dynamic item, Map<dynamic, dynamic> matchState) { | |
| if (item is ColorSwatch) { | |
| final matcher = _ColorMatcher(_target, _threshold); | |
| if (!matcher.matches(item, matchState)) { | |
| return false; | |
| } | |
| for (final T key in _target.keys) { | |
| final matcher = _ColorMatcher(_target[key]!, _threshold); | |
| if (!matcher.matches(item[key], matchState)) { | |
| return false; | |
| } | |
| } | |
| return item.keys.length == _target.keys.length; | |
| } else { | |
| return false; | |
| } | |
| } | |
| } | |
| class _ColorMatcher extends Matcher { | |
| _ColorMatcher(this._target, this._threshold); | |
| final ui.Color _target; | |
| final double _threshold; | |
| @override | |
| Description describe(Description description) { | |
| return description.add('matches color "$_target" with threshold "$_threshold".'); | |
| } | |
| @override | |
| bool matches(dynamic item, Map<dynamic, dynamic> matchState) { | |
| return item is ui.Color && | |
| item.colorSpace == _target.colorSpace && | |
| (item.a - _target.a).abs() <= _threshold && | |
| (item.r - _target.r).abs() <= _threshold && | |
| (item.g - _target.g).abs() <= _threshold && | |
| (item.b - _target.b).abs() <= _threshold; | |
| } | |
| } | |
| int _countDifferentPixels(Uint8List imageA, Uint8List imageB) { | |
| assert(imageA.length == imageB.length); | |
| var delta = 0; | |
| for (var 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; | |
| final bool disposeImage; // set to true if the matcher created and owns the image and must therefore dispose it. | |
| if (item is Future<ui.Image>) { | |
| imageFuture = item; | |
| disposeImage = false; | |
| } else if (item is ui.Image) { | |
| imageFuture = Future<ui.Image>.value(item); | |
| disposeImage = false; | |
| } else { | |
| final 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); | |
| disposeImage = true; | |
| } | |
| final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance; | |
| return binding.runAsync<String?>(() async { | |
| final ui.Image image = await imageFuture; | |
| try { | |
| 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'; | |
| } finally { | |
| if (disposeImage) { | |
| image.dispose(); | |
| } | |
| } | |
| }); | |
| } | |
| @override | |
| Description describe(Description description) { | |
| return description.add('rasterized image matches that of a $referenceImage reference image'); | |
| } | |
| } | |
| class _MatchesSemanticsData extends Matcher { | |
| _MatchesSemanticsData({ | |
| required this.identifier, | |
| required this.traversalParentIdentifier, | |
| required this.traversalChildIdentifier, | |
| required this.label, | |
| required this.attributedLabel, | |
| required this.hint, | |
| required this.attributedHint, | |
| required this.value, | |
| required this.attributedValue, | |
| required this.increasedValue, | |
| required this.attributedIncreasedValue, | |
| required this.decreasedValue, | |
| required this.attributedDecreasedValue, | |
| required this.tooltip, | |
| required this.textDirection, | |
| required this.rect, | |
| required this.size, | |
| required this.platformViewId, | |
| required this.maxValueLength, | |
| required this.currentValueLength, | |
| required this.validationResult, | |
| required this.inputType, | |
| required this.minValue, | |
| required this.maxValue, | |
| required this.role, | |
| // Flags | |
| required bool? hasCheckedState, | |
| required bool? isChecked, | |
| required bool? isCheckStateMixed, | |
| required bool? isSelected, | |
| required bool? hasSelectedState, | |
| required bool? isButton, | |
| required bool? isSlider, | |
| required bool? isKeyboardKey, | |
| required bool? isLink, | |
| required bool? isFocused, | |
| required bool? isFocusable, | |
| required bool? isTextField, | |
| required bool? isReadOnly, | |
| required bool? hasEnabledState, | |
| required bool? isEnabled, | |
| required bool? isInMutuallyExclusiveGroup, | |
| required bool? isHeader, | |
| required bool? isObscured, | |
| required bool? isMultiline, | |
| required bool? namesRoute, | |
| required bool? scopesRoute, | |
| required bool? isHidden, | |
| required bool? isImage, | |
| required bool? isLiveRegion, | |
| required bool? hasToggledState, | |
| required bool? isToggled, | |
| required bool? hasImplicitScrolling, | |
| required bool? hasExpandedState, | |
| required bool? isExpanded, | |
| required bool? hasRequiredState, | |
| required bool? isRequired, | |
| // Actions | |
| required bool? hasTapAction, | |
| required bool? hasFocusAction, | |
| required bool? hasLongPressAction, | |
| required bool? hasScrollLeftAction, | |
| required bool? hasScrollRightAction, | |
| required bool? hasScrollUpAction, | |
| required bool? hasScrollDownAction, | |
| required bool? hasIncreaseAction, | |
| required bool? hasDecreaseAction, | |
| required bool? hasShowOnScreenAction, | |
| required bool? hasMoveCursorForwardByCharacterAction, | |
| required bool? hasMoveCursorBackwardByCharacterAction, | |
| required bool? hasMoveCursorForwardByWordAction, | |
| required bool? hasMoveCursorBackwardByWordAction, | |
| required bool? hasSetTextAction, | |
| required bool? hasSetSelectionAction, | |
| required bool? hasCopyAction, | |
| required bool? hasCutAction, | |
| required bool? hasPasteAction, | |
| required bool? hasDidGainAccessibilityFocusAction, | |
| required bool? hasDidLoseAccessibilityFocusAction, | |
| required bool? hasDismissAction, | |
| // Custom actions and overrides | |
| required String? onTapHint, | |
| required String? onLongPressHint, | |
| required this.customActions, | |
| required this.children, | |
| }) : flags = <String, bool>{ | |
| 'hasCheckedState': ?hasCheckedState, | |
| 'isChecked': ?isChecked, | |
| 'isCheckStateMixed': ?isCheckStateMixed, | |
| 'isSelected': ?isSelected, | |
| 'hasSelectedState': ?hasSelectedState, | |
| 'isButton': ?isButton, | |
| 'isSlider': ?isSlider, | |
| 'isKeyboardKey': ?isKeyboardKey, | |
| 'isLink': ?isLink, | |
| 'isTextField': ?isTextField, | |
| 'isReadOnly': ?isReadOnly, | |
| 'isFocused': ?isFocused, | |
| 'isFocusable': ?isFocusable, | |
| 'hasEnabledState': ?hasEnabledState, | |
| 'isEnabled': ?isEnabled, | |
| 'isInMutuallyExclusiveGroup': ?isInMutuallyExclusiveGroup, | |
| 'isHeader': ?isHeader, | |
| 'isObscured': ?isObscured, | |
| 'isMultiline': ?isMultiline, | |
| 'namesRoute': ?namesRoute, | |
| 'scopesRoute': ?scopesRoute, | |
| 'isHidden': ?isHidden, | |
| 'isImage': ?isImage, | |
| 'isLiveRegion': ?isLiveRegion, | |
| 'hasToggledState': ?hasToggledState, | |
| 'isToggled': ?isToggled, | |
| 'hasImplicitScrolling': ?hasImplicitScrolling, | |
| 'hasExpandedState': ?hasExpandedState, | |
| 'isExpanded': ?isExpanded, | |
| 'hasRequiredState': ?hasRequiredState, | |
| 'isRequired': ?isRequired, | |
| }, | |
| actions = <SemanticsAction, bool>{ | |
| SemanticsAction.tap: ?hasTapAction, | |
| SemanticsAction.focus: ?hasFocusAction, | |
| SemanticsAction.longPress: ?hasLongPressAction, | |
| SemanticsAction.scrollLeft: ?hasScrollLeftAction, | |
| SemanticsAction.scrollRight: ?hasScrollRightAction, | |
| SemanticsAction.scrollUp: ?hasScrollUpAction, | |
| SemanticsAction.scrollDown: ?hasScrollDownAction, | |
| SemanticsAction.increase: ?hasIncreaseAction, | |
| SemanticsAction.decrease: ?hasDecreaseAction, | |
| SemanticsAction.showOnScreen: ?hasShowOnScreenAction, | |
| SemanticsAction.moveCursorForwardByCharacter: ?hasMoveCursorForwardByCharacterAction, | |
| SemanticsAction.moveCursorBackwardByCharacter: ?hasMoveCursorBackwardByCharacterAction, | |
| SemanticsAction.setSelection: ?hasSetSelectionAction, | |
| SemanticsAction.copy: ?hasCopyAction, | |
| SemanticsAction.cut: ?hasCutAction, | |
| SemanticsAction.paste: ?hasPasteAction, | |
| SemanticsAction.didGainAccessibilityFocus: ?hasDidGainAccessibilityFocusAction, | |
| SemanticsAction.didLoseAccessibilityFocus: ?hasDidLoseAccessibilityFocusAction, | |
| SemanticsAction.customAction: ?customActions?.isNotEmpty, | |
| SemanticsAction.dismiss: ?hasDismissAction, | |
| SemanticsAction.moveCursorForwardByWord: ?hasMoveCursorForwardByWordAction, | |
| SemanticsAction.moveCursorBackwardByWord: ?hasMoveCursorBackwardByWordAction, | |
| SemanticsAction.setText: ?hasSetTextAction, | |
| }, | |
| hintOverrides = onTapHint == null && onLongPressHint == null | |
| ? null | |
| : SemanticsHintOverrides(onTapHint: onTapHint, onLongPressHint: onLongPressHint); | |
| final String? identifier; | |
| final String? traversalParentIdentifier; | |
| final String? traversalChildIdentifier; | |
| final String? label; | |
| final AttributedString? attributedLabel; | |
| final String? hint; | |
| final AttributedString? attributedHint; | |
| final String? value; | |
| final AttributedString? attributedValue; | |
| final String? increasedValue; | |
| final AttributedString? attributedIncreasedValue; | |
| final String? decreasedValue; | |
| final AttributedString? attributedDecreasedValue; | |
| final String? tooltip; | |
| final SemanticsHintOverrides? hintOverrides; | |
| final List<CustomSemanticsAction>? customActions; | |
| final TextDirection? textDirection; | |
| final Rect? rect; | |
| final Size? size; | |
| final int? platformViewId; | |
| final int? maxValueLength; | |
| final int? currentValueLength; | |
| final ui.SemanticsInputType? inputType; | |
| final List<Matcher>? children; | |
| final SemanticsValidationResult? validationResult; | |
| final String? maxValue; | |
| final String? minValue; | |
| final ui.SemanticsRole? role; | |
| /// There are three possible states for these two maps: | |
| /// | |
| /// 1. If the flag/action maps to `true`, then it must be present in the SemanticData | |
| /// 2. If the flag/action maps to `false`, then it must not be present in the SemanticData | |
| /// 3. If the flag/action is not in the map, then it will not be validated against | |
| final Map<SemanticsAction, bool> actions; | |
| final Map<String, bool> flags; | |
| @override | |
| Description describe(Description description, [String? index]) { | |
| description.add('${index == null ? '' : 'Child $index '}has semantics'); | |
| if (label != null) { | |
| description.add(' with label: ${_escape(label!)}'); | |
| } | |
| if (attributedLabel != null) { | |
| description.add(' with attributedLabel: ${_escapeAttributedString(attributedLabel!)}'); | |
| } | |
| if (value != null) { | |
| description.add(' with value: ${_escape(value!)}'); | |
| } | |
| if (attributedValue != null) { | |
| description.add(' with attributedValue: ${_escapeAttributedString(attributedValue!)}'); | |
| } | |
| if (hint != null) { | |
| description.add(' with hint: ${_escape(hint!)}'); | |
| } | |
| if (attributedHint != null) { | |
| description.add(' with attributedHint: ${_escapeAttributedString(attributedHint!)}'); | |
| } | |
| if (increasedValue != null) { | |
| description.add(' with increasedValue: ${_escape(increasedValue!)} '); | |
| } | |
| if (attributedIncreasedValue != null) { | |
| description.add( | |
| ' with attributedIncreasedValue: ${_escapeAttributedString(attributedIncreasedValue!)}', | |
| ); | |
| } | |
| if (decreasedValue != null) { | |
| description.add(' with decreasedValue: ${_escape(decreasedValue!)} '); | |
| } | |
| if (attributedDecreasedValue != null) { | |
| description.add( | |
| ' with attributedDecreasedValue: ${_escapeAttributedString(attributedDecreasedValue!)}', | |
| ); | |
| } | |
| if (tooltip != null) { | |
| description.add(' with tooltip: ${_escape(tooltip!)}'); | |
| } | |
| if (inputType != null) { | |
| description.add(' with inputType: $inputType'); | |
| } | |
| if (actions.isNotEmpty) { | |
| final List<SemanticsAction> expectedActions = actions.entries | |
| .where((MapEntry<ui.SemanticsAction, bool> e) => e.value) | |
| .map((MapEntry<ui.SemanticsAction, bool> e) => e.key) | |
| .toList(); | |
| final List<SemanticsAction> notExpectedActions = actions.entries | |
| .where((MapEntry<ui.SemanticsAction, bool> e) => !e.value) | |
| .map((MapEntry<ui.SemanticsAction, bool> e) => e.key) | |
| .toList(); | |
| if (expectedActions.isNotEmpty) { | |
| description.add(' with actions: ${_createSemanticsActionSummary(expectedActions)} '); | |
| } | |
| if (notExpectedActions.isNotEmpty) { | |
| description.add(' without actions: ${_createSemanticsActionSummary(notExpectedActions)} '); | |
| } | |
| } | |
| if (flags.isNotEmpty) { | |
| final List<String> expectedFlags = flags.entries | |
| .where((MapEntry<String, bool> e) => e.value) | |
| .map((MapEntry<String, bool> e) => e.key) | |
| .toList(); | |
| final List<String> notExpectedFlags = flags.entries | |
| .where((MapEntry<String, bool> e) => !e.value) | |
| .map((MapEntry<String, bool> e) => e.key) | |
| .toList(); | |
| if (expectedFlags.isNotEmpty) { | |
| description.add(' with flags: ${expectedFlags.join(',')} '); | |
| } | |
| if (notExpectedFlags.isNotEmpty) { | |
| description.add(' without flags: ${notExpectedFlags.join(', ')} '); | |
| } | |
| } | |
| 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 (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 (validationResult != null) { | |
| description.add(' with validation result: $validationResult'); | |
| } | |
| if (minValue != null) { | |
| description.add(' with minValue: $minValue'); | |
| } | |
| if (maxValue != null) { | |
| description.add(' with maxValue: $maxValue'); | |
| } | |
| if (role != null) { | |
| description.add(' with role: $role'); | |
| } | |
| if (children != null) { | |
| description.add(' with children:\n '); | |
| final List<_MatchesSemanticsData> childMatches = children!.cast<_MatchesSemanticsData>(); | |
| var childIndex = 1; | |
| for (final child in childMatches) { | |
| child.describe(description, index != null ? '$index:$childIndex' : '$childIndex'); | |
| if (child != childMatches.last) { | |
| description.add('\n '); | |
| } | |
| childIndex += 1; | |
| } | |
| } | |
| return description; | |
| } | |
| bool _stringAttributesEqual(List<StringAttribute> first, List<StringAttribute> second) { | |
| if (first.length != second.length) { | |
| return false; | |
| } | |
| for (var i = 0; i < first.length; i++) { | |
| if (first[i] is SpellOutStringAttribute && | |
| (second[i] is! SpellOutStringAttribute || second[i].range != first[i].range)) { | |
| return false; | |
| } | |
| if (first[i] is LocaleStringAttribute && | |
| (second[i] is! LocaleStringAttribute || | |
| second[i].range != first[i].range || | |
| (second[i] as LocaleStringAttribute).locale != | |
| (second[i] as LocaleStringAttribute).locale)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| static String _escape(String string) => string.replaceAll('\u202f', r'\u202f'); | |
| static AttributedString _escapeAttributedString(AttributedString attributedString) { | |
| return AttributedString( | |
| _escape(attributedString.string), | |
| attributes: attributedString.attributes, | |
| ); | |
| } | |
| bool _checkStringMismatch(Map<dynamic, dynamic> matchState, String prefixText, String actual) { | |
| return failWithDescription(matchState, '$prefixText was ${_escape(actual)}'); | |
| } | |
| bool _checkStringAttributeMismatch( | |
| Map<dynamic, dynamic> matchState, | |
| String prefixText, | |
| AttributedString actual, | |
| ) { | |
| return failWithDescription(matchState, '$prefixText was: ${_escapeAttributedString(actual)}'); | |
| } | |
| @override | |
| bool matches(dynamic node, Map<dynamic, dynamic> matchState) { | |
| if (node == null) { | |
| return failWithDescription( | |
| matchState, | |
| 'No SemanticsData provided. ' | |
| 'Maybe you forgot to enable semantics?', | |
| ); | |
| } | |
| final SemanticsData data = switch (node) { | |
| SemanticsNode() => node.getSemanticsData(), | |
| FinderBase<SemanticsNode>() => node.evaluate().single.getSemanticsData(), | |
| _ => node as SemanticsData, | |
| }; | |
| if (label != null && label != data.label) { | |
| return _checkStringMismatch(matchState, 'label', data.label); | |
| } | |
| if (attributedLabel != null && | |
| (attributedLabel!.string != data.attributedLabel.string || | |
| !_stringAttributesEqual( | |
| attributedLabel!.attributes, | |
| data.attributedLabel.attributes, | |
| ))) { | |
| return _checkStringAttributeMismatch(matchState, 'attributedLabel', data.attributedLabel); | |
| } | |
| if (hint != null && hint != data.hint) { | |
| return _checkStringMismatch(matchState, 'hint', data.hint); | |
| } | |
| if (attributedHint != null && | |
| (attributedHint!.string != data.attributedHint.string || | |
| !_stringAttributesEqual(attributedHint!.attributes, data.attributedHint.attributes))) { | |
| return _checkStringAttributeMismatch(matchState, 'attributedHint', data.attributedHint); | |
| } | |
| if (value != null && value != data.value) { | |
| return _checkStringMismatch(matchState, 'value', data.value); | |
| } | |
| if (attributedValue != null && | |
| (attributedValue!.string != data.attributedValue.string || | |
| !_stringAttributesEqual( | |
| attributedValue!.attributes, | |
| data.attributedValue.attributes, | |
| ))) { | |
| return _checkStringAttributeMismatch(matchState, 'attributedValue', data.attributedValue); | |
| } | |
| if (increasedValue != null && increasedValue != data.increasedValue) { | |
| return _checkStringMismatch(matchState, 'increasedValue', data.increasedValue); | |
| } | |
| if (attributedIncreasedValue != null && | |
| (attributedIncreasedValue!.string != data.attributedIncreasedValue.string || | |
| !_stringAttributesEqual( | |
| attributedIncreasedValue!.attributes, | |
| data.attributedIncreasedValue.attributes, | |
| ))) { | |
| return _checkStringAttributeMismatch( | |
| matchState, | |
| 'attributedIncreasedValue', | |
| data.attributedIncreasedValue, | |
| ); | |
| } | |
| if (decreasedValue != null && decreasedValue != data.decreasedValue) { | |
| return _checkStringMismatch(matchState, 'decreasedValue', data.decreasedValue); | |
| } | |
| if (attributedDecreasedValue != null && | |
| (attributedDecreasedValue!.string != data.attributedDecreasedValue.string || | |
| !_stringAttributesEqual( | |
| attributedDecreasedValue!.attributes, | |
| data.attributedDecreasedValue.attributes, | |
| ))) { | |
| return _checkStringAttributeMismatch( | |
| matchState, | |
| 'attributedDecreasedValue', | |
| data.attributedDecreasedValue, | |
| ); | |
| } | |
| if (tooltip != null && tooltip != data.tooltip) { | |
| return _checkStringMismatch(matchState, 'tooltip', data.tooltip); | |
| } | |
| 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 (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 (validationResult != null && validationResult != data.validationResult) { | |
| return failWithDescription(matchState, 'validationResult was: ${data.validationResult}'); | |
| } | |
| if (inputType != null && inputType != data.inputType) { | |
| return failWithDescription(matchState, 'inputType was: ${data.inputType}'); | |
| } | |
| if (minValue != null && minValue != data.minValue) { | |
| return failWithDescription(matchState, 'minValue was: ${data.minValue}'); | |
| } | |
| if (maxValue != null && maxValue != data.maxValue) { | |
| return failWithDescription(matchState, 'maxValue was: ${data.maxValue}'); | |
| } | |
| if (role != null && role != data.role) { | |
| return failWithDescription(matchState, 'role was: ${data.role}'); | |
| } | |
| if (actions.isNotEmpty) { | |
| final unexpectedActions = <SemanticsAction>[]; | |
| final missingActions = <SemanticsAction>[]; | |
| for (final MapEntry<ui.SemanticsAction, bool> actionEntry in actions.entries) { | |
| final ui.SemanticsAction action = actionEntry.key; | |
| final bool actionExpected = actionEntry.value; | |
| final actionPresent = (action.index & data.actions) == action.index; | |
| if (actionPresent != actionExpected) { | |
| if (actionExpected) { | |
| missingActions.add(action); | |
| } else { | |
| unexpectedActions.add(action); | |
| } | |
| } | |
| } | |
| if (unexpectedActions.isNotEmpty || missingActions.isNotEmpty) { | |
| return failWithDescription( | |
| matchState, | |
| 'missing actions: ${_createSemanticsActionSummary(missingActions)} unexpected actions: ${_createSemanticsActionSummary(unexpectedActions)}', | |
| ); | |
| } | |
| } | |
| if (customActions != null || hintOverrides != null) { | |
| final List<CustomSemanticsAction> providedCustomActions = | |
| data.customSemanticsActionIds?.map<CustomSemanticsAction>((int id) { | |
| return CustomSemanticsAction.getAction(id)!; | |
| }).toList() ?? | |
| <CustomSemanticsAction>[]; | |
| 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 were: $providedCustomActions'); | |
| } | |
| int sortActions(CustomSemanticsAction left, CustomSemanticsAction right) { | |
| return CustomSemanticsAction.getIdentifier(left) - | |
| CustomSemanticsAction.getIdentifier(right); | |
| } | |
| expectedCustomActions.sort(sortActions); | |
| providedCustomActions.sort(sortActions); | |
| for (var i = 0; i < expectedCustomActions.length; i++) { | |
| if (expectedCustomActions[i] != providedCustomActions[i]) { | |
| return failWithDescription(matchState, 'custom actions were: $providedCustomActions'); | |
| } | |
| } | |
| } | |
| if (flags.isNotEmpty) { | |
| final Map<String, bool> foundFlags = _stringsMapFromSemanticsFlags(data.flagsCollection); | |
| final unexpectedFlags = <String>[]; | |
| final missingFlags = <String>[]; | |
| for (final MapEntry<String, bool> flagEntry in flags.entries) { | |
| final String flagName = flagEntry.key; | |
| final bool flagExpected = flagEntry.value; | |
| final bool flagPresent = foundFlags[flagName]!; | |
| if (flagPresent != flagExpected) { | |
| if (flagExpected) { | |
| missingFlags.add(flagName); | |
| } else { | |
| unexpectedFlags.add(flagName); | |
| } | |
| } | |
| } | |
| if (unexpectedFlags.isNotEmpty || missingFlags.isNotEmpty) { | |
| return failWithDescription( | |
| matchState, | |
| 'missing flags: ${missingFlags.join(',')} unexpected flags: ${unexpectedFlags.join(',')}', | |
| ); | |
| } | |
| } | |
| var allMatched = true; | |
| if (children != null) { | |
| final int actualChildrenCount = (node as SemanticsNode).childrenCount; | |
| if (children!.length != actualChildrenCount) { | |
| return failWithDescription( | |
| matchState, | |
| 'expected ${children!.length} child${children!.length == 1 ? '' : 'ren'}, found $actualChildrenCount', | |
| ); | |
| } | |
| var 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); | |
| } | |
| static String _createSemanticsActionSummary(List<SemanticsAction> enums) { | |
| return '[${enums.map((ui.SemanticsAction d) => d.name).join(', ')}]'; | |
| } | |
| } | |
| class _MatchesAccessibilityAnnouncement extends Matcher { | |
| const _MatchesAccessibilityAnnouncement({ | |
| required this.expectedMessage, | |
| required this.expectedTextDirection, | |
| required this.expectedAssertiveness, | |
| required this.expectedViewId, | |
| }); | |
| final String expectedMessage; | |
| final TextDirection? expectedTextDirection; | |
| final Assertiveness? expectedAssertiveness; | |
| final int? expectedViewId; | |
| @override | |
| bool matches( | |
| covariant CapturedAccessibilityAnnouncement event, | |
| Map<dynamic, dynamic> matchState, | |
| ) { | |
| return event.message == expectedMessage && | |
| (expectedTextDirection == null || event.textDirection == expectedTextDirection) && | |
| (expectedAssertiveness == null || event.assertiveness == expectedAssertiveness) && | |
| (expectedViewId == null || event.viewId == expectedViewId); | |
| } | |
| @override | |
| Description describe(Description description) { | |
| description.add('Semantic announcement with message "$expectedMessage"'); | |
| if (expectedTextDirection != null) { | |
| description.add(', textDirection: $expectedTextDirection'); | |
| } | |
| if (expectedAssertiveness != null) { | |
| description.add(', assertiveness: $expectedAssertiveness'); | |
| } | |
| if (expectedViewId != null) { | |
| description.add(', viewId: $expectedViewId'); | |
| } | |
| return description; | |
| } | |
| } | |
| 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; | |
| } | |
| } | |
| Map<String, bool> _stringsMapFromSemanticsFlags(SemanticsFlags flagsCollection) { | |
| return <String, bool>{ | |
| 'hasCheckedState': flagsCollection.isChecked != ui.CheckedState.none, | |
| 'isChecked': flagsCollection.isChecked == ui.CheckedState.isTrue, | |
| 'isCheckStateMixed': flagsCollection.isChecked == ui.CheckedState.mixed, | |
| 'isSelected': flagsCollection.isSelected == ui.Tristate.isTrue, | |
| 'hasSelectedState': flagsCollection.isSelected != ui.Tristate.none, | |
| 'isButton': flagsCollection.isButton, | |
| 'isSlider': flagsCollection.isSlider, | |
| 'isKeyboardKey': flagsCollection.isKeyboardKey, | |
| 'isLink': flagsCollection.isLink, | |
| 'isFocused': flagsCollection.isFocused == ui.Tristate.isTrue, | |
| 'isFocusable': flagsCollection.isFocused != ui.Tristate.none, | |
| 'isTextField': flagsCollection.isTextField, | |
| 'isReadOnly': flagsCollection.isReadOnly, | |
| 'hasEnabledState': flagsCollection.isEnabled != ui.Tristate.none, | |
| 'isEnabled': flagsCollection.isEnabled == ui.Tristate.isTrue, | |
| 'isInMutuallyExclusiveGroup': flagsCollection.isInMutuallyExclusiveGroup, | |
| 'isHeader': flagsCollection.isHeader, | |
| 'isObscured': flagsCollection.isObscured, | |
| 'isMultiline': flagsCollection.isMultiline, | |
| 'namesRoute': flagsCollection.namesRoute, | |
| 'scopesRoute': flagsCollection.scopesRoute, | |
| 'isHidden': flagsCollection.isHidden, | |
| 'isImage': flagsCollection.isImage, | |
| 'isLiveRegion': flagsCollection.isLiveRegion, | |
| 'hasToggledState': flagsCollection.isToggled != ui.Tristate.none, | |
| 'isToggled': flagsCollection.isToggled == ui.Tristate.isTrue, | |
| 'hasImplicitScrolling': flagsCollection.hasImplicitScrolling, | |
| 'hasExpandedState': flagsCollection.isExpanded != ui.Tristate.none, | |
| 'isExpanded': flagsCollection.isExpanded == ui.Tristate.isTrue, | |
| 'hasRequiredState': flagsCollection.isRequired != ui.Tristate.none, | |
| 'isRequired': flagsCollection.isRequired == ui.Tristate.isTrue, | |
| }; | |
| } |