From a75f003b9ca0e4584f6375154d59a710e08d36e1 Mon Sep 17 00:00:00 2001 From: Yegor Date: Wed, 8 Nov 2017 13:36:16 -0800 Subject: [PATCH] add `within` matcher for comparing metric-space values (#12908) --- packages/flutter/test/material/arc_test.dart | 8 +- .../flutter/test/material/material_test.dart | 12 +-- .../flutter/test/widgets/heroes_test.dart | 12 +-- packages/flutter_test/lib/src/matchers.dart | 102 ++++++++++++++++++ packages/flutter_test/test/matchers_test.dart | 30 ++++++ 5 files changed, 145 insertions(+), 19 deletions(-) diff --git a/packages/flutter/test/material/arc_test.dart b/packages/flutter/test/material/arc_test.dart index 8f9b30c425d..51f98a41436 100644 --- a/packages/flutter/test/material/arc_test.dart +++ b/packages/flutter/test/material/arc_test.dart @@ -73,14 +73,14 @@ void main() { MaterialPointArcTween tween = new MaterialPointArcTween(begin: begin, end: end); 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.75) - const Offset(48.0, 196.0)).distance, closeTo(0.0, 2.0)); + expect(tween.lerp(0.25), within(distance: 2.0, from: const Offset(126.0, 120.0))); + expect(tween.lerp(0.75), within(distance: 2.0, from: const Offset(48.0, 196.0))); expect(tween.lerp(1.0), end); tween = new MaterialPointArcTween(begin: end, end: begin); 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.75) - const Offset(168.3, 163.8)).distance, closeTo(0.0, 2.0)); + expect(tween.lerp(0.25), within(distance: 2.0, from: const Offset(91.0, 239.0))); + expect(tween.lerp(0.75), within(distance: 2.0, from: const Offset(168.3, 163.8))); expect(tween.lerp(1.0), begin); }); diff --git a/packages/flutter/test/material/material_test.dart b/packages/flutter/test/material/material_test.dart index ddd9f4f54d2..2aa31ee378b 100644 --- a/packages/flutter/test/material/material_test.dart +++ b/packages/flutter/test/material/material_test.dart @@ -142,7 +142,7 @@ void main() { }); 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. await tester.pumpWidget(buildMaterial(shadowColor: const Color(0xFF00FF00))); @@ -155,17 +155,11 @@ void main() { await tester.pump(const Duration(milliseconds: 1)); final RenderPhysicalModel modelC = getShadow(tester); - expect(modelC.shadowColor.alpha, equals(0xFF)); - expect(modelC.shadowColor.red, closeTo(0x00, 1)); - expect(modelC.shadowColor.green, closeTo(0xFF, 1)); - expect(modelC.shadowColor.blue, equals(0x00)); + expect(modelC.shadowColor, within(distance: 1, from: const Color(0xFF00FF00))); await tester.pump(kThemeChangeDuration ~/ 2); final RenderPhysicalModel modelD = getShadow(tester); - expect(modelD.shadowColor.alpha, equals(0xFF)); - expect(modelD.shadowColor.red, isNot(closeTo(0x00, 1))); - expect(modelD.shadowColor.green, isNot(closeTo(0xFF, 1))); - expect(modelD.shadowColor.blue, equals(0x00)); + expect(modelD.shadowColor, isNot(within(distance: 1, from: const Color(0xFF00FF00)))); await tester.pump(kThemeChangeDuration); final RenderPhysicalModel modelE = getShadow(tester); diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 631cb750b8f..b334951afe0 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -1104,17 +1104,17 @@ void main() { await tester.pump(duration * 0.25); Offset actualHeroCenter = tester.getCenter(find.byKey(secondKey)); Offset predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.25)); - expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); + expect(actualHeroCenter, within(distance: epsilon, from: predictedHeroCenter)); await tester.pump(duration * 0.25); actualHeroCenter = tester.getCenter(find.byKey(secondKey)); predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.5)); - expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); + expect(actualHeroCenter, within(distance: epsilon, from: predictedHeroCenter)); await tester.pump(duration * 0.25); actualHeroCenter = tester.getCenter(find.byKey(secondKey)); predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.75)); - expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); + expect(actualHeroCenter, within(distance: epsilon, from: predictedHeroCenter)); await tester.pumpAndSettle(); expect(tester.getCenter(find.byKey(secondKey)), const Offset(400.0, 300.0)); @@ -1135,17 +1135,17 @@ void main() { await tester.pump(duration * 0.25); actualHeroCenter = tester.getCenter(find.byKey(firstKey)); predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.25)); - expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); + expect(actualHeroCenter, within(distance: epsilon, from: predictedHeroCenter)); await tester.pump(duration * 0.25); actualHeroCenter = tester.getCenter(find.byKey(firstKey)); predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.5)); - expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); + expect(actualHeroCenter, within(distance: epsilon, from: predictedHeroCenter)); await tester.pump(duration * 0.25); actualHeroCenter = tester.getCenter(find.byKey(firstKey)); predictedHeroCenter = popCenterTween.lerp(curve.flipped.transform(0.75)); - expect((actualHeroCenter - predictedHeroCenter).distance, closeTo(0.0, epsilon)); + expect(actualHeroCenter, within(distance: epsilon, from: predictedHeroCenter)); await tester.pumpAndSettle(); expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0)); diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 5ac536939b9..a78ae1ec1b8 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -2,7 +2,10 @@ // 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 'package:flutter/material.dart'; +import 'package:meta/meta.dart'; import 'package:test/test.dart'; import 'finders.dart'; @@ -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 a, T b); + +const Map> _kStandardDistanceFunctions = const >{ + 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((a.red - b.red).abs(), (a.green - b.green).abs()); + delta = math.max(delta, (a.blue - b.blue).abs()); + delta = math.max(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({ + @required num distance, + @required T from, + DistanceFunction 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(distanceFunction, from, distance); +} + +class _IsWithinDistance extends Matcher { + const _IsWithinDistance(this.distanceFunction, this.value, this.epsilon); + + final DistanceFunction distanceFunction; + final T value; + final num epsilon; + + @override + bool matches(Object object, Map 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 { const _MoreOrLessEquals(this.value, this.epsilon); diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index 391b7713a3d..f01376aced0 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter_test/flutter_test.dart'; /// Class that makes it easy to mock common toStringDeep behavior. @@ -179,4 +181,32 @@ void main() { expect(11.0, moreOrLessEquals(-11.0, epsilon: 100.0)); expect(-11.0, moreOrLessEquals(11.0, epsilon: 100.0)); }); + + test('within', () { + expect(0.0, within(distance: 0.1, from: 0.05)); + expect(0.0, isNot(within(distance: 0.1, from: 0.2))); + + expect(0, within(distance: 1, from: 1)); + expect(0, isNot(within(distance: 1, from: 2))); + + expect(const Color(0x00000000), within(distance: 1, from: const Color(0x01000000))); + expect(const Color(0x00000000), within(distance: 1, from: const Color(0x00010000))); + expect(const Color(0x00000000), within(distance: 1, from: const Color(0x00000100))); + expect(const Color(0x00000000), within(distance: 1, from: const Color(0x00000001))); + expect(const Color(0x00000000), within(distance: 1, from: const Color(0x01010101))); + expect(const Color(0x00000000), isNot(within(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(distance: 1, from: false), + throwsArgumentError, + ); + + expect( + () => within(distance: 1, from: 2, distanceFunction: (int a, int b) => -1).matches(1, {}), + throwsArgumentError, + ); + }); }