mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00

fuchsia_tester.dart still assumes Dart 1. Previously, it ran tests directly from source, flutter_platform.dart automatically runs a kernel compile when operating in Dart 2 mode, but this assumes a functional Dart SDK is available in the artifacts directly, and fuchsia_tester.dart mocks out the artifacts directory with an empty temp dir. Remaining work is: 1. Get the frontend server building as a dependency on Fuchsia. 2. Patch fuchsia_tester.dart to use a valid Dart SDK and frontend server. This also reverts migration to Dart 2 typedef syntax. This reverts commit6c56bb2
. (#18362) This reverts commit3daebd0
. (#18316)
1272 lines
43 KiB
Dart
1272 lines
43 KiB
Dart
// Copyright 2016 The Chromium Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'dart:async';
|
|
import 'dart:math' as math;
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:meta/meta.dart';
|
|
import 'package:test/test.dart' hide TypeMatcher;
|
|
import 'package:test/src/frontend/async_matcher.dart'; // ignore: implementation_imports
|
|
|
|
import 'binding.dart';
|
|
import 'finders.dart';
|
|
import 'goldens.dart';
|
|
|
|
/// Asserts that the [Finder] matches no widgets in the widget tree.
|
|
///
|
|
/// ## Sample code
|
|
///
|
|
/// ```dart
|
|
/// expect(find.text('Save'), findsNothing);
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [findsWidgets], when you want the finder to find one or more widgets.
|
|
/// * [findsOneWidget], when you want the finder to find exactly one widget.
|
|
/// * [findsNWidgets], when you want the finder to find a specific number of widgets.
|
|
const Matcher findsNothing = const _FindsWidgetMatcher(null, 0);
|
|
|
|
/// Asserts that the [Finder] locates at least one widget in the widget tree.
|
|
///
|
|
/// ## Sample code
|
|
///
|
|
/// ```dart
|
|
/// expect(find.text('Save'), findsWidgets);
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [findsNothing], when you want the finder to not find anything.
|
|
/// * [findsOneWidget], when you want the finder to find exactly one widget.
|
|
/// * [findsNWidgets], when you want the finder to find a specific number of widgets.
|
|
const Matcher findsWidgets = const _FindsWidgetMatcher(1, null);
|
|
|
|
/// Asserts that the [Finder] locates at exactly one widget in the widget tree.
|
|
///
|
|
/// ## Sample code
|
|
///
|
|
/// ```dart
|
|
/// expect(find.text('Save'), findsOneWidget);
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [findsNothing], when you want the finder to not find anything.
|
|
/// * [findsWidgets], when you want the finder to find one or more widgets.
|
|
/// * [findsNWidgets], when you want the finder to find a specific number of widgets.
|
|
const Matcher findsOneWidget = const _FindsWidgetMatcher(1, 1);
|
|
|
|
/// Asserts that the [Finder] locates the specified number of widgets in the widget tree.
|
|
///
|
|
/// ## Sample code
|
|
///
|
|
/// ```dart
|
|
/// expect(find.text('Save'), findsNWidgets(2));
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [findsNothing], when you want the finder to not find anything.
|
|
/// * [findsWidgets], when you want the finder to find one or more widgets.
|
|
/// * [findsOneWidget], when you want the finder to find exactly one widget.
|
|
Matcher findsNWidgets(int n) => new _FindsWidgetMatcher(n, n);
|
|
|
|
/// Asserts that the [Finder] locates the 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 = const _IsOffstage();
|
|
|
|
/// Asserts that the [Finder] locates the a single widget that has no
|
|
/// [Offstage] widget ancestors.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [isOffstage], the opposite.
|
|
const Matcher isOnstage = const _IsOnstage();
|
|
|
|
/// Asserts that the [Finder] locates the a single widget that has at
|
|
/// least one [Card] widget ancestor.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [isNotInCard], the opposite.
|
|
const Matcher isInCard = const _IsInCard();
|
|
|
|
/// Asserts that the [Finder] locates the a single widget that has no
|
|
/// [Card] widget ancestors.
|
|
///
|
|
/// This is equivalent to `isNot(isInCard)`.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [isInCard], the opposite.
|
|
const Matcher isNotInCard = const _IsNotInCard();
|
|
|
|
/// 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 = const _HasOneLineDescription();
|
|
|
|
/// Asserts that an object's toStringDeep() is a plausible multi-line
|
|
/// 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 = const _HasGoodToStringDeep();
|
|
|
|
/// A matcher for functions that throw [FlutterError].
|
|
///
|
|
/// This is equivalent to `throwsA(const isInstanceOf<FlutterError>())`.
|
|
///
|
|
/// 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(const isInstanceOf<AssertionError>())`.
|
|
///
|
|
/// 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 `const isInstanceOf<FlutterError>()`.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [throwsFlutterError], to test if a function throws a [FlutterError].
|
|
/// * [isAssertionError], to test if any object is any kind of [AssertionError].
|
|
const Matcher isFlutterError = const isInstanceOf<FlutterError>();
|
|
|
|
/// A matcher for [AssertionError].
|
|
///
|
|
/// This is equivalent to `const isInstanceOf<AssertionError>()`.
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [throwsAssertionError], to test if a function throws any [AssertionError].
|
|
/// * [isFlutterError], to test if any object is a [FlutterError].
|
|
const Matcher isAssertionError = const isInstanceOf<AssertionError>();
|
|
|
|
/// Asserts that two [double]s are equal, within some tolerated error.
|
|
///
|
|
/// Two values are considered equal if the difference between them is within
|
|
/// 1e-10 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.
|
|
///
|
|
/// 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.
|
|
Matcher moreOrLessEquals(double value, { double epsilon = 1e-10 }) {
|
|
return new _MoreOrLessEquals(value, epsilon);
|
|
}
|
|
|
|
/// Asserts that two [String]s are equal after normalizing likely hash codes.
|
|
///
|
|
/// A `#` followed by 5 hexadecimal digits is assumed to be a short hash code
|
|
/// and is normalized to #00000.
|
|
///
|
|
/// See Also:
|
|
///
|
|
/// * [describeIdentity], a method that generates short descriptions of objects
|
|
/// with ids that match the pattern #[0-9a-f]{5}.
|
|
/// * [shortHash], a method that generates a 5 character long hexadecimal
|
|
/// [String] based on [Object.hashCode].
|
|
/// * [TreeDiagnosticsMixin.toStringDeep], a method that returns a [String]
|
|
/// typically containing multiple hash codes.
|
|
Matcher equalsIgnoringHashCodes(String value) {
|
|
return new _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 new _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})
|
|
=> new _CoversSameAreaAs(expectedPath, areaToCompare: areaToCompare, sampleSize: sampleSize);
|
|
|
|
/// Asserts that a [Finder] matches exactly one widget whose rendered image
|
|
/// matches the golden image file identified by [key].
|
|
///
|
|
/// [key] may be either a [Uri] or a [String] representation of a URI.
|
|
///
|
|
/// 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
|
|
/// await expectLater(find.text('Save'), matchesGoldenFile('save.png'));
|
|
/// ```
|
|
///
|
|
/// See also:
|
|
///
|
|
/// * [goldenFileComparator], which acts as the backend for this matcher.
|
|
/// * [flutter_test] for a discussion of test configurations, whereby callers
|
|
/// may swap out the backend for this matcher.
|
|
Matcher matchesGoldenFile(dynamic key) {
|
|
if (key is Uri) {
|
|
return new _MatchesGoldenFile(key);
|
|
} else if (key is String) {
|
|
return new _MatchesGoldenFile.forStringPath(key);
|
|
}
|
|
throw new ArgumentError('Unexpected type for golden file: ${key.runtimeType}');
|
|
}
|
|
|
|
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];
|
|
final int count = finder.evaluate().length;
|
|
if (count == 0) {
|
|
assert(min != null && min > 0);
|
|
if (min == 1 && max == 1)
|
|
return mismatchDescription.add('means none were found but one was expected');
|
|
return mismatchDescription.add('means none were found but some were expected');
|
|
}
|
|
if (max == 0) {
|
|
if (count == 1)
|
|
return mismatchDescription.add('means one was found but none were expected');
|
|
return mismatchDescription.add('means some were found but none were expected');
|
|
}
|
|
if (min != null && count < min)
|
|
return mismatchDescription.add('is not enough');
|
|
assert(max != null && count > min);
|
|
return mismatchDescription.add('is too many');
|
|
}
|
|
}
|
|
|
|
bool _hasAncestorMatching(Finder finder, bool predicate(Widget widget)) {
|
|
final Iterable<Element> nodes = finder.evaluate();
|
|
if (nodes.length != 1)
|
|
return false;
|
|
bool result = false;
|
|
nodes.single.visitAncestorElements((Element ancestor) {
|
|
if (predicate(ancestor.widget)) {
|
|
result = true;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
bool _hasAncestorOfType(Finder finder, Type targetType) {
|
|
return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType);
|
|
}
|
|
|
|
class _IsOffstage extends Matcher {
|
|
const _IsOffstage();
|
|
|
|
@override
|
|
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
|
|
return _hasAncestorMatching(finder, (Widget widget) {
|
|
if (widget is Offstage)
|
|
return widget.offstage;
|
|
return false;
|
|
});
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) => description.add('offstage');
|
|
}
|
|
|
|
class _IsOnstage extends Matcher {
|
|
const _IsOnstage();
|
|
|
|
@override
|
|
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) {
|
|
final Iterable<Element> nodes = finder.evaluate();
|
|
if (nodes.length != 1)
|
|
return false;
|
|
bool result = true;
|
|
nodes.single.visitAncestorElements((Element ancestor) {
|
|
final Widget widget = ancestor.widget;
|
|
if (widget is Offstage) {
|
|
result = !widget.offstage;
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) => description.add('onstage');
|
|
}
|
|
|
|
class _IsInCard extends Matcher {
|
|
const _IsInCard();
|
|
|
|
@override
|
|
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => _hasAncestorOfType(finder, Card);
|
|
|
|
@override
|
|
Description describe(Description description) => description.add('in card');
|
|
}
|
|
|
|
class _IsNotInCard extends Matcher {
|
|
const _IsNotInCard();
|
|
|
|
@override
|
|
bool matches(covariant Finder finder, Map<dynamic, dynamic> matchState) => !_hasAncestorOfType(finder, Card);
|
|
|
|
@override
|
|
Description describe(Description description) => description.add('not in card');
|
|
}
|
|
|
|
class _HasOneLineDescription extends Matcher {
|
|
const _HasOneLineDescription();
|
|
|
|
@override
|
|
bool matches(Object object, Map<dynamic, dynamic> matchState) {
|
|
final String description = object.toString();
|
|
return description.isNotEmpty
|
|
&& !description.contains('\n')
|
|
&& !description.contains('Instance of ')
|
|
&& description.trim() == description;
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) => description.add('one line description');
|
|
}
|
|
|
|
class _EqualsIgnoringHashCodes extends Matcher {
|
|
_EqualsIgnoringHashCodes(String v) : _value = _normalize(v);
|
|
|
|
final String _value;
|
|
|
|
static final Object _mismatchedValueKey = new Object();
|
|
|
|
static String _normalize(String s) {
|
|
return s.replaceAll(new RegExp(r'#[0-9a-f]{5}'), '#00000');
|
|
}
|
|
|
|
@override
|
|
bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
|
|
final String description = _normalize(object);
|
|
if (_value != description) {
|
|
matchState[_mismatchedValueKey] = description;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) {
|
|
return description.add('multi line description equals $_value');
|
|
}
|
|
|
|
@override
|
|
Description describeMismatch(
|
|
dynamic item,
|
|
Description mismatchDescription,
|
|
Map<dynamic, dynamic> matchState,
|
|
bool verbose
|
|
) {
|
|
if (matchState.containsKey(_mismatchedValueKey)) {
|
|
final String actualValue = matchState[_mismatchedValueKey];
|
|
// Leading whitespace is added so that lines in the multi-line
|
|
// description returned by addDescriptionOf are all indented equally
|
|
// which makes the output easier to read for this case.
|
|
return mismatchDescription
|
|
.add('expected normalized value\n ')
|
|
.addDescriptionOf(_value)
|
|
.add('\nbut got\n ')
|
|
.addDescriptionOf(actualValue);
|
|
}
|
|
return mismatchDescription;
|
|
}
|
|
}
|
|
|
|
/// Returns true if [c] represents a whitespace code unit.
|
|
bool _isWhitespace(int c) => (c <= 0x000D && c >= 0x0009) || c == 0x0020;
|
|
|
|
/// Returns true if [c] represents a vertical line Unicode line art code unit.
|
|
///
|
|
/// See [https://en.wikipedia.org/wiki/Box-drawing_character]. This method only
|
|
/// specifies vertical line art code units currently used by Flutter line art.
|
|
/// There are other line art characters that technically also represent vertical
|
|
/// lines.
|
|
bool _isVerticalLine(int c) {
|
|
return c == 0x2502 || c == 0x2503 || c == 0x2551 || c == 0x254e;
|
|
}
|
|
|
|
/// Returns whether a [line] is all vertical tree connector characters.
|
|
///
|
|
/// Example vertical tree connector characters: `│ ║ ╎`.
|
|
/// The last line of a text tree contains only vertical tree connector
|
|
/// characters indicates a poorly formatted tree.
|
|
bool _isAllTreeConnectorCharacters(String line) {
|
|
for (int i = 0; i < line.length; ++i) {
|
|
final int c = line.codeUnitAt(i);
|
|
if (!_isWhitespace(c) && !_isVerticalLine(c))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
class _HasGoodToStringDeep extends Matcher {
|
|
const _HasGoodToStringDeep();
|
|
|
|
static final Object _toStringDeepErrorDescriptionKey = new Object();
|
|
|
|
@override
|
|
bool matches(dynamic object, Map<dynamic, dynamic> matchState) {
|
|
final List<String> issues = <String>[];
|
|
String description = object.toStringDeep();
|
|
if (description.endsWith('\n')) {
|
|
// Trim off trailing \n as the remaining calculations assume
|
|
// the description does not end with a trailing \n.
|
|
description = description.substring(0, description.length - 1);
|
|
} else {
|
|
issues.add('Not terminated with a line break.');
|
|
}
|
|
|
|
if (description.trim() != description)
|
|
issues.add('Has trailing whitespace.');
|
|
|
|
final List<String> lines = description.split('\n');
|
|
if (lines.length < 2)
|
|
issues.add('Does not have multiple lines.');
|
|
|
|
if (description.contains('Instance of '))
|
|
issues.add('Contains text "Instance of ".');
|
|
|
|
for (int i = 0; i < lines.length; ++i) {
|
|
final String line = lines[i];
|
|
if (line.isEmpty)
|
|
issues.add('Line ${i+1} is empty.');
|
|
|
|
if (line.trimRight() != line)
|
|
issues.add('Line ${i+1} has trailing whitespace.');
|
|
}
|
|
|
|
if (_isAllTreeConnectorCharacters(lines.last))
|
|
issues.add('Last line is all tree connector characters.');
|
|
|
|
// If a toStringDeep method doesn't properly handle nested values that
|
|
// contain line breaks it can fail to add the required prefixes to all
|
|
// lined when toStringDeep is called specifying prefixes.
|
|
const String prefixLineOne = 'PREFIX_LINE_ONE____';
|
|
const String prefixOtherLines = 'PREFIX_OTHER_LINES_';
|
|
final List<String> prefixIssues = <String>[];
|
|
String descriptionWithPrefixes =
|
|
object.toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines);
|
|
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 = new 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]);
|
|
}
|
|
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 num DistanceFunction<T>(T a, T b);
|
|
|
|
/// The type of a union of instances of [DistanceFunction<T>] for various types
|
|
/// T.
|
|
///
|
|
/// This type is used to describe a collection of [DistanceFunction<T>]
|
|
/// functions which have (potentially) unrelated argument types. Since the
|
|
/// argument types of the functions may be unrelated, the only thing that the
|
|
/// type system can statically assume about them is that they accept null (since
|
|
/// all types in Dart are nullable).
|
|
///
|
|
/// Calling an instance of this type must either be done dynamically, or by
|
|
/// first casting it to a [DistanceFunction<T>] for some concrete T.
|
|
typedef num AnyDistanceFunction(Null a, Null b);
|
|
|
|
const Map<Type, AnyDistanceFunction> _kStandardDistanceFunctions = const <Type, AnyDistanceFunction>{
|
|
Color: _maxComponentColorDistance,
|
|
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();
|
|
}
|
|
|
|
double _rectDistance(Rect a, Rect b) {
|
|
double delta = math.max<double>((a.left - b.left).abs(), (a.top - b.top).abs());
|
|
delta = math.max<double>(delta, (a.right - b.right).abs());
|
|
delta = math.max<double>(delta, (a.bottom - b.bottom).abs());
|
|
return delta;
|
|
}
|
|
|
|
double _sizeDistance(Size a, Size b) {
|
|
final Offset delta = b - a;
|
|
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
|
|
/// `runtimeType` of the `from` 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.
|
|
/// * [closeTo], which specializes in numbers only.
|
|
Matcher within<T>({
|
|
@required num distance,
|
|
@required T from,
|
|
DistanceFunction<T> distanceFunction,
|
|
}) {
|
|
distanceFunction ??= _kStandardDistanceFunctions[from.runtimeType];
|
|
|
|
if (distanceFunction == null) {
|
|
throw new ArgumentError(
|
|
'The specified distanceFunction was null, and a standard distance '
|
|
'function was not found for type ${from.runtimeType} of the provided '
|
|
'`from` argument.'
|
|
);
|
|
}
|
|
|
|
return new _IsWithinDistance<T>(distanceFunction, from, distance);
|
|
}
|
|
|
|
class _IsWithinDistance<T> extends Matcher {
|
|
const _IsWithinDistance(this.distanceFunction, this.value, this.epsilon);
|
|
|
|
final DistanceFunction<T> distanceFunction;
|
|
final T value;
|
|
final num epsilon;
|
|
|
|
@override
|
|
bool matches(Object object, Map<dynamic, dynamic> matchState) {
|
|
if (object is! T)
|
|
return false;
|
|
if (object == value)
|
|
return true;
|
|
final T test = object;
|
|
final num distance = distanceFunction(test, value);
|
|
if (distance < 0) {
|
|
throw new ArgumentError(
|
|
'Invalid distance function was used to compare a ${value.runtimeType} '
|
|
'to a ${object.runtimeType}. The function must return a non-negative '
|
|
'double value, but it returned $distance.'
|
|
);
|
|
}
|
|
matchState['distance'] = distance;
|
|
return distance <= epsilon;
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) => description.add('$value (±$epsilon)');
|
|
|
|
@override
|
|
Description describeMismatch(
|
|
Object object,
|
|
Description mismatchDescription,
|
|
Map<dynamic, dynamic> matchState,
|
|
bool verbose,
|
|
) {
|
|
mismatchDescription.add('was ${matchState['distance']} away from the desired value.');
|
|
return mismatchDescription;
|
|
}
|
|
}
|
|
|
|
class _MoreOrLessEquals extends Matcher {
|
|
const _MoreOrLessEquals(this.value, this.epsilon);
|
|
|
|
final double value;
|
|
final double epsilon;
|
|
|
|
@override
|
|
bool matches(Object object, Map<dynamic, dynamic> matchState) {
|
|
if (object is! double)
|
|
return false;
|
|
if (object == value)
|
|
return true;
|
|
final double test = object;
|
|
return (test - value).abs() <= epsilon;
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) => description.add('$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 (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 = const _ClipsWithBoundingRect();
|
|
|
|
/// 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 new _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 new _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 new _RendersOnPhysicalModel(
|
|
shape: shape,
|
|
borderRadius: borderRadius,
|
|
elevation: elevation,
|
|
);
|
|
}
|
|
|
|
/// Asserts that a [Finder] locates a single object whose root RenderObject
|
|
/// is [RenderPhysicalShape] that uses a [ShapeBorderClipper] that clips to
|
|
/// [shape] as its clipper.
|
|
/// If [elevation] is non null asserts that [RenderPhysicalShape.elevation] is
|
|
/// equal to [elevation].
|
|
Matcher rendersOnPhysicalShape({
|
|
ShapeBorder shape,
|
|
double elevation,
|
|
}) {
|
|
return new _RendersOnPhysicalShape(
|
|
shape: shape,
|
|
elevation: elevation,
|
|
);
|
|
}
|
|
|
|
abstract class _MatchRenderObject<M extends RenderObject, T extends RenderObject> extends Matcher {
|
|
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);
|
|
|
|
if (renderObject.runtimeType == M)
|
|
return renderObjectMatchesM(matchState, renderObject);
|
|
|
|
return failWithDescription(matchState, 'had a root render object of type: ${renderObject.runtimeType}');
|
|
}
|
|
|
|
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']);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
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;
|
|
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, Null> {
|
|
const _RendersOnPhysicalShape({
|
|
this.shape,
|
|
this.elevation,
|
|
});
|
|
|
|
final ShapeBorder shape;
|
|
final double elevation;
|
|
|
|
@override
|
|
bool renderObjectMatchesM(Map<dynamic, dynamic> matchState, RenderPhysicalShape renderObject) {
|
|
if (renderObject.clipper.runtimeType != ShapeBorderClipper)
|
|
return failWithDescription(matchState, 'clipper was: ${renderObject.clipper}');
|
|
final ShapeBorderClipper shapeClipper = renderObject.clipper;
|
|
|
|
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;
|
|
if (shapeClipper.shape.runtimeType != RoundedRectangleBorder)
|
|
return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
|
|
final RoundedRectangleBorder border = shapeClipper.shape;
|
|
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;
|
|
if (shapeClipper.shape.runtimeType != RoundedRectangleBorder)
|
|
return failWithDescription(matchState, 'shape was: ${shapeClipper.shape}');
|
|
final RoundedRectangleBorder border = shapeClipper.shape;
|
|
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, Null> {
|
|
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;
|
|
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 = new math.Random(1);
|
|
}
|
|
|
|
final Path expectedPath;
|
|
final Rect areaToCompare;
|
|
final int sampleSize;
|
|
final double maxHorizontalNoise;
|
|
final double maxVerticalNoise;
|
|
math.Random random;
|
|
|
|
@override
|
|
bool matches(covariant Path actualPath, Map<dynamic, dynamic> matchState) {
|
|
for (int i = 0; i < sampleSize; i += 1) {
|
|
for (int j = 0; j < sampleSize; j += 1) {
|
|
final Offset offset = new Offset(
|
|
i * (areaToCompare.width / sampleSize),
|
|
j * (areaToCompare.height / sampleSize)
|
|
);
|
|
|
|
if (!_samplePoint(matchState, actualPath, offset))
|
|
return false;
|
|
|
|
final Offset noise = new 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']);
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) =>
|
|
description.add('covers expected area and only expected area');
|
|
}
|
|
|
|
class _MatchesGoldenFile extends AsyncMatcher {
|
|
const _MatchesGoldenFile(this.key);
|
|
|
|
_MatchesGoldenFile.forStringPath(String path) : key = Uri.parse(path);
|
|
|
|
final Uri key;
|
|
|
|
@override
|
|
Future<String> matchAsync(covariant Finder finder) async {
|
|
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';
|
|
}
|
|
final Element element = elements.single;
|
|
|
|
RenderObject renderObject = element.renderObject;
|
|
while (!renderObject.isRepaintBoundary) {
|
|
renderObject = renderObject.parent;
|
|
assert(renderObject != null);
|
|
}
|
|
assert(!renderObject.debugNeedsPaint);
|
|
final OffsetLayer layer = renderObject.layer;
|
|
final Future<ui.Image> imageFuture = layer.toImage(renderObject.paintBounds);
|
|
|
|
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
|
|
return binding.runAsync<String>(() async {
|
|
final ui.Image image = await imageFuture;
|
|
final ByteData bytes = await image.toByteData(format: ui.ImageByteFormat.png)
|
|
.timeout(const Duration(seconds: 10), onTimeout: () => null);
|
|
if (bytes == null)
|
|
return 'Failed to generate screenshot from engine within the 10,000ms timeout.';
|
|
if (autoUpdateGoldenFiles) {
|
|
await goldenFileComparator.update(key, bytes.buffer.asUint8List());
|
|
} else {
|
|
try {
|
|
final bool success = await goldenFileComparator.compare(bytes.buffer.asUint8List(), key);
|
|
return success ? null : 'does not match';
|
|
} on TestFailure catch (ex) {
|
|
return ex.message;
|
|
}
|
|
}
|
|
}, additionalTime: const Duration(seconds: 11));
|
|
}
|
|
|
|
@override
|
|
Description describe(Description description) =>
|
|
description.add('one widget whose rasterized image matches golden image "$key"');
|
|
}
|