blob: 4fcd23767267ba0522de79dffcc9595c9aa414ab [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart: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:test_api/src/expect/async_matcher.dart'; // ignore: implementation_imports
// This import is discouraged in general, but we need it to implement flutter_test.
// ignore: deprecated_member_use
import 'package:test_api/test_api.dart';
import '_matchers_io.dart' if (dart.library.html) '_matchers_web.dart' show MatchesGoldenFile, captureImage;
import 'accessibility.dart';
import 'binding.dart';
import 'finders.dart';
import 'goldens.dart';
import 'widget_tester.dart' show WidgetTester;
/// Asserts that the [Finder] matches no widgets in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsNothing);
/// ```
///
/// See also:
///
/// * [findsWidgets], when you want the finder to find one or more widgets.
/// * [findsOneWidget], when you want the finder to find exactly one widget.
/// * [findsNWidgets], when you want the finder to find a specific number of widgets.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
const Matcher findsNothing = _FindsWidgetMatcher(null, 0);
/// Asserts that the [Finder] locates at least one widget in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsWidgets);
/// ```
///
/// See also:
///
/// * [findsNothing], when you want the finder to not find anything.
/// * [findsOneWidget], when you want the finder to find exactly one widget.
/// * [findsNWidgets], when you want the finder to find a specific number of widgets.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
const Matcher findsWidgets = _FindsWidgetMatcher(1, null);
/// Asserts that the [Finder] locates at exactly one widget in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsOneWidget);
/// ```
///
/// See also:
///
/// * [findsNothing], when you want the finder to not find anything.
/// * [findsWidgets], when you want the finder to find one or more widgets.
/// * [findsNWidgets], when you want the finder to find a specific number of widgets.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
const Matcher findsOneWidget = _FindsWidgetMatcher(1, 1);
/// Asserts that the [Finder] locates the specified number of widgets in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsNWidgets(2));
/// ```
///
/// See also:
///
/// * [findsNothing], when you want the finder to not find anything.
/// * [findsWidgets], when you want the finder to find one or more widgets.
/// * [findsOneWidget], when you want the finder to find exactly one widget.
/// * [findsAtLeastNWidgets], when you want the finder to find at least a specific number of widgets.
Matcher findsNWidgets(int n) => _FindsWidgetMatcher(n, n);
/// Asserts that the [Finder] locates at least a number of widgets in the widget tree.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save'), findsAtLeastNWidgets(2));
/// ```
///
/// See also:
///
/// * [findsNothing], when you want the finder to not find anything.
/// * [findsWidgets], when you want the finder to find one or more widgets.
/// * [findsOneWidget], when you want the finder to find exactly one widget.
/// * [findsNWidgets], when you want the finder to find a specific number of widgets.
Matcher findsAtLeastNWidgets(int n) => _FindsWidgetMatcher(n, null);
/// Asserts that the [Finder] locates a single widget that has at
/// least one [Offstage] widget ancestor.
///
/// It's important to use a full finder, since by default finders exclude
/// offstage widgets.
///
/// ## Sample code
///
/// ```dart
/// expect(find.text('Save', skipOffstage: false), isOffstage);
/// ```
///
/// See also:
///
/// * [isOnstage], the opposite.
const Matcher isOffstage = _IsOffstage();
/// Asserts that the [Finder] locates a single widget that has no
/// [Offstage] widget ancestors.
///
/// See also:
///
/// * [isOffstage], the opposite.
const Matcher isOnstage = _IsOnstage();
/// Asserts that the [Finder] locates a single widget that has at
/// least one [Card] widget ancestor.
///
/// See also:
///
/// * [isNotInCard], the opposite.
const Matcher isInCard = _IsInCard();
/// Asserts that the [Finder] locates a single widget that has no
/// [Card] widget ancestors.
///
/// This is equivalent to `isNot(isInCard)`.
///
/// See also:
///
/// * [isInCard], the opposite.
const Matcher isNotInCard = _IsNotInCard();
/// Asserts that the object represents the same color as [color] when used to paint.
///
/// Specifically this matcher checks the object is of type [Color] and its [Color.value]
/// equals to that of the given [color].
Matcher isSameColorAs(Color color) => _ColorMatcher(targetColor: color);
/// Asserts that an object's toString() is a plausible one-line description.
///
/// Specifically, this matcher checks that the string does not contains newline
/// characters, and does not have leading or trailing whitespace, is not
/// empty, and does not contain the default `Instance of ...` string.
const Matcher hasOneLineDescription = _HasOneLineDescription();
/// Asserts that an object's toStringDeep() is a plausible multiline
/// description.
///
/// Specifically, this matcher checks that an object's
/// `toStringDeep(prefixLineOne, prefixOtherLines)`:
///
/// * Does not have leading or trailing whitespace.
/// * Does not contain the default `Instance of ...` string.
/// * The last line has characters other than tree connector characters and
/// whitespace. For example: the line ` │ ║ ╎` has only tree connector
/// characters and whitespace.
/// * Does not contain lines with trailing white space.
/// * Has multiple lines.
/// * The first line starts with `prefixLineOne`
/// * All subsequent lines start with `prefixOtherLines`.
const Matcher hasAGoodToStringDeep = _HasGoodToStringDeep();
/// A matcher for functions that throw [FlutterError].
///
/// This is equivalent to `throwsA(isA<FlutterError>())`.
///
/// If you are trying to test whether a call to [WidgetTester.pumpWidget]
/// results in a [FlutterError], see [TestWidgetsFlutterBinding.takeException].
///
/// See also:
///
/// * [throwsAssertionError], to test if a function throws any [AssertionError].
/// * [throwsArgumentError], to test if a functions throws an [ArgumentError].
/// * [isFlutterError], to test if any object is a [FlutterError].
final Matcher throwsFlutterError = throwsA(isFlutterError);
/// A matcher for functions that throw [AssertionError].
///
/// This is equivalent to `throwsA(isA<AssertionError>())`.
///
/// If you are trying to test whether a call to [WidgetTester.pumpWidget]
/// results in an [AssertionError], see
/// [TestWidgetsFlutterBinding.takeException].
///
/// See also:
///
/// * [throwsFlutterError], to test if a function throws a [FlutterError].
/// * [throwsArgumentError], to test if a functions throws an [ArgumentError].
/// * [isAssertionError], to test if any object is any kind of [AssertionError].
final Matcher throwsAssertionError = throwsA(isAssertionError);
/// A matcher for [FlutterError].
///
/// This is equivalent to `isInstanceOf<FlutterError>()`.
///
/// See also:
///
/// * [throwsFlutterError], to test if a function throws a [FlutterError].
/// * [isAssertionError], to test if any object is any kind of [AssertionError].
final 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.
Matcher matrixMoreOrLessEquals(Matrix4 value, { double epsilon = precisionErrorTolerance }) {
return _IsWithinDistance<Matrix4>(_matrixDistance, 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);
/// 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', (tester) async {
/// // Assuming the 'Roboto.ttf' file is declared in the pubspec.yaml file
/// final font = rootBundle.load('path/to/font-file/Roboto.ttf');
///
/// final fontLoader = FontLoader('Roboto')..addFont(font);
/// await fontLoader.load();
///
/// await tester.pumpWidget(const SomeWidget());
///
/// await expectLater(
/// find.byType(SomeWidget),
/// matchesGoldenFile('someWidget.png'),
/// );
/// });
/// ```
/// {@end-tool}
///
/// The example above loads the desired font only for that specific test. To load
/// a font for all golden file tests, the `FontLoader.load()` call could be
/// moved in the `flutter_test_config.dart`. In this way, the font will always be
/// loaded before a test:
///
/// {@tool snippet}
/// Loading a custom font from the flutter_test_config.dart file.
/// ```dart
/// Future<void> testExecutable(FutureOr<void> Function() testMain) async {
/// setUpAll(() async {
/// final fontLoader = FontLoader('SomeFont')..addFont(someFont);
/// await fontLoader.load();
/// });
///
/// await testMain();
/// });
/// ```
/// {@end-tool}
/// {@endtemplate}
///
/// 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}) {
if (key is Uri) {
return MatchesGoldenFile(key, version);
} else if (key is String) {
return MatchesGoldenFile.forStringPath(key, version);
}
throw ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
}
/// Asserts that a [Finder], [Future<ui.Image>], or [ui.Image] matches a
/// reference image identified by [image].
///
/// For the case of a [Finder], the [Finder] must match exactly one widget and
/// the rendered image of the first [RepaintBoundary] ancestor of the widget is
/// treated as the image for the widget.
///
/// This is an asynchronous matcher, meaning that callers should use
/// [expectLater] when using this matcher and await the future returned by
/// [expectLater].
///
/// ## Sample code
///
/// ```dart
/// final ui.Paint paint = ui.Paint()
/// ..style = ui.PaintingStyle.stroke
/// ..strokeWidth = 1.0;
/// final ui.PictureRecorder recorder = ui.PictureRecorder();
/// final ui.Canvas pictureCanvas = ui.Canvas(recorder);
/// pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
/// final ui.Picture picture = recorder.endRecording();
/// ui.Image referenceImage = picture.toImage(50, 50);
///
/// await expectLater(find.text('Save'), matchesReferenceImage(referenceImage));
/// await expectLater(image, matchesReferenceImage(referenceImage);
/// await expectLater(imageFuture, matchesReferenceImage(referenceImage));
/// ```
///
/// See also:
///
/// * [matchesGoldenFile], which should be used instead if you need to verify
/// that a [Finder] or [ui.Image] matches a golden image.
AsyncMatcher matchesReferenceImage(ui.Image image) {
return _MatchesReferenceImage(image);
}
/// Asserts that a [SemanticsNode] contains the specified information.
///
/// If either the label, hint, value, textDirection, or rect fields are not
/// provided, then they are not part of the comparison. All of the boolean
/// flag and action fields must match, and default to false.
///
/// To retrieve the semantics data of a widget, use [WidgetTester.getSemantics]
/// with a [Finder] that returns a single widget. Semantics must be enabled
/// in order to use this method.
///
/// ## Sample code
///
/// ```dart
/// final SemanticsHandle handle = tester.ensureSemantics();
/// expect(tester.getSemantics(find.text('hello')), matchesSemantics(label: 'hello'));
/// handle.dispose();
/// ```
///
/// See also:
///
/// * [WidgetTester.getSemantics], the tester method which retrieves semantics.
/// * [containsSemantics], a similar matcher without default values for flags or actions.
Matcher matchesSemantics({
String? label,
AttributedString? attributedLabel,
String? hint,
AttributedString? attributedHint,
String? value,
AttributedString? attributedValue,
String? increasedValue,
AttributedString? attributedIncreasedValue,
String? decreasedValue,
AttributedString? attributedDecreasedValue,
String? tooltip,
TextDirection? textDirection,
Rect? rect,
Size? size,
double? elevation,
double? thickness,
int? platformViewId,
int? maxValueLength,
int? currentValueLength,
// Flags //
bool hasCheckedState = false,
bool isChecked = false,
bool isCheckStateMixed = false,
bool isSelected = 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,
// Actions //
bool hasTapAction = false,
bool hasLongPressAction = false,
bool hasScrollLeftAction = false,
bool hasScrollRightAction = false,
bool hasScrollUpAction = false,
bool hasScrollDownAction = false,
bool hasIncreaseAction = false,
bool hasDecreaseAction = false,
bool hasShowOnScreenAction = false,
bool hasMoveCursorForwardByCharacterAction = false,
bool hasMoveCursorBackwardByCharacterAction = false,
bool hasMoveCursorForwardByWordAction = false,
bool hasMoveCursorBackwardByWordAction = false,
bool 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(
label: label,
attributedLabel: attributedLabel,
hint: hint,
attributedHint: attributedHint,
value: value,
attributedValue: attributedValue,
increasedValue: increasedValue,
attributedIncreasedValue: attributedIncreasedValue,
decreasedValue: decreasedValue,
attributedDecreasedValue: attributedDecreasedValue,
tooltip: tooltip,
textDirection: textDirection,
rect: rect,
size: size,
elevation: elevation,
thickness: thickness,
platformViewId: platformViewId,
customActions: customActions,
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
// Flags
hasCheckedState: hasCheckedState,
isChecked: isChecked,
isCheckStateMixed: isCheckStateMixed,
isSelected: isSelected,
isButton: isButton,
isSlider: isSlider,
isKeyboardKey: isKeyboardKey,
isLink: isLink,
isFocused: isFocused,
isFocusable: isFocusable,
isTextField: isTextField,
isReadOnly: isReadOnly,
hasEnabledState: hasEnabledState,
isEnabled: isEnabled,
isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup,
isHeader: isHeader,
isObscured: isObscured,
isMultiline: isMultiline,
namesRoute: namesRoute,
scopesRoute: scopesRoute,
isHidden: isHidden,
isImage: isImage,
isLiveRegion: isLiveRegion,
hasToggledState: hasToggledState,
isToggled: isToggled,
hasImplicitScrolling: hasImplicitScrolling,
// Actions
hasTapAction: hasTapAction,
hasLongPressAction: hasLongPressAction,
hasScrollLeftAction: hasScrollLeftAction,
hasScrollRightAction: hasScrollRightAction,
hasScrollUpAction: hasScrollUpAction,
hasScrollDownAction: hasScrollDownAction,
hasIncreaseAction: hasIncreaseAction,
hasDecreaseAction: hasDecreaseAction,
hasShowOnScreenAction: hasShowOnScreenAction,
hasMoveCursorForwardByCharacterAction: hasMoveCursorForwardByCharacterAction,
hasMoveCursorBackwardByCharacterAction: hasMoveCursorBackwardByCharacterAction,
hasMoveCursorForwardByWordAction: hasMoveCursorForwardByWordAction,
hasMoveCursorBackwardByWordAction: hasMoveCursorBackwardByWordAction,
hasSetTextAction: hasSetTextAction,
hasSetSelectionAction: hasSetSelectionAction,
hasCopyAction: hasCopyAction,
hasCutAction: hasCutAction,
hasPasteAction: hasPasteAction,
hasDidGainAccessibilityFocusAction: hasDidGainAccessibilityFocusAction,
hasDidLoseAccessibilityFocusAction: hasDidLoseAccessibilityFocusAction,
hasDismissAction: hasDismissAction,
// Custom actions and overrides
children: children,
onLongPressHint: onLongPressHint,
onTapHint: onTapHint,
);
}
/// Asserts that a [SemanticsNode] contains the specified information.
///
/// There are no default expected values, so no unspecified values will be
/// validated.
///
/// To retrieve the semantics data of a widget, use [WidgetTester.getSemantics]
/// with a [Finder] that returns a single widget. Semantics must be enabled
/// in order to use this method.
///
/// ## Sample code
///
/// ```dart
/// final SemanticsHandle handle = tester.ensureSemantics();
/// expect(tester.getSemantics(find.text('hello')), hasSemantics(label: 'hello'));
/// handle.dispose();
/// ```
///
/// See also:
///
/// * [WidgetTester.getSemantics], the tester method which retrieves semantics.
/// * [matchesSemantics], a similar matcher with default values for flags and actions.
Matcher containsSemantics({
String? label,
AttributedString? attributedLabel,
String? hint,
AttributedString? attributedHint,
String? value,
AttributedString? attributedValue,
String? increasedValue,
AttributedString? attributedIncreasedValue,
String? decreasedValue,
AttributedString? attributedDecreasedValue,
String? tooltip,
TextDirection? textDirection,
Rect? rect,
Size? size,
double? elevation,
double? thickness,
int? platformViewId,
int? maxValueLength,
int? currentValueLength,
// Flags
bool? hasCheckedState,
bool? isChecked,
bool? isCheckStateMixed,
bool? isSelected,
bool? isButton,
bool? isSlider,
bool? isKeyboardKey,
bool? isLink,
bool? isFocused,
bool? isFocusable,
bool? isTextField,
bool? isReadOnly,
bool? hasEnabledState,
bool? isEnabled,
bool? isInMutuallyExclusiveGroup,
bool? isHeader,
bool? isObscured,
bool? isMultiline,
bool? namesRoute,
bool? scopesRoute,
bool? isHidden,
bool? isImage,
bool? isLiveRegion,
bool? hasToggledState,
bool? isToggled,
bool? hasImplicitScrolling,
// Actions
bool? hasTapAction,
bool? hasLongPressAction,
bool? hasScrollLeftAction,
bool? hasScrollRightAction,
bool? hasScrollUpAction,
bool? hasScrollDownAction,
bool? hasIncreaseAction,
bool? hasDecreaseAction,
bool? hasShowOnScreenAction,
bool? hasMoveCursorForwardByCharacterAction,
bool? hasMoveCursorBackwardByCharacterAction,
bool? hasMoveCursorForwardByWordAction,
bool? hasMoveCursorBackwardByWordAction,
bool? hasSetTextAction,
bool? hasSetSelectionAction,
bool? hasCopyAction,
bool? hasCutAction,
bool? hasPasteAction,
bool? hasDidGainAccessibilityFocusAction,
bool? hasDidLoseAccessibilityFocusAction,
bool? hasDismissAction,
// Custom actions and overrides
String? onTapHint,
String? onLongPressHint,
List<CustomSemanticsAction>? customActions,
List<Matcher>? children,
}) {
return _MatchesSemanticsData(
label: label,
attributedLabel: attributedLabel,
hint: hint,
attributedHint: attributedHint,
value: value,
attributedValue: attributedValue,
increasedValue: increasedValue,
attributedIncreasedValue: attributedIncreasedValue,
decreasedValue: decreasedValue,
attributedDecreasedValue: attributedDecreasedValue,
tooltip: tooltip,
textDirection: textDirection,
rect: rect,
size: size,
elevation: elevation,
thickness: thickness,
platformViewId: platformViewId,
customActions: customActions,
maxValueLength: maxValueLength,
currentValueLength: currentValueLength,
// Flags
hasCheckedState: hasCheckedState,
isChecked: isChecked,
isCheckStateMixed: isCheckStateMixed,
isSelected: isSelected,
isButton: isButton,
isSlider: isSlider,
isKeyboardKey: isKeyboardKey,
isLink: isLink,
isFocused: isFocused,
isFocusable: isFocusable,
isTextField: isTextField,
isReadOnly: isReadOnly,
hasEnabledState: hasEnabledState,
isEnabled: isEnabled,
isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup,
isHeader: isHeader,
isObscured: isObscured,
isMultiline: isMultiline,
namesRoute: namesRoute,
scopesRoute: scopesRoute,
isHidden: isHidden,
isImage: isImage,
isLiveRegion: isLiveRegion,
hasToggledState: hasToggledState,
isToggled: isToggled,
hasImplicitScrolling: hasImplicitScrolling,
// Actions
hasTapAction: hasTapAction,
hasLongPressAction: hasLongPressAction,
hasScrollLeftAction: hasScrollLeftAction,
hasScrollRightAction: hasScrollRightAction,
hasScrollUpAction: hasScrollUpAction,
hasScrollDownAction: hasScrollDownAction,
hasIncreaseAction: hasIncreaseAction,
hasDecreaseAction: hasDecreaseAction,
hasShowOnScreenAction: hasShowOnScreenAction,
hasMoveCursorForwardByCharacterAction: hasMoveCursorForwardByCharacterAction,
hasMoveCursorBackwardByCharacterAction: hasMoveCursorBackwardByCharacterAction,
hasMoveCursorForwardByWordAction: hasMoveCursorForwardByWordAction,
hasMoveCursorBackwardByWordAction: hasMoveCursorBackwardByWordAction,
hasSetTextAction: hasSetTextAction,
hasSetSelectionAction: hasSetSelectionAction,
hasCopyAction: hasCopyAction,
hasCutAction: hasCutAction,
hasPasteAction: hasPasteAction,
hasDidGainAccessibilityFocusAction: hasDidGainAccessibilityFocusAction,
hasDidLoseAccessibilityFocusAction: hasDidLoseAccessibilityFocusAction,
hasDismissAction: hasDismissAction,
// Custom actions and overrides
children: children,
onLongPressHint: onLongPressHint,
onTapHint: onTapHint,
);
}
/// Asserts that the currently rendered widget meets the provided accessibility
/// `guideline`.
///
/// This matcher requires the result to be awaited and for semantics to be
/// enabled first.
///
/// ## Sample code
///
/// ```dart
/// final SemanticsHandle handle = tester.ensureSemantics();
/// await expectLater(tester, meetsGuideline(textContrastGuideline));
/// handle.dispose();
/// ```
///
/// Supported accessibility guidelines:
///
/// * [androidTapTargetGuideline], for Android minimum tappable area guidelines.
/// * [iOSTapTargetGuideline], for iOS minimum tappable area guidelines.
/// * [textContrastGuideline], for WCAG minimum text contrast guidelines.
/// * [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 _FindsWidgetMatcher extends Matcher {
const _FindsWidgetMatcher(this.min, this.max);
final int? min;
final int? max;
@override
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
assert(min != null || max != null);
assert(min == null || max == null || min! <= max!);
matchState[Finder] = finder;
int count = 0;
final Iterator<Element> iterator = finder.evaluate().iterator;
if (min != null) {
while (count < min! && iterator.moveNext()) {
count += 1;
}
if (count < min!) {
return false;
}
}
if (max != null) {
while (count <= max! && iterator.moveNext()) {
count += 1;
}
if (count > max!) {
return false;
}
}
return true;
}
@override
Description describe(Description description) {
assert(min != null || max != null);
if (min == max) {
if (min == 1) {
return description.add('exactly one matching node in the widget tree');
}
return description.add('exactly $min matching nodes in the widget tree');
}
if (min == null) {
if (max == 0) {
return description.add('no matching nodes in the widget tree');
}
if (max == 1) {
return description.add('at most one matching node in the widget tree');
}
return description.add('at most $max matching nodes in the widget tree');
}
if (max == null) {
if (min == 1) {
return description.add('at least one matching node in the widget tree');
}
return description.add('at least $min matching nodes in the widget tree');
}
return description.add('between $min and $max matching nodes in the widget tree (inclusive)');
}
@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
final Finder finder = matchState[Finder] as Finder;
final int count = finder.evaluate().length;
if (count == 0) {
assert(min != null && min! > 0);
if (min == 1 && max == 1) {
return mismatchDescription.add('means none were found but one was expected');
}
return mismatchDescription.add('means none were found but some were expected');
}
if (max == 0) {
if (count == 1) {
return mismatchDescription.add('means one was found but none were expected');
}
return mismatchDescription.add('means some were found but none were expected');
}
if (min != null && count < min!) {
return mismatchDescription.add('is not enough');
}
assert(max != null && count > min!);
return mismatchDescription.add('is too many');
}
}
bool _hasAncestorMatching(Finder finder, bool Function(Widget widget) predicate) {
final Iterable<Element> nodes = finder.evaluate();
if (nodes.length != 1) {
return false;
}
bool result = false;
nodes.single.visitAncestorElements((Element ancestor) {
if (predicate(ancestor.widget)) {
result = true;
return false;
}
return true;
});
return result;
}
bool _hasAncestorOfType(Finder finder, Type targetType) {
return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType);
}
class _IsOffstage extends Matcher {
const _IsOffstage();
@override
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
return _hasAncestorMatching(finder, (Widget widget) {
if (widget is Offstage) {
return widget.offstage;
}
return false;
});
}
@override
Description describe(Description description) => description.add('offstage');
}
class _IsOnstage extends Matcher {
const _IsOnstage();
@override
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
final Iterable<Element> nodes = finder.evaluate();
if (nodes.length != 1) {
return false;
}
bool result = true;
nodes.single.visitAncestorElements((Element ancestor) {
final Widget widget = ancestor.widget;
if (widget is Offstage) {
result = !widget.offstage;
return false;
}
return true;
});
return result;
}
@override
Description describe(Description description) => description.add('onstage');
}
class _IsInCard extends Matcher {
const _IsInCard();
@override
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => _hasAncestorOfType(finder, Card);
@override
Description describe(Description description) => description.add('in card');
}
class _IsNotInCard extends Matcher {
const _IsNotInCard();
@override
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => !_hasAncestorOfType(finder, Card);
@override
Description describe(Description description) => description.add('not in card');
}
class _HasOneLineDescription extends Matcher {
const _HasOneLineDescription();
@override
bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
final String description = object.toString();
return description.isNotEmpty
&& !description.contains('\n')
&& !description.contains('Instance of ')
&& description.trim() == description;
}
@override
Description describe(Description description) => description.add('one line description');
}
class _EqualsIgnoringHashCodes extends Matcher {
_EqualsIgnoringHashCodes(Object v) : _value = _normalize(v);
final Object _value;
static final Object _mismatchedValueKey = Object();
static String _normalizeString(String value) {
return value.replaceAll(RegExp(r'#[\da-fA-F]{5}'), '#00000');
}
static Object _normalize(Object value, {bool expected = true}) {
if (value is String) {
return _normalizeString(value);
}
if (value is Iterable<String>) {
return value.map<String>((dynamic item) => _normalizeString(item.toString()));
}
throw ArgumentError('The specified ${expected ? 'expected' : 'comparison'} value for '
'equalsIgnoringHashCodes must be a String or an Iterable<String>, '
'not a ${value.runtimeType}');
}
@override
bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
final Object normalized = _normalize(object as Object, expected: false);
if (!equals(_value).matches(normalized, matchState)) {
matchState[_mismatchedValueKey] = normalized;
return false;
}
return true;
}
@override
Description describe(Description description) {
if (_value is String) {
return description.add('normalized value matches $_value');
}
return description.add('normalized value matches\n').addDescriptionOf(_value);
}
@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
if (matchState.containsKey(_mismatchedValueKey)) {
final Object actualValue = matchState[_mismatchedValueKey] as Object;
// Leading whitespace is added so that lines in the multiline
// description returned by addDescriptionOf are all indented equally
// which makes the output easier to read for this case.
return mismatchDescription
.add('was expected to be normalized value\n')
.addDescriptionOf(_value)
.add('\nbut got\n')
.addDescriptionOf(actualValue);
}
return mismatchDescription;
}
}
/// Returns true if [c] represents a whitespace code unit.
bool _isWhitespace(int c) => (c <= 0x000D && c >= 0x0009) || c == 0x0020;
/// Returns true if [c] represents a vertical line Unicode line art code unit.
///
/// See [https://en.wikipedia.org/wiki/Box-drawing_character]. This method only
/// specifies vertical line art code units currently used by Flutter line art.
/// There are other line art characters that technically also represent vertical
/// lines.
bool _isVerticalLine(int c) {
return c == 0x2502 || c == 0x2503 || c == 0x2551 || c == 0x254e;
}
/// Returns whether a [line] is all vertical tree connector characters.
///
/// Example vertical tree connector characters: `│ ║ ╎`.
/// The last line of a text tree contains only vertical tree connector
/// characters indicates a poorly formatted tree.
bool _isAllTreeConnectorCharacters(String line) {
for (int i = 0; i < line.length; ++i) {
final int c = line.codeUnitAt(i);
if (!_isWhitespace(c) && !_isVerticalLine(c)) {
return false;
}
}
return true;
}
class _HasGoodToStringDeep extends Matcher {
const _HasGoodToStringDeep();
static final Object _toStringDeepErrorDescriptionKey = Object();
@override
bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
final List<String> issues = <String>[];
String description = object.toStringDeep() as String; // 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 (int i = 0; i < lines.length; ++i) {
final String line = lines[i];
if (line.isEmpty) {
issues.add('Line ${i + 1} is empty.');
}
if (line.trimRight() != line) {
issues.add('Line ${i + 1} has trailing whitespace.');
}
}
if (_isAllTreeConnectorCharacters(lines.last)) {
issues.add('Last line is all tree connector characters.');
}
// If a toStringDeep method doesn't properly handle nested values that
// contain line breaks it can fail to add the required prefixes to all
// lined when toStringDeep is called specifying prefixes.
const String prefixLineOne = 'PREFIX_LINE_ONE____';
const String prefixOtherLines = 'PREFIX_OTHER_LINES_';
final List<String> prefixIssues = <String>[];
// ignore: avoid_dynamic_calls
String descriptionWithPrefixes = object.toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines) as String;
if (descriptionWithPrefixes.endsWith('\n')) {
// Trim off trailing \n as the remaining calculations assume
// the description does not end with a trailing \n.
descriptionWithPrefixes = descriptionWithPrefixes.substring(
0, descriptionWithPrefixes.length - 1);
}
final List<String> linesWithPrefixes = descriptionWithPrefixes.split('\n');
if (!linesWithPrefixes.first.startsWith(prefixLineOne)) {
prefixIssues.add('First line does not contain expected prefix.');
}
for (int i = 1; i < linesWithPrefixes.length; ++i) {
if (!linesWithPrefixes[i].startsWith(prefixOtherLines)) {
prefixIssues.add('Line ${i + 1} does not contain the expected prefix.');
}
}
final StringBuffer errorDescription = StringBuffer();
if (issues.isNotEmpty) {
errorDescription.writeln('Bad toStringDeep():');
errorDescription.writeln(description);
errorDescription.writeAll(issues, '\n');
}
if (prefixIssues.isNotEmpty) {
errorDescription.writeln(
'Bad toStringDeep(prefixLineOne: "$prefixLineOne", prefixOtherLines: "$prefixOtherLines"):');
errorDescription.writeln(descriptionWithPrefixes);
errorDescription.writeAll(prefixIssues, '\n');
}
if (errorDescription.isNotEmpty) {
matchState[_toStringDeepErrorDescriptionKey] =
errorDescription.toString();
return false;
}
return true;
}
@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
if (matchState.containsKey(_toStringDeepErrorDescriptionKey)) {
return mismatchDescription.add(matchState[_toStringDeepErrorDescriptionKey] as String);
}
return mismatchDescription;
}
@override
Description describe(Description description) {
return description.add('multi line description');
}
}
/// Computes the distance between two values.
///
/// The distance should be a metric in a metric space (see
/// https://en.wikipedia.org/wiki/Metric_space). Specifically, if `f` is a
/// distance function then the following conditions should hold:
///
/// - f(a, b) >= 0
/// - f(a, b) == 0 if and only if a == b
/// - f(a, b) == f(b, a)
/// - f(a, c) <= f(a, b) + f(b, c), known as triangle inequality
///
/// This makes it useful for comparing numbers, [Color]s, [Offset]s and other
/// sets of value for which a metric space is defined.
typedef DistanceFunction<T> = num Function(T a, T b);
/// The type of a union of instances of [DistanceFunction<T>] for various types
/// T.
///
/// This type is used to describe a collection of [DistanceFunction<T>]
/// functions which have (potentially) unrelated argument types. Since the
/// argument types of the functions may be unrelated, 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) {
double delta = 0.0;
for (int i = 0; i < 16; i += 1) {
delta = math.max<double>((a[i] - b[i]).abs(), delta);
}
return delta;
}
double _sizeDistance(Size a, Size b) {
// TODO(a14n): remove ignore when lint is updated, https://github.com/dart-lang/linter/issues/1843
// ignore: unnecessary_parenthesis
final Offset delta = (b - a) as Offset;
return delta.distance;
}
/// Asserts that two values are within a certain distance from each other.
///
/// The distance is computed by a [DistanceFunction].
///
/// If `distanceFunction` is null, a standard distance function is used for the
/// `T` generic argument. Standard functions are defined for the following
/// types:
///
/// * [Color], whose distance is the maximum component-wise delta.
/// * [Offset], whose distance is the Euclidean distance computed using the
/// method [Offset.distance].
/// * [Rect], whose distance is the maximum component-wise delta.
/// * [Size], whose distance is the [Offset.distance] of the offset computed as
/// the difference between two sizes.
/// * [int], whose distance is the absolute difference between two integers.
/// * [double], whose distance is the absolute difference between two doubles.
///
/// See also:
///
/// * [moreOrLessEquals], which is similar to this function, but specializes in
/// [double]s and has an optional `epsilon` parameter.
/// * [rectMoreOrLessEquals], which is similar to this function, but
/// specializes in [Rect]s and has an optional `epsilon` parameter.
/// * [closeTo], which specializes in numbers only.
Matcher within<T>({
required num distance,
required T from,
DistanceFunction<T>? distanceFunction,
}) {
distanceFunction ??= _kStandardDistanceFunctions[T] as DistanceFunction<T>?;
if (distanceFunction == null) {
throw ArgumentError(
'The specified distanceFunction was null, and a standard distance '
'function was not found for type ${from.runtimeType} of the provided '
'`from` argument.'
);
}
return _IsWithinDistance<T>(distanceFunction, from, distance);
}
class _IsWithinDistance<T> extends Matcher {
const _IsWithinDistance(this.distanceFunction, this.value, this.epsilon);
final DistanceFunction<T> distanceFunction;
final T value;
final num epsilon;
@override
bool matches(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! double) {
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 (int i = 0; i < a.length; i++) {
if (!_deepEquals(a[i], b[i])) {
return false;
}
}
return true;
}
bool _deepEqualsMap(Map<dynamic, dynamic> a, Map<dynamic, dynamic> b) {
if (a.length != b.length) {
return false;
}
for (final dynamic key in a.keys) {
if (!b.containsKey(key) || !_deepEquals(a[key], b[key])) {
return false;
}
}
return true;
}
@override
Description describe(Description description) {
return description
.add('has method name: ').addDescriptionOf(name)
.add(' with arguments: ').addDescriptionOf(arguments);
}
}
/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderClipRect] with no clipper set, or an equivalent
/// [RenderClipPath].
const Matcher clipsWithBoundingRect = _ClipsWithBoundingRect();
/// Asserts that a [Finder] locates a single object whose root RenderObject is
/// not a [RenderClipRect], [RenderClipRRect], [RenderClipOval], or
/// [RenderClipPath].
const Matcher hasNoImmediateClip = _MatchAnythingExceptClip();
/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderClipRRect] with no clipper set, and border radius equals to
/// [borderRadius], or an equivalent [RenderClipPath].
Matcher clipsWithBoundingRRect({ required BorderRadius borderRadius }) {
return _ClipsWithBoundingRRect(borderRadius: borderRadius);
}
/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderClipPath] with a [ShapeBorderClipper] that clips to
/// [shape].
Matcher clipsWithShapeBorder({ required ShapeBorder shape }) {
return _ClipsWithShapeBorder(shape: shape);
}
/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is a [RenderPhysicalModel] or a [RenderPhysicalShape].
///
/// - If the render object is a [RenderPhysicalModel]
/// - If [shape] is non null asserts that [RenderPhysicalModel.shape] is equal to
/// [shape].
/// - If [borderRadius] is non null asserts that [RenderPhysicalModel.borderRadius] is equal to
/// [borderRadius].
/// - If [elevation] is non null asserts that [RenderPhysicalModel.elevation] is equal to
/// [elevation].
/// - If the render object is a [RenderPhysicalShape]
/// - If [borderRadius] is non null asserts that the shape is a rounded
/// rectangle with this radius.
/// - If [borderRadius] is null, asserts that the shape is equivalent to
/// [shape].
/// - If [elevation] is non null asserts that [RenderPhysicalModel.elevation] is equal to
/// [elevation].
Matcher rendersOnPhysicalModel({
BoxShape? shape,
BorderRadius? borderRadius,
double? elevation,
}) {
return _RendersOnPhysicalModel(
shape: shape,
borderRadius: borderRadius,
elevation: elevation,
);
}
/// Asserts that a [Finder] locates a single object whose root RenderObject
/// is [RenderPhysicalShape] that uses a [ShapeBorderClipper] that clips to
/// [shape] as its clipper.
/// If [elevation] is non null asserts that [RenderPhysicalShape.elevation] is
/// equal to [elevation].
Matcher rendersOnPhysicalShape({
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 RenderClipPath:
case RenderClipOval:
case RenderClipRect:
case RenderClipRRect:
return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
default:
return true;
}
}
@override
Description describe(Description description) {
return description.add('does not have a clip as an immediate child');
}
}
abstract class _MatchRenderObject<M extends RenderObject, T extends RenderObject> extends _FailWithDescriptionMatcher {
const _MatchRenderObject();
bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, T renderObject);
bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, M renderObject);
@override
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
final Iterable<Element> nodes = finder.evaluate();
if (nodes.length != 1) {
return failWithDescription(matchState, 'did not have a exactly one child element');
}
final RenderObject renderObject = nodes.single.renderObject!;
if (renderObject.runtimeType == T) {
return renderObjectMatchesT(matchState, renderObject as T);
}
if (renderObject.runtimeType == M) {
return renderObjectMatchesM(matchState, renderObject as M);
}
return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
}
}
class _RendersOnPhysicalModel extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
const _RendersOnPhysicalModel({
this.shape,
this.borderRadius,
this.elevation,
});
final BoxShape? shape;
final BorderRadius? borderRadius;
final double? elevation;
@override
bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) {
if (shape != null && renderObject.shape != shape) {
return failWithDescription(matchState, 'had shape: ${renderObject.shape}');
}
if (borderRadius != null && renderObject.borderRadius != borderRadius) {
return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');
}
if (elevation != null && renderObject.elevation != elevation) {
return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
}
return true;
}
@override
bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
}
final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
if (borderRadius != null && !assertRoundedRectangle(shapeClipper, borderRadius!, matchState)) {
return false;
}
if (borderRadius == null &&
shape == BoxShape.rectangle &&
!assertRoundedRectangle(shapeClipper, BorderRadius.zero, matchState)) {
return false;
}
if (borderRadius == null &&
shape == BoxShape.circle &&
!assertCircle(shapeClipper, matchState)) {
return false;
}
if (elevation != null && renderObject.elevation != elevation) {
return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
}
return true;
}
bool assertRoundedRectangle(ShapeBorderClipper shapeClipper, BorderRadius borderRadius, Map<dynamic, dynamic> matchState) {
if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) {
return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
}
final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
if (border.borderRadius != borderRadius) {
return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
}
return true;
}
bool assertCircle(ShapeBorderClipper shapeClipper, Map<dynamic, dynamic> matchState) {
if (shapeClipper.shape.runtimeType != CircleBorder) {
return failWithDescription(matchState, 'had shape border: ${shapeClipper.shape}');
}
return true;
}
@override
Description describe(Description description) {
description.add('renders on a physical model');
if (shape != null) {
description.add(' with shape $shape');
}
if (borderRadius != null) {
description.add(' with borderRadius $borderRadius');
}
if (elevation != null) {
description.add(' with elevation $elevation');
}
return description;
}
}
class _RendersOnPhysicalShape extends _MatchRenderObject<RenderPhysicalShape, RenderPhysicalModel> {
const _RendersOnPhysicalShape({
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 ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
if (shapeClipper.shape != shape) {
return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
}
if (elevation != null && renderObject.elevation != elevation) {
return failWithDescription(matchState, 'had elevation: ${renderObject.elevation}');
}
return true;
}
@override
bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderPhysicalModel renderObject) {
return false;
}
@override
Description describe(Description description) {
description.add('renders on a physical model with shape $shape');
if (elevation != null) {
description.add(' with elevation $elevation');
}
return description;
}
}
class _ClipsWithBoundingRect extends _MatchRenderObject<RenderClipPath, RenderClipRect> {
const _ClipsWithBoundingRect();
@override
bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRect renderObject) {
if (renderObject.clipper != null) {
return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');
}
return true;
}
@override
bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
}
final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) {
return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
}
final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
if (border.borderRadius != BorderRadius.zero) {
return failWithDescription(matchState, 'borderRadius was: ${border.borderRadius}');
}
return true;
}
@override
Description describe(Description description) =>
description.add('clips with bounding rectangle');
}
class _ClipsWithBoundingRRect extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
const _ClipsWithBoundingRRect({required this.borderRadius});
final BorderRadius borderRadius;
@override
bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) {
if (renderObject.clipper != null) {
return failWithDescription(matchState, 'had a non null clipper ${renderObject.clipper}');
}
if (renderObject.borderRadius != borderRadius) {
return failWithDescription(matchState, 'had borderRadius: ${renderObject.borderRadius}');
}
return true;
}
@override
bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
}
final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
if (shapeClipper.shape.runtimeType != RoundedRectangleBorder) {
return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
}
final RoundedRectangleBorder border = shapeClipper.shape as RoundedRectangleBorder;
if (border.borderRadius != borderRadius) {
return failWithDescription(matchState, 'had borderRadius: ${border.borderRadius}');
}
return true;
}
@override
Description describe(Description description) =>
description.add('clips with bounding rounded rectangle with borderRadius: $borderRadius');
}
class _ClipsWithShapeBorder extends _MatchRenderObject<RenderClipPath, RenderClipRRect> {
const _ClipsWithShapeBorder({required this.shape});
final ShapeBorder shape;
@override
bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderClipPath renderObject) {
if (renderObject.clipper.runtimeType != ShapeBorderClipper) {
return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
}
final ShapeBorderClipper shapeClipper = renderObject.clipper! as ShapeBorderClipper;
if (shapeClipper.shape != shape) {
return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
}
return true;
}
@override
bool renderObjectMatchesT(Map<dynamic, dynamic> matchState, RenderClipRRect renderObject) {
return false;
}
@override
Description describe(Description description) =>
description.add('clips with shape: $shape');
}
class _CoversSameAreaAs extends Matcher {
_CoversSameAreaAs(
this.expectedPath, {
required this.areaToCompare,
this.sampleSize = 20,
}) : maxHorizontalNoise = areaToCompare.width / sampleSize,
maxVerticalNoise = areaToCompare.height / sampleSize {
// Use a fixed random seed to make sure tests are deterministic.
random = math.Random(1);
}
final Path expectedPath;
final Rect areaToCompare;
final int sampleSize;
final double maxHorizontalNoise;
final double maxVerticalNoise;
late math.Random random;
@override
bool matches(covariant Path actualPath, Map<dynamic, dynamic> matchState) {
for (int i = 0; i < sampleSize; i += 1) {
for (int j = 0; j < sampleSize; j += 1) {
final Offset offset = Offset(
i * (areaToCompare.width / sampleSize),
j * (areaToCompare.height / sampleSize),
);
if (!_samplePoint(matchState, actualPath, offset)) {
return false;
}
final Offset noise = Offset(
maxHorizontalNoise * random.nextDouble(),
maxVerticalNoise * random.nextDouble(),
);
if (!_samplePoint(matchState, actualPath, offset + noise)) {
return false;
}
}
}
return true;
}
bool _samplePoint(Map<dynamic, dynamic> matchState, Path actualPath, Offset offset) {
if (expectedPath.contains(offset) == actualPath.contains(offset)) {
return true;
}
if (actualPath.contains(offset)) {
return failWithDescription(matchState, '$offset is contained in the actual path but not in the expected path');
} else {
return failWithDescription(matchState, '$offset is contained in the expected path but not in the actual path');
}
}
bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
matchState['failure'] = description;
return false;
}
@override
Description describeMismatch(
dynamic item,
Description mismatchDescription,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
return mismatchDescription.add(matchState['failure'] as String);
}
@override
Description describe(Description description) =>
description.add('covers expected area and only expected area');
}
class _ColorMatcher extends Matcher {
const _ColorMatcher({
required this.targetColor,
}) : assert(targetColor != null);
final Color targetColor;
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
if (item is Color) {
return item == targetColor || item.value == targetColor.value;
}
return false;
}
@override
Description describe(Description description) => description.add('matches color $targetColor');
}
int _countDifferentPixels(Uint8List imageA, Uint8List imageB) {
assert(imageA.length == imageB.length);
int delta = 0;
for (int i = 0; i < imageA.length; i+=4) {
if (imageA[i] != imageB[i] ||
imageA[i + 1] != imageB[i + 1] ||
imageA[i + 2] != imageB[i + 2] ||
imageA[i + 3] != imageB[i + 3]) {
delta++;
}
}
return delta;
}
class _MatchesReferenceImage extends AsyncMatcher {
const _MatchesReferenceImage(this.referenceImage);
final ui.Image referenceImage;
@override
Future<String?> matchAsync(dynamic item) async {
Future<ui.Image> imageFuture;
if (item is Future<ui.Image>) {
imageFuture = item;
} else if (item is ui.Image) {
imageFuture = Future<ui.Image>.value(item);
} else {
final Finder finder = item as Finder;
final Iterable<Element> elements = finder.evaluate();
if (elements.isEmpty) {
return 'could not be rendered because no widget was found';
} else if (elements.length > 1) {
return 'matched too many widgets';
}
imageFuture = captureImage(elements.single);
}
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
return binding.runAsync<String?>(() async {
final ui.Image image = await imageFuture;
final ByteData? bytes = await image.toByteData();
if (bytes == null) {
return 'could not be encoded.';
}
final ByteData? referenceBytes = await referenceImage.toByteData();
if (referenceBytes == null) {
return 'could not have its reference image encoded.';
}
if (referenceImage.height != image.height || referenceImage.width != image.width) {
return 'does not match as width or height do not match. $image != $referenceImage';
}
final int countDifferentPixels = _countDifferentPixels(
Uint8List.view(bytes.buffer),
Uint8List.view(referenceBytes.buffer),
);
return countDifferentPixels == 0 ? null : 'does not match on $countDifferentPixels pixels';
}, additionalTime: const Duration(minutes: 1));
}
@override
Description describe(Description description) {
return description.add('rasterized image matches that of a $referenceImage reference image');
}
}
class _MatchesSemanticsData extends Matcher {
_MatchesSemanticsData({
required this.label,
required this.attributedLabel,
required this.hint,
required this.attributedHint,
required this.value,
required this.attributedValue,
required this.increasedValue,
required this.attributedIncreasedValue,
required this.decreasedValue,
required this.attributedDecreasedValue,
required this.tooltip,
required this.textDirection,
required this.rect,
required this.size,
required this.elevation,
required this.thickness,
required this.platformViewId,
required this.maxValueLength,
required this.currentValueLength,
// Flags
required bool? hasCheckedState,
required bool? isChecked,
required bool? isCheckStateMixed,
required bool? isSelected,
required bool? isButton,
required bool? isSlider,
required bool? isKeyboardKey,
required bool? isLink,
required bool? isFocused,
required bool? isFocusable,
required bool? isTextField,
required bool? isReadOnly,
required bool? hasEnabledState,
required bool? isEnabled,
required bool? isInMutuallyExclusiveGroup,
required bool? isHeader,
required bool? isObscured,
required bool? isMultiline,
required bool? namesRoute,
required bool? scopesRoute,
required bool? isHidden,
required bool? isImage,
required bool? isLiveRegion,
required bool? hasToggledState,
required bool? isToggled,
required bool? hasImplicitScrolling,
// Actions
required bool? hasTapAction,
required bool? hasLongPressAction,
required bool? hasScrollLeftAction,
required bool? hasScrollRightAction,
required bool? hasScrollUpAction,
required bool? hasScrollDownAction,
required bool? hasIncreaseAction,
required bool? hasDecreaseAction,
required bool? hasShowOnScreenAction,
required bool? hasMoveCursorForwardByCharacterAction,
required bool? hasMoveCursorBackwardByCharacterAction,
required bool? hasMoveCursorForwardByWordAction,
required bool? hasMoveCursorBackwardByWordAction,
required bool? hasSetTextAction,
required bool? hasSetSelectionAction,
required bool? hasCopyAction,
required bool? hasCutAction,
required bool? hasPasteAction,
required bool? hasDidGainAccessibilityFocusAction,
required bool? hasDidLoseAccessibilityFocusAction,
required bool? hasDismissAction,
// Custom actions and overrides
required String? onTapHint,
required String? onLongPressHint,
required this.customActions,
required this.children,
}) : flags = <SemanticsFlag, bool>{
if (hasCheckedState != null) SemanticsFlag.hasCheckedState: hasCheckedState,
if (isChecked != null) SemanticsFlag.isChecked: isChecked,
if (isCheckStateMixed != null) SemanticsFlag.isCheckStateMixed: isCheckStateMixed,
if (isSelected != null) SemanticsFlag.isSelected: isSelected,
if (isButton != null) SemanticsFlag.isButton: isButton,
if (isSlider != null) SemanticsFlag.isSlider: isSlider,
if (isKeyboardKey != null) SemanticsFlag.isKeyboardKey: isKeyboardKey,
if (isLink != null) SemanticsFlag.isLink: isLink,
if (isTextField != null) SemanticsFlag.isTextField: isTextField,
if (isReadOnly != null) SemanticsFlag.isReadOnly: isReadOnly,
if (isFocused != null) SemanticsFlag.isFocused: isFocused,
if (isFocusable != null) SemanticsFlag.isFocusable: isFocusable,
if (hasEnabledState != null) SemanticsFlag.hasEnabledState: hasEnabledState,
if (isEnabled != null) SemanticsFlag.isEnabled: isEnabled,
if (isInMutuallyExclusiveGroup != null) SemanticsFlag.isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup,
if (isHeader != null) SemanticsFlag.isHeader: isHeader,
if (isObscured != null) SemanticsFlag.isObscured: isObscured,
if (isMultiline != null) SemanticsFlag.isMultiline: isMultiline,
if (namesRoute != null) SemanticsFlag.namesRoute: namesRoute,
if (scopesRoute != null) SemanticsFlag.scopesRoute: scopesRoute,
if (isHidden != null) SemanticsFlag.isHidden: isHidden,
if (isImage != null) SemanticsFlag.isImage: isImage,
if (isLiveRegion != null) SemanticsFlag.isLiveRegion: isLiveRegion,
if (hasToggledState != null) SemanticsFlag.hasToggledState: hasToggledState,
if (isToggled != null) SemanticsFlag.isToggled: isToggled,
if (hasImplicitScrolling != null) SemanticsFlag.hasImplicitScrolling: hasImplicitScrolling,
if (isSlider != null) SemanticsFlag.isSlider: isSlider,
},
actions = <SemanticsAction, bool>{
if (hasTapAction != null) SemanticsAction.tap: hasTapAction,
if (hasLongPressAction != null) SemanticsAction.longPress: hasLongPressAction,
if (hasScrollLeftAction != null) SemanticsAction.scrollLeft: hasScrollLeftAction,
if (hasScrollRightAction != null) SemanticsAction.scrollRight: hasScrollRightAction,
if (hasScrollUpAction != null) SemanticsAction.scrollUp: hasScrollUpAction,
if (hasScrollDownAction != null) SemanticsAction.scrollDown: hasScrollDownAction,
if (hasIncreaseAction != null) SemanticsAction.increase: hasIncreaseAction,
if (hasDecreaseAction != null) SemanticsAction.decrease: hasDecreaseAction,
if (hasShowOnScreenAction != null) SemanticsAction.showOnScreen: hasShowOnScreenAction,
if (hasMoveCursorForwardByCharacterAction != null) SemanticsAction.moveCursorForwardByCharacter: hasMoveCursorForwardByCharacterAction,
if (hasMoveCursorBackwardByCharacterAction != null) SemanticsAction.moveCursorBackwardByCharacter: hasMoveCursorBackwardByCharacterAction,
if (hasSetSelectionAction != null) SemanticsAction.setSelection: hasSetSelectionAction,
if (hasCopyAction != null) SemanticsAction.copy: hasCopyAction,
if (hasCutAction != null) SemanticsAction.cut: hasCutAction,
if (hasPasteAction != null) SemanticsAction.paste: hasPasteAction,
if (hasDidGainAccessibilityFocusAction != null) SemanticsAction.didGainAccessibilityFocus: hasDidGainAccessibilityFocusAction,
if (hasDidLoseAccessibilityFocusAction != null) SemanticsAction.didLoseAccessibilityFocus: hasDidLoseAccessibilityFocusAction,
if (customActions != null) SemanticsAction.customAction: customActions.isNotEmpty,
if (hasDismissAction != null) SemanticsAction.dismiss: hasDismissAction,
if (hasMoveCursorForwardByWordAction != null) SemanticsAction.moveCursorForwardByWord: hasMoveCursorForwardByWordAction,
if (hasMoveCursorBackwardByWordAction != null) SemanticsAction.moveCursorBackwardByWord: hasMoveCursorBackwardByWordAction,
if (hasSetTextAction != null) SemanticsAction.setText: hasSetTextAction,
},
hintOverrides = onTapHint == null && onLongPressHint == null
? null
: SemanticsHintOverrides(
onTapHint: onTapHint,
onLongPressHint: onLongPressHint,
);
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 double? elevation;
final double? thickness;
final int? platformViewId;
final int? maxValueLength;
final int? currentValueLength;
final List<Matcher>? children;
/// There are three possible states for these two maps:
///
/// 1. If the flag/action maps to `true`, then it must be present in the SemanticData
/// 2. If the flag/action maps to `false`, then it must not be present in the SemanticData
/// 3. If the flag/action is not in the map, then it will not be validated against
final Map<SemanticsAction, bool> actions;
final Map<SemanticsFlag, bool> flags;
@override
Description describe(Description description) {
description.add('has semantics');
if (label != null) {
description.add(' with label: $label');
}
if (attributedLabel != null) {
description.add(' with attributedLabel: $attributedLabel');
}
if (value != null) {
description.add(' with value: $value');
}
if (attributedValue != null) {
description.add(' with attributedValue: $attributedValue');
}
if (hint != null) {
description.add(' with hint: $hint');
}
if (attributedHint != null) {
description.add(' with attributedHint: $attributedHint');
}
if (increasedValue != null) {
description.add(' with increasedValue: $increasedValue ');
}
if (attributedIncreasedValue != null) {
description.add(' with attributedIncreasedValue: $attributedIncreasedValue');
}
if (decreasedValue != null) {
description.add(' with decreasedValue: $decreasedValue ');
}
if (attributedDecreasedValue != null) {
description.add(' with attributedDecreasedValue: $attributedDecreasedValue');
}
if (tooltip != null) {
description.add(' with tooltip: $tooltip');
}
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: ${_createEnumsSummary(expectedActions)} ');
}
if (notExpectedActions.isNotEmpty) {
description.add(' without actions: ${_createEnumsSummary(notExpectedActions)} ');
}
}
if (flags.isNotEmpty) {
final List<SemanticsFlag> expectedFlags = flags.entries
.where((MapEntry<ui.SemanticsFlag, bool> e) => e.value)
.map((MapEntry<ui.SemanticsFlag, bool> e) => e.key)
.toList();
final List<SemanticsFlag> notExpectedFlags = flags.entries
.where((MapEntry<ui.SemanticsFlag, bool> e) => !e.value)
.map((MapEntry<ui.SemanticsFlag, bool> e) => e.key)
.toList();
if (expectedFlags.isNotEmpty) {
description.add(' with flags: ${_createEnumsSummary(expectedFlags)} ');
}
if (notExpectedFlags.isNotEmpty) {
description.add(' without flags: ${_createEnumsSummary(notExpectedFlags)} ');
}
}
if (textDirection != null) {
description.add(' with textDirection: $textDirection ');
}
if (rect != null) {
description.add(' with rect: $rect');
}
if (size != null) {
description.add(' with size: $size');
}
if (elevation != null) {
description.add(' with elevation: $elevation');
}
if (thickness != null) {
description.add(' with thickness: $thickness');
}
if (platformViewId != null) {
description.add(' with platformViewId: $platformViewId');
}
if (maxValueLength != null) {
description.add(' with maxValueLength: $maxValueLength');
}
if (currentValueLength != null) {
description.add(' with currentValueLength: $currentValueLength');
}
if (customActions != null) {
description.add(' with custom actions: $customActions');
}
if (hintOverrides != null) {
description.add(' with custom hints: $hintOverrides');
}
if (children != null) {
description.add(' with children:\n');
for (final _MatchesSemanticsData child in children!.cast<_MatchesSemanticsData>()) {
child.describe(description);
}
}
return description;
}
bool _stringAttributesEqual(List<StringAttribute> first, List<StringAttribute> second) {
if (first.length != second.length) {
return false;
}
for (int i = 0; i < first.length; i++) {
if (first[i] is SpellOutStringAttribute &&
(second[i] is! SpellOutStringAttribute ||
second[i].range != first[i].range)) {
return false;
}
if (first[i] is LocaleStringAttribute &&
(second[i] is! LocaleStringAttribute ||
second[i].range != first[i].range ||
(second[i] as LocaleStringAttribute).locale != (second[i] as LocaleStringAttribute).locale)) {
return false;
}
}
return true;
}
@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 = node is SemanticsNode ? node.getSemanticsData() : (node as SemanticsData);
if (label != null && label != data.label) {
return failWithDescription(matchState, 'label was: ${data.label}');
}
if (attributedLabel != null &&
(attributedLabel!.string != data.attributedLabel.string ||
!_stringAttributesEqual(attributedLabel!.attributes, data.attributedLabel.attributes))) {
return failWithDescription(
matchState, 'attributedLabel was: ${data.attributedLabel}');
}
if (hint != null && hint != data.hint) {
return failWithDescription(matchState, 'hint was: ${data.hint}');
}
if (attributedHint != null &&
(attributedHint!.string != data.attributedHint.string ||
!_stringAttributesEqual(attributedHint!.attributes, data.attributedHint.attributes))) {
return failWithDescription(
matchState, 'attributedHint was: ${data.attributedHint}');
}
if (value != null && value != data.value) {
return failWithDescription(matchState, 'value was: ${data.value}');
}
if (attributedValue != null &&
(attributedValue!.string != data.attributedValue.string ||
!_stringAttributesEqual(attributedValue!.attributes, data.attributedValue.attributes))) {
return failWithDescription(
matchState, 'attributedValue was: ${data.attributedValue}');
}
if (increasedValue != null && increasedValue != data.increasedValue) {
return failWithDescription(matchState, 'increasedValue was: ${data.increasedValue}');
}
if (attributedIncreasedValue != null &&
(attributedIncreasedValue!.string != data.attributedIncreasedValue.string ||
!_stringAttributesEqual(attributedIncreasedValue!.attributes, data.attributedIncreasedValue.attributes))) {
return failWithDescription(
matchState, 'attributedIncreasedValue was: ${data.attributedIncreasedValue}');
}
if (decreasedValue != null && decreasedValue != data.decreasedValue) {
return failWithDescription(matchState, 'decreasedValue was: ${data.decreasedValue}');
}
if (attributedDecreasedValue != null &&
(attributedDecreasedValue!.string != data.attributedDecreasedValue.string ||
!_stringAttributesEqual(attributedDecreasedValue!.attributes, data.attributedDecreasedValue.attributes))) {
return failWithDescription(
matchState, 'attributedDecreasedValue was: ${data.attributedDecreasedValue}');
}
if (tooltip != null && tooltip != data.tooltip) {
return failWithDescription(matchState, 'tooltip was: ${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 (elevation != null && elevation != data.elevation) {
return failWithDescription(matchState, 'elevation was: ${data.elevation}');
}
if (thickness != null && thickness != data.thickness) {
return failWithDescription(matchState, 'thickness was: ${data.thickness}');
}
if (platformViewId != null && platformViewId != data.platformViewId) {
return failWithDescription(matchState, 'platformViewId was: ${data.platformViewId}');
}
if (currentValueLength != null && currentValueLength != data.currentValueLength) {
return failWithDescription(matchState, 'currentValueLength was: ${data.currentValueLength}');
}
if (maxValueLength != null && maxValueLength != data.maxValueLength) {
return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}');
}
if (actions.isNotEmpty) {
final List<SemanticsAction> unexpectedActions = <SemanticsAction>[];
final List<SemanticsAction> missingActions = <SemanticsAction>[];
for (final MapEntry<ui.SemanticsAction, bool> actionEntry in actions.entries) {
final ui.SemanticsAction action = actionEntry.key;
final bool actionExpected = actionEntry.value;
final bool actionPresent = (action.index & data.actions) == action.index;
if (actionPresent != actionExpected) {
if(actionExpected) {
missingActions.add(action);
} else {
unexpectedActions.add(action);
}
}
}
if (unexpectedActions.isNotEmpty || missingActions.isNotEmpty) {
return failWithDescription(matchState, 'missing actions: ${_createEnumsSummary(missingActions)} unexpected actions: ${_createEnumsSummary(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 (int i = 0; i < expectedCustomActions.length; i++) {
if (expectedCustomActions[i] != providedCustomActions[i]) {
return failWithDescription(matchState, 'custom actions were: $providedCustomActions');
}
}
}
if (flags.isNotEmpty) {
final List<SemanticsFlag> unexpectedFlags = <SemanticsFlag>[];
final List<SemanticsFlag> missingFlags = <SemanticsFlag>[];
for (final MapEntry<ui.SemanticsFlag, bool> flagEntry in flags.entries) {
final ui.SemanticsFlag flag = flagEntry.key;
final bool flagExpected = flagEntry.value;
final bool flagPresent = flag.index & data.flags == flag.index;
if (flagPresent != flagExpected) {
if(flagExpected) {
missingFlags.add(flag);
} else {
unexpectedFlags.add(flag);
}
}
}
if (unexpectedFlags.isNotEmpty || missingFlags.isNotEmpty) {
return failWithDescription(matchState, 'missing flags: ${_createEnumsSummary(missingFlags)} unexpected flags: ${_createEnumsSummary(unexpectedFlags)}');
}
}
bool allMatched = true;
if (children != null) {
int i = 0;
(node as SemanticsNode).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 _createEnumsSummary<T extends Object>(List<T> enums) {
assert(T == SemanticsAction || T == SemanticsFlag, 'This method is only intended for lists of SemanticsActions or SemanticsFlags.');
return '[${enums.map(describeEnum).join(', ')}]';
}
}
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;
}
}