diff --git a/packages/flutter/test/widgets/keep_alive_test.dart b/packages/flutter/test/widgets/keep_alive_test.dart index 2c5ec19b1ba..65696736321 100644 --- a/packages/flutter/test/widgets/keep_alive_test.dart +++ b/packages/flutter/test/widgets/keep_alive_test.dart @@ -212,13 +212,12 @@ void main() { ), ); // The important lines below are the ones marked with "<----" - expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( + expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info, wrapWidth: 600), equalsIgnoringHashCodes( '_ReusableRenderView#00000\n' ' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n' - ' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in\n' - ' │ logical pixels)\n' + ' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in logical pixels)\n' ' │\n' ' └─child: RenderRepaintBoundary#00000\n' ' │ needs compositing\n' @@ -227,8 +226,7 @@ void main() { ' │ layer: OffsetLayer#00000\n' ' │ size: Size(800.0, 600.0)\n' ' │ metrics: 0.0% useful (1 bad vs 0 good)\n' - ' │ diagnosis: insufficient data to draw conclusion (less than five\n' - ' │ repaints)\n' + ' │ diagnosis: insufficient data to draw conclusion (less than five repaints)\n' ' │\n' ' └─child: RenderCustomPaint#00000\n' ' │ needs compositing\n' @@ -236,10 +234,7 @@ void main() { ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' ' │ painter: null\n' - ' │ foregroundPainter:\n' - ' │ _GlowingOverscrollIndicatorPainter(_GlowController(color:\n' - ' │ Color(0xffffffff), axis: vertical), _GlowController(color:\n' - ' │ Color(0xffffffff), axis: vertical))\n' + ' │ foregroundPainter: _GlowingOverscrollIndicatorPainter(_GlowController(color: ${const Color(0xffffffff)}, axis: vertical), _GlowController(color: ${const Color(0xffffffff)}, axis: vertical))\n' ' │\n' ' └─child: RenderRepaintBoundary#00000\n' ' │ needs compositing\n' @@ -248,8 +243,7 @@ void main() { ' │ layer: OffsetLayer#00000\n' ' │ size: Size(800.0, 600.0)\n' ' │ metrics: 0.0% useful (1 bad vs 0 good)\n' - ' │ diagnosis: insufficient data to draw conclusion (less than five\n' - ' │ repaints)\n' + ' │ diagnosis: insufficient data to draw conclusion (less than five repaints)\n' ' │\n' ' └─child: _RenderScrollSemantics#00000\n' ' │ needs compositing\n' @@ -305,38 +299,20 @@ void main() { ' │ size: Size(800.0, 600.0)\n' ' │ axisDirection: down\n' ' │ crossAxisDirection: right\n' - ' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range:\n' - ' │ 0.0..39400.0, viewport: 600.0, ScrollableState,\n' - ' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics ->\n' - ' │ RangeMaintainingScrollPhysics, IdleScrollActivity#00000,\n' - ' │ ScrollDirection.idle)\n' + ' │ offset: ScrollPositionWithSingleContext#00000(offset: 0.0, range: 0.0..39400.0, viewport: 600.0, ScrollableState, AlwaysScrollableScrollPhysics -> ClampingScrollPhysics -> RangeMaintainingScrollPhysics, IdleScrollActivity#00000, ScrollDirection.idle)\n' ' │ anchor: 0.0\n' ' │\n' ' └─center child: RenderSliverPadding#00000 relayoutBoundary=up1\n' ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n' - ' │ constraints: SliverConstraints(AxisDirection.down,\n' - ' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n' - ' │ 0.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0,\n' - ' │ crossAxisExtent: 800.0, crossAxisDirection:\n' - ' │ AxisDirection.right, viewportMainAxisExtent: 600.0,\n' - ' │ remainingCacheExtent: 850.0, cacheOrigin: 0.0)\n' - ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n' - ' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n' - ' │ cacheExtent: 850.0)\n' + ' │ constraints: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.idle, scrollOffset: 0.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0, cacheOrigin: 0.0)\n' + ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent: 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, cacheExtent: 850.0)\n' ' │ padding: EdgeInsets.zero\n' ' │ textDirection: ltr\n' ' │\n' ' └─child: RenderSliverFixedExtentList#00000 relayoutBoundary=up2\n' ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n' - ' │ constraints: SliverConstraints(AxisDirection.down,\n' - ' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n' - ' │ 0.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0,\n' - ' │ crossAxisExtent: 800.0, crossAxisDirection:\n' - ' │ AxisDirection.right, viewportMainAxisExtent: 600.0,\n' - ' │ remainingCacheExtent: 850.0, cacheOrigin: 0.0)\n' - ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n' - ' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n' - ' │ cacheExtent: 850.0)\n' + ' │ constraints: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.idle, scrollOffset: 0.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 600.0, remainingCacheExtent: 850.0, cacheOrigin: 0.0)\n' + ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent: 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, cacheExtent: 850.0)\n' ' │ currently live children: 0 to 2\n' ' │\n' ' ├─child with index 0: RenderLimitedBox#00000\n' @@ -387,13 +363,12 @@ void main() { const GlobalObjectKey<_LeafState>(3).currentState!.setKeepAlive(true); await tester.drag(find.byType(ListView), const Offset(0.0, -1000.0)); await tester.pump(); - expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( + expect(tester.binding.renderView.toStringDeep(minLevel: DiagnosticLevel.info, wrapWidth: 600), equalsIgnoringHashCodes( '_ReusableRenderView#00000\n' ' │ debug mode enabled - ${Platform.operatingSystem}\n' ' │ view size: Size(2400.0, 1800.0) (in physical pixels)\n' ' │ device pixel ratio: 3.0 (physical pixels per logical pixel)\n' - ' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in\n' - ' │ logical pixels)\n' + ' │ configuration: BoxConstraints(w=800.0, h=600.0) at 3.0x (in logical pixels)\n' ' │\n' ' └─child: RenderRepaintBoundary#00000\n' ' │ needs compositing\n' @@ -402,8 +377,7 @@ void main() { ' │ layer: OffsetLayer#00000\n' ' │ size: Size(800.0, 600.0)\n' ' │ metrics: 0.0% useful (1 bad vs 0 good)\n' - ' │ diagnosis: insufficient data to draw conclusion (less than five\n' - ' │ repaints)\n' + ' │ diagnosis: insufficient data to draw conclusion (less than five repaints)\n' ' │\n' ' └─child: RenderCustomPaint#00000\n' ' │ needs compositing\n' @@ -411,10 +385,7 @@ void main() { ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' ' │ painter: null\n' - ' │ foregroundPainter:\n' - ' │ _GlowingOverscrollIndicatorPainter(_GlowController(color:\n' - ' │ Color(0xffffffff), axis: vertical), _GlowController(color:\n' - ' │ Color(0xffffffff), axis: vertical))\n' + ' │ foregroundPainter: _GlowingOverscrollIndicatorPainter(_GlowController(color: ${const Color(0xffffffff)}, axis: vertical), _GlowController(color: ${const Color(0xffffffff)}, axis: vertical))\n' ' │\n' ' └─child: RenderRepaintBoundary#00000\n' ' │ needs compositing\n' @@ -423,8 +394,7 @@ void main() { ' │ layer: OffsetLayer#00000\n' ' │ size: Size(800.0, 600.0)\n' ' │ metrics: 0.0% useful (1 bad vs 0 good)\n' - ' │ diagnosis: insufficient data to draw conclusion (less than five\n' - ' │ repaints)\n' + ' │ diagnosis: insufficient data to draw conclusion (less than five repaints)\n' ' │\n' ' └─child: _RenderScrollSemantics#00000\n' ' │ needs compositing\n' @@ -480,38 +450,20 @@ void main() { ' │ size: Size(800.0, 600.0)\n' ' │ axisDirection: down\n' ' │ crossAxisDirection: right\n' - ' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0,\n' - ' │ range: 0.0..39400.0, viewport: 600.0, ScrollableState,\n' - ' │ AlwaysScrollableScrollPhysics -> ClampingScrollPhysics ->\n' - ' │ RangeMaintainingScrollPhysics, IdleScrollActivity#00000,\n' - ' │ ScrollDirection.idle)\n' + ' │ offset: ScrollPositionWithSingleContext#00000(offset: 2000.0, range: 0.0..39400.0, viewport: 600.0, ScrollableState, AlwaysScrollableScrollPhysics -> ClampingScrollPhysics -> RangeMaintainingScrollPhysics, IdleScrollActivity#00000, ScrollDirection.idle)\n' ' │ anchor: 0.0\n' ' │\n' ' └─center child: RenderSliverPadding#00000 relayoutBoundary=up1\n' ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n' - ' │ constraints: SliverConstraints(AxisDirection.down,\n' - ' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n' - ' │ 2000.0, precedingScrollExtent: 0.0, remainingPaintExtent:\n' - ' │ 600.0, crossAxisExtent: 800.0, crossAxisDirection:\n' - ' │ AxisDirection.right, viewportMainAxisExtent: 600.0,\n' - ' │ remainingCacheExtent: 1100.0, cacheOrigin: -250.0)\n' - ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n' - ' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n' - ' │ cacheExtent: 1100.0)\n' + ' │ constraints: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.idle, scrollOffset: 2000.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 600.0, remainingCacheExtent: 1100.0, cacheOrigin: -250.0)\n' + ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent: 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, cacheExtent: 1100.0)\n' ' │ padding: EdgeInsets.zero\n' ' │ textDirection: ltr\n' ' │\n' ' └─child: RenderSliverFixedExtentList#00000 relayoutBoundary=up2\n' ' │ parentData: paintOffset=Offset(0.0, 0.0) (can use size)\n' - ' │ constraints: SliverConstraints(AxisDirection.down,\n' - ' │ GrowthDirection.forward, ScrollDirection.idle, scrollOffset:\n' - ' │ 2000.0, precedingScrollExtent: 0.0, remainingPaintExtent:\n' - ' │ 600.0, crossAxisExtent: 800.0, crossAxisDirection:\n' - ' │ AxisDirection.right, viewportMainAxisExtent: 600.0,\n' - ' │ remainingCacheExtent: 1100.0, cacheOrigin: -250.0)\n' - ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent:\n' - ' │ 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true,\n' - ' │ cacheExtent: 1100.0)\n' + ' │ constraints: SliverConstraints(AxisDirection.down, GrowthDirection.forward, ScrollDirection.idle, scrollOffset: 2000.0, precedingScrollExtent: 0.0, remainingPaintExtent: 600.0, crossAxisExtent: 800.0, crossAxisDirection: AxisDirection.right, viewportMainAxisExtent: 600.0, remainingCacheExtent: 1100.0, cacheOrigin: -250.0)\n' + ' │ geometry: SliverGeometry(scrollExtent: 40000.0, paintExtent: 600.0, maxPaintExtent: 40000.0, hasVisualOverflow: true, cacheExtent: 1100.0)\n' ' │ currently live children: 4 to 7\n' ' │\n' ' ├─child with index 4: RenderLimitedBox#00000 NEEDS-PAINT\n' diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 48a0f3726c0..3bbb16f8ccb 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -5,6 +5,7 @@ /// @docImport '_goldens_io.dart'; library; +import 'dart:convert' show LineSplitter; import 'dart:math' as math; import 'dart:ui' as ui; @@ -1198,19 +1199,24 @@ class _HasOneLineDescription extends Matcher { } class _EqualsIgnoringHashCodes extends Matcher { - _EqualsIgnoringHashCodes(Object v) : _value = _normalize(v); + _EqualsIgnoringHashCodes(Object v) + : _value = _normalize(v), + _stringValue = v is String ? _normalizeString(v) : null; - final Object _value; + final Iterable _value; + final String? _stringValue; - static final Object _mismatchedValueKey = Object(); + static final Object _lineNumberValueKey = Object(); + static final Object _expectedLineValueKey = Object(); + static final Object _seenLineValueKey = Object(); static String _normalizeString(String value) { return value.replaceAll(RegExp(r'#[\da-fA-F]{5}'), '#00000'); } - static Object _normalize(Object value, {bool expected = true}) { + static Iterable _normalize(Object value, {bool expected = true}) { if (value is String) { - return _normalizeString(value); + return LineSplitter.split(value).map((dynamic item) => _normalizeString(item.toString())); } if (value is Iterable) { return value.map((dynamic item) => _normalizeString(item.toString())); @@ -1222,20 +1228,33 @@ class _EqualsIgnoringHashCodes extends Matcher { @override bool matches(dynamic object, Map matchState) { - final Object normalized = _normalize(object as Object, expected: false); - if (!equals(_value).matches(normalized, matchState)) { - matchState[_mismatchedValueKey] = normalized; - return false; + final Iterable normalized = _normalize(object as Object, expected: false); + final Iterator expectedIt = _value.iterator; + final Iterator seenIt = normalized.iterator; + + int lineNumber = 1; + + bool hasExpected = expectedIt.moveNext(); + bool hasSeen = seenIt.moveNext(); + while (hasExpected && hasSeen) { + if (!equals(expectedIt.current).matches(seenIt.current, matchState)) { + matchState[_lineNumberValueKey] = lineNumber; + matchState[_expectedLineValueKey] = expectedIt.current; + matchState[_seenLineValueKey] = seenIt.current; + return false; + } + + lineNumber += 1; + hasExpected = expectedIt.moveNext(); + hasSeen = seenIt.moveNext(); } - return true; + + return !hasExpected && !hasSeen; } @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); + return description.add('normalized value matches\n').addDescriptionOf(_stringValue ?? _value); } @override @@ -1245,16 +1264,17 @@ class _EqualsIgnoringHashCodes extends Matcher { Map 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) + if (matchState.containsKey(_lineNumberValueKey) && + matchState.containsKey(_expectedLineValueKey) && + matchState.containsKey(_seenLineValueKey)) { + final int lineNumber = matchState[_lineNumberValueKey] as int; + if (lineNumber > 1) { + mismatchDescription = mismatchDescription + .add('Lines $lineNumber differed, expected: \n') + .addDescriptionOf(matchState[_expectedLineValueKey]) .add('\nbut got\n') - .addDescriptionOf(actualValue); + .addDescriptionOf(matchState[_seenLineValueKey]); + } } return mismatchDescription; } diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index 2f22abb23c2..7f8a7063896 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -194,6 +194,26 @@ void main() { ); }); + test('equalsIgnoringHashCodes - wrong line', () { + TestFailure? failure; + try { + expect( + '1\n2\n3\n4\n5\n6\n7\n8\n9\n10', + equalsIgnoringHashCodes('1\n2\n3\n4\n5\n6\na\n8\n9\n10'), + ); + } on TestFailure catch (e) { + failure = e; + } + + expect(failure, isNotNull); + if (failure != null) { + final String? message = failure.message; + expect(message, contains('Lines 7 differed')); + expect(message, contains("'a'")); + expect(message, contains("'7'")); + } + }); + test('moreOrLessEquals', () { expect(0.0, moreOrLessEquals(1e-11)); expect(1e-11, moreOrLessEquals(0.0));