diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index b674584e984..df411d063ef 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -224,6 +224,18 @@ 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); + class _FindsWidgetMatcher extends Matcher { const _FindsWidgetMatcher(this.min, this.max); @@ -1090,3 +1102,75 @@ class _ClipsWithShapeBorder extends _MatchRenderObject { 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 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 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 matchState, String description) { + matchState['failure'] = description; + return false; + } + + @override + Description describeMismatch( + dynamic item, + Description mismatchDescription, + Map matchState, + bool verbose + ) { + return mismatchDescription.add(matchState['failure']); + } + + @override + Description describe(Description description) => + description.add('covers expected area and only expected area'); +} diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index f01376aced0..11d8159586c 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -209,4 +209,77 @@ void main() { throwsArgumentError, ); }); + + group('coversSameAreaAs', () { + test('empty Paths', () { + expect( + new Path(), + coversSameAreaAs( + new Path(), + areaToCompare: new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0) + ), + ); + }); + + test('mismatch', () { + final Path rectPath = new Path() + ..addRect(new Rect.fromLTRB(5.0, 5.0, 6.0, 6.0)); + expect( + new Path(), + isNot(coversSameAreaAs( + rectPath, + areaToCompare: new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0) + )), + ); + }); + + test('mismatch out of examined area', () { + final Path rectPath = new Path() + ..addRect(new Rect.fromLTRB(5.0, 5.0, 6.0, 6.0)); + rectPath.addRect(new Rect.fromLTRB(5.0, 5.0, 6.0, 6.0)); + expect( + new Path(), + coversSameAreaAs( + rectPath, + areaToCompare: new Rect.fromLTRB(0.0, 0.0, 4.0, 4.0) + ), + ); + }); + + test('differently constructed rects match', () { + final Path rectPath = new Path() + ..addRect(new Rect.fromLTRB(5.0, 5.0, 6.0, 6.0)); + final Path linePath = new Path() + ..moveTo(5.0, 5.0) + ..lineTo(5.0, 6.0) + ..lineTo(6.0, 6.0) + ..lineTo(6.0, 5.0) + ..close(); + expect( + linePath, + coversSameAreaAs( + rectPath, + areaToCompare: new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0) + ), + ); + }); + + test('partially overlapping paths', () { + final Path rectPath = new Path() + ..addRect(new Rect.fromLTRB(5.0, 5.0, 6.0, 6.0)); + final Path linePath = new Path() + ..moveTo(5.0, 5.0) + ..lineTo(5.0, 6.0) + ..lineTo(6.0, 6.0) + ..lineTo(6.0, 5.5) + ..close(); + expect( + linePath, + isNot(coversSameAreaAs( + rectPath, + areaToCompare: new Rect.fromLTRB(0.0, 0.0, 10.0, 10.0) + )), + ); + }); + }); }