Unverified Commit a75f003b authored by Yegor's avatar Yegor Committed by GitHub

add `within` matcher for comparing metric-space values (#12908)

parent b04b89ac
...@@ -73,14 +73,14 @@ void main() { ...@@ -73,14 +73,14 @@ void main() {
MaterialPointArcTween tween = new MaterialPointArcTween(begin: begin, end: end); MaterialPointArcTween tween = new MaterialPointArcTween(begin: begin, end: end);
expect(tween.lerp(0.0), begin); expect(tween.lerp(0.0), begin);
expect((tween.lerp(0.25) - const Offset(126.0, 120.0)).distance, closeTo(0.0, 2.0)); expect(tween.lerp(0.25), within<Offset>(distance: 2.0, from: const Offset(126.0, 120.0)));
expect((tween.lerp(0.75) - const Offset(48.0, 196.0)).distance, closeTo(0.0, 2.0)); expect(tween.lerp(0.75), within<Offset>(distance: 2.0, from: const Offset(48.0, 196.0)));
expect(tween.lerp(1.0), end); expect(tween.lerp(1.0), end);
tween = new MaterialPointArcTween(begin: end, end: begin); tween = new MaterialPointArcTween(begin: end, end: begin);
expect(tween.lerp(0.0), end); expect(tween.lerp(0.0), end);
expect((tween.lerp(0.25) - const Offset(91.0, 239.0)).distance, closeTo(0.0, 2.0)); expect(tween.lerp(0.25), within<Offset>(distance: 2.0, from: const Offset(91.0, 239.0)));
expect((tween.lerp(0.75) - const Offset(168.3, 163.8)).distance, closeTo(0.0, 2.0)); expect(tween.lerp(0.75), within<Offset>(distance: 2.0, from: const Offset(168.3, 163.8)));
expect(tween.lerp(1.0), begin); expect(tween.lerp(1.0), begin);
}); });
......
...@@ -142,7 +142,7 @@ void main() { ...@@ -142,7 +142,7 @@ void main() {
}); });
testWidgets('Shadow colors animate smoothly', (WidgetTester tester) async { testWidgets('Shadow colors animate smoothly', (WidgetTester tester) async {
// This code verifies that the PhysicalModel's elevation animates over // This code verifies that the PhysicalModel's shadowColor animates over
// a kThemeChangeDuration time interval. // a kThemeChangeDuration time interval.
await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFF00FF00))); await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFF00FF00)));
...@@ -155,17 +155,11 @@ void main() { ...@@ -155,17 +155,11 @@ void main() {
await tester.pump(const Duration(milliseconds: 1)); await tester.pump(const Duration(milliseconds: 1));
final RenderPhysicalModel modelC = getShadow(tester); final RenderPhysicalModel modelC = getShadow(tester);
expect(modelC.shadowColor.alpha, equals(0xFF)); expect(modelC.shadowColor, within<Color>(distance: 1, from: const Color(0xFF00FF00)));
expect(modelC.shadowColor.red, closeTo(0x00, 1));
expect(modelC.shadowColor.green, closeTo(0xFF, 1));
expect(modelC.shadowColor.blue, equals(0x00));
await tester.pump(kThemeChangeDuration ~/ 2); await tester.pump(kThemeChangeDuration ~/ 2);
final RenderPhysicalModel modelD = getShadow(tester); final RenderPhysicalModel modelD = getShadow(tester);
expect(modelD.shadowColor.alpha, equals(0xFF)); expect(modelD.shadowColor, isNot(within<Color>(distance: 1, from: const Color(0xFF00FF00))));
expect(modelD.shadowColor.red, isNot(closeTo(0x00, 1)));
expect(modelD.shadowColor.green, isNot(closeTo(0xFF, 1)));
expect(modelD.shadowColor.blue, equals(0x00));
await tester.pump(kThemeChangeDuration); await tester.pump(kThemeChangeDuration);
final RenderPhysicalModel modelE = getShadow(tester); final RenderPhysicalModel modelE = getShadow(tester);
......
...@@ -1104,17 +1104,17 @@ void main() { ...@@ -1104,17 +1104,17 @@ void main() {
await tester.pump(duration * 0.25); await tester.pump(duration * 0.25);
Offset actualHeroCenter = tester.getCenter(find.byKey(secondKey)); Offset actualHeroCenter = tester.getCenter(find.byKey(secondKey));
Offset predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.25)); Offset predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.25));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
await tester.pump(duration * 0.25); await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(secondKey)); actualHeroCenter = tester.getCenter(find.byKey(secondKey));
predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.5)); predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.5));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
await tester.pump(duration * 0.25); await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(secondKey)); actualHeroCenter = tester.getCenter(find.byKey(secondKey));
predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.75)); predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.75));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.getCenter(find.byKey(secondKey)), const Offset(400.0, 300.0)); expect(tester.getCenter(find.byKey(secondKey)), const Offset(400.0, 300.0));
...@@ -1135,17 +1135,17 @@ void main() { ...@@ -1135,17 +1135,17 @@ void main() {
await tester.pump(duration * 0.25); await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(firstKey)); actualHeroCenter = tester.getCenter(find.byKey(firstKey));
predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.25)); predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.25));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
await tester.pump(duration * 0.25); await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(firstKey)); actualHeroCenter = tester.getCenter(find.byKey(firstKey));
predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.5)); predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.5));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
await tester.pump(duration * 0.25); await tester.pump(duration * 0.25);
actualHeroCenter = tester.getCenter(find.byKey(firstKey)); actualHeroCenter = tester.getCenter(find.byKey(firstKey));
predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.75)); predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.75));
expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0)); expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0));
......
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'finders.dart'; import 'finders.dart';
...@@ -565,6 +568,105 @@ class _HasGoodToStringDeep extends Matcher { ...@@ -565,6 +568,105 @@ class _HasGoodToStringDeep extends Matcher {
} }
} }
/// 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);
const Map<Type, DistanceFunction<dynamic>> _kStandardDistanceFunctions = const <Type, DistanceFunction<dynamic>>{
Color: _maxComponentColorDistance,
Offset: _offsetDistance,
int: _intDistance,
double: _doubleDistance,
};
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();
}
/// 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].
/// * [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.'
);
}
return distance <= epsilon;
}
@override
Description describe(Description description) => description.add('$value$epsilon)');
}
class _MoreOrLessEquals extends Matcher { class _MoreOrLessEquals extends Matcher {
const _MoreOrLessEquals(this.value, this.epsilon); const _MoreOrLessEquals(this.value, this.epsilon);
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
/// Class that makes it easy to mock common toStringDeep behavior. /// Class that makes it easy to mock common toStringDeep behavior.
...@@ -179,4 +181,32 @@ void main() { ...@@ -179,4 +181,32 @@ void main() {
expect(11.0, moreOrLessEquals(-11.0, epsilon: 100.0)); expect(11.0, moreOrLessEquals(-11.0, epsilon: 100.0));
expect(-11.0, moreOrLessEquals(11.0, epsilon: 100.0)); expect(-11.0, moreOrLessEquals(11.0, epsilon: 100.0));
}); });
test('within', () {
expect(0.0, within<double>(distance: 0.1, from: 0.05));
expect(0.0, isNot(within<double>(distance: 0.1, from: 0.2)));
expect(0, within<int>(distance: 1, from: 1));
expect(0, isNot(within<int>(distance: 1, from: 2)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x01000000)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x00010000)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x00000100)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x00000001)));
expect(const Color(0x00000000), within<Color>(distance: 1, from: const Color(0x01010101)));
expect(const Color(0x00000000), isNot(within<Color>(distance: 1, from: const Color(0x02000000))));
expect(const Offset(1.0, 0.0), within(distance: 1.0, from: const Offset(0.0, 0.0)));
expect(const Offset(1.0, 0.0), isNot(within(distance: 1.0, from: const Offset(-1.0, 0.0))));
expect(
() => within<bool>(distance: 1, from: false),
throwsArgumentError,
);
expect(
() => within<int>(distance: 1, from: 2, distanceFunction: (int a, int b) => -1).matches(1, <dynamic, dynamic>{}),
throwsArgumentError,
);
});
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment