From f7c41d0988dec34471e595c7f48cd5f84b4012f9 Mon Sep 17 00:00:00 2001 From: Greg Spencer Date: Wed, 3 Aug 2022 12:48:06 -0700 Subject: [PATCH] Update `equalsIgnoringHashCodes` to take a list of Strings (#108507) --- .../test/material/checkbox_theme_test.dart | 19 +++--- .../flutter/test/material/list_tile_test.dart | 53 +++++++++------- .../test/material/list_tile_theme_test.dart | 34 +++++----- .../test/material/radio_theme_test.dart | 17 +++-- .../flutter/test/widgets/actions_test.dart | 18 ++++-- packages/flutter_test/lib/src/matchers.dart | 63 ++++++++++++------- packages/flutter_test/test/matchers_test.dart | 20 +++++- 7 files changed, 144 insertions(+), 80 deletions(-) diff --git a/packages/flutter/test/material/checkbox_theme_test.dart b/packages/flutter/test/material/checkbox_theme_test.dart index c180dacc1554..b57ce852e9f6 100644 --- a/packages/flutter/test/material/checkbox_theme_test.dart +++ b/packages/flutter/test/material/checkbox_theme_test.dart @@ -64,13 +64,18 @@ void main() { .map((DiagnosticsNode node) => node.toString()) .toList(); - expect(description[0], 'mouseCursor: MaterialStatePropertyAll(SystemMouseCursor(click))'); - expect(description[1], 'fillColor: MaterialStatePropertyAll(Color(0xfffffff0))'); - expect(description[2], 'checkColor: MaterialStatePropertyAll(Color(0xfffffff1))'); - expect(description[3], 'overlayColor: MaterialStatePropertyAll(Color(0xfffffff2))'); - expect(description[4], 'splashRadius: 1.0'); - expect(description[5], 'materialTapTargetSize: MaterialTapTargetSize.shrinkWrap'); - expect(description[6], equalsIgnoringHashCodes('visualDensity: VisualDensity#00000(h: 0.0, v: 0.0)')); + expect( + description, + equalsIgnoringHashCodes([ + 'mouseCursor: MaterialStatePropertyAll(SystemMouseCursor(click))', + 'fillColor: MaterialStatePropertyAll(Color(0xfffffff0))', + 'checkColor: MaterialStatePropertyAll(Color(0xfffffff1))', + 'overlayColor: MaterialStatePropertyAll(Color(0xfffffff2))', + 'splashRadius: 1.0', + 'materialTapTargetSize: MaterialTapTargetSize.shrinkWrap', + 'visualDensity: VisualDensity#00000(h: 0.0, v: 0.0)', + ]), + ); }); testWidgets('Checkbox is themeable', (WidgetTester tester) async { diff --git a/packages/flutter/test/material/list_tile_test.dart b/packages/flutter/test/material/list_tile_test.dart index e54352736245..8aaca6f21117 100644 --- a/packages/flutter/test/material/list_tile_test.dart +++ b/packages/flutter/test/material/list_tile_test.dart @@ -2276,30 +2276,35 @@ void main() { .map((DiagnosticsNode node) => node.toString()) .toList(); - expect(description[0], 'leading: Text'); - expect(description[1], 'title: Text'); - expect(description[2], 'subtitle: Text'); - expect(description[3], 'trailing: Text'); - expect(description[4], 'isThreeLine: THREE_LINE'); - expect(description[5], 'dense: true'); - expect(description[6], equalsIgnoringHashCodes('visualDensity: VisualDensity#00000(h: 0.0, v: 0.0)')); - expect(description[7], 'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.zero)'); - expect(description[8], 'style: ListTileStyle.list'); - expect(description[9], 'selectedColor: Color(0xff0000ff)'); - expect(description[10], 'iconColor: Color(0xff00ff00)'); - expect(description[11], 'textColor: Color(0xffff0000)'); - expect(description[12], 'contentPadding: EdgeInsets.zero'); - expect(description[13], 'enabled: false'); - expect(description[14], 'selected: true'); - expect(description[15], 'focusColor: Color(0xff00ffff)'); - expect(description[16], 'hoverColor: Color(0xff0000ff)'); - expect(description[17], 'autofocus: true'); - expect(description[18], 'tileColor: Color(0xffffff00)'); - expect(description[19], 'selectedTileColor: Color(0xff123456)'); - expect(description[20], 'enableFeedback: false'); - expect(description[21], 'horizontalTitleGap: 4.0'); - expect(description[22], 'minVerticalPadding: 2.0'); - expect(description[23], 'minLeadingWidth: 6.0'); + expect( + description, + equalsIgnoringHashCodes([ + 'leading: Text', + 'title: Text', + 'subtitle: Text', + 'trailing: Text', + 'isThreeLine: THREE_LINE', + 'dense: true', + 'visualDensity: VisualDensity#00000(h: 0.0, v: 0.0)', + 'shape: RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.zero)', + 'style: ListTileStyle.list', + 'selectedColor: Color(0xff0000ff)', + 'iconColor: Color(0xff00ff00)', + 'textColor: Color(0xffff0000)', + 'contentPadding: EdgeInsets.zero', + 'enabled: false', + 'selected: true', + 'focusColor: Color(0xff00ffff)', + 'hoverColor: Color(0xff0000ff)', + 'autofocus: true', + 'tileColor: Color(0xffffff00)', + 'selectedTileColor: Color(0xff123456)', + 'enableFeedback: false', + 'horizontalTitleGap: 4.0', + 'minVerticalPadding: 2.0', + 'minLeadingWidth: 6.0', + ]), + ); }); group('Material 2', () { diff --git a/packages/flutter/test/material/list_tile_theme_test.dart b/packages/flutter/test/material/list_tile_theme_test.dart index 5a64101e76fc..bed87072fdee 100644 --- a/packages/flutter/test/material/list_tile_theme_test.dart +++ b/packages/flutter/test/material/list_tile_theme_test.dart @@ -107,23 +107,25 @@ void main() { .map((DiagnosticsNode node) => node.toString()) .toList(); - expect(description[0], 'dense: true'); - expect(description[1], 'shape: StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none))'); - expect(description[2], 'style: drawer'); - expect(description[3], 'selectedColor: Color(0x00000001)'); - expect(description[4], 'iconColor: Color(0x00000002)'); - expect(description[5], 'textColor: Color(0x00000003)'); - expect(description[6], 'contentPadding: EdgeInsets.all(100.0)'); - expect(description[7], 'tileColor: Color(0x00000004)'); - expect(description[8], 'selectedTileColor: Color(0x00000005)'); - expect(description[9], 'horizontalTitleGap: 200.0'); - expect(description[10], 'minVerticalPadding: 300.0'); - expect(description[11], 'minLeadingWidth: 400.0'); - expect(description[12], 'enableFeedback: true'); - expect(description[13], 'mouseCursor: MaterialStateMouseCursor(clickable)'); expect( - description[14], - equalsIgnoringHashCodes('visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)'), + description, + equalsIgnoringHashCodes([ + 'dense: true', + 'shape: StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none))', + 'style: drawer', + 'selectedColor: Color(0x00000001)', + 'iconColor: Color(0x00000002)', + 'textColor: Color(0x00000003)', + 'contentPadding: EdgeInsets.all(100.0)', + 'tileColor: Color(0x00000004)', + 'selectedTileColor: Color(0x00000005)', + 'horizontalTitleGap: 200.0', + 'minVerticalPadding: 300.0', + 'minLeadingWidth: 400.0', + 'enableFeedback: true', + 'mouseCursor: MaterialStateMouseCursor(clickable)', + 'visualDensity: VisualDensity#00000(h: -1.0, v: -1.0)(horizontal: -1.0, vertical: -1.0)', + ]), ); }); diff --git a/packages/flutter/test/material/radio_theme_test.dart b/packages/flutter/test/material/radio_theme_test.dart index f3a5f76e501b..ab53ff8c8894 100644 --- a/packages/flutter/test/material/radio_theme_test.dart +++ b/packages/flutter/test/material/radio_theme_test.dart @@ -61,12 +61,17 @@ void main() { .map((DiagnosticsNode node) => node.toString()) .toList(); - expect(description[0], 'mouseCursor: MaterialStatePropertyAll(SystemMouseCursor(click))'); - expect(description[1], 'fillColor: MaterialStatePropertyAll(Color(0xfffffff0))'); - expect(description[2], 'overlayColor: MaterialStatePropertyAll(Color(0xfffffff1))'); - expect(description[3], 'splashRadius: 1.0'); - expect(description[4], 'materialTapTargetSize: MaterialTapTargetSize.shrinkWrap'); - expect(description[5], equalsIgnoringHashCodes('visualDensity: VisualDensity#00000(h: 0.0, v: 0.0)')); + expect( + description, + equalsIgnoringHashCodes([ + 'mouseCursor: MaterialStatePropertyAll(SystemMouseCursor(click))', + 'fillColor: MaterialStatePropertyAll(Color(0xfffffff0))', + 'overlayColor: MaterialStatePropertyAll(Color(0xfffffff1))', + 'splashRadius: 1.0', + 'materialTapTargetSize: MaterialTapTargetSize.shrinkWrap', + 'visualDensity: VisualDensity#00000(h: 0.0, v: 0.0)', + ]), + ); }); testWidgets('Radio is themeable', (WidgetTester tester) async { diff --git a/packages/flutter/test/widgets/actions_test.dart b/packages/flutter/test/widgets/actions_test.dart index 24d1f6fbb8b2..26de85156ab8 100644 --- a/packages/flutter/test/widgets/actions_test.dart +++ b/packages/flutter/test/widgets/actions_test.dart @@ -1009,8 +1009,13 @@ void main() { .toList(); expect(description.length, equals(2)); - expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000')); - expect(description[1], equals('actions: {}')); + expect( + description, + equalsIgnoringHashCodes([ + 'dispatcher: ActionDispatcher#00000', + 'actions: {}', + ]), + ); }); testWidgets('Actions implements debugFillProperties', (WidgetTester tester) async { final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); @@ -1032,8 +1037,13 @@ void main() { .toList(); expect(description.length, equals(2)); - expect(description[0], equalsIgnoringHashCodes('dispatcher: ActionDispatcher#00000')); - expect(description[1], equalsIgnoringHashCodes('actions: {TestIntent: TestAction#00000}')); + expect( + description, + equalsIgnoringHashCodes([ + 'dispatcher: ActionDispatcher#00000', + 'actions: {TestIntent: TestAction#00000}', + ]), + ); }); }); diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index cb9932f894e8..0d069761aef8 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -292,11 +292,14 @@ Matcher offsetMoreOrLessEquals(Offset value, { double epsilon = precisionErrorTo return _IsWithinDistance(_offsetDistance, value, epsilon); } -/// Asserts that two [String]s are equal after normalizing likely hash codes. +/// Asserts that two [String]s or `Iterable`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`. /// +/// Only [String] or `Iterable` are allowed types for `value`. +/// /// See Also: /// /// * [describeIdentity], a method that generates short descriptions of objects @@ -305,7 +308,8 @@ Matcher offsetMoreOrLessEquals(Offset value, { double epsilon = precisionErrorTo /// [String] based on [Object.hashCode]. /// * [DiagnosticableTree.toStringDeep], a method that returns a [String] /// typically containing multiple hash codes. -Matcher equalsIgnoringHashCodes(String value) { +Matcher equalsIgnoringHashCodes(Object value) { + assert(value is String || value is Iterable, "Only String or Iterable are allowed types for equalsIgnoringHashCodes, it doesn't accept ${value.runtimeType}"); return _EqualsIgnoringHashCodes(value); } @@ -1056,21 +1060,33 @@ class _HasOneLineDescription extends Matcher { } class _EqualsIgnoringHashCodes extends Matcher { - _EqualsIgnoringHashCodes(String v) : _value = _normalize(v); + _EqualsIgnoringHashCodes(Object v) : _value = _normalize(v); - final String _value; + final Object _value; static final Object _mismatchedValueKey = Object(); - static String _normalize(String s) { - return s.replaceAll(RegExp(r'#[0-9a-fA-F]{5}'), '#00000'); + static String _normalizeString(String value) { + return value.replaceAll(RegExp(r'#[\da-fA-F]{5}'), '#00000'); + } + + static Object _normalize(Object value, {bool expected = true}) { + if (value is String) { + return _normalizeString(value); + } + if (value is Iterable) { + return value.map((dynamic item) => _normalizeString(item.toString())); + } + throw ArgumentError('The specified ${expected ? 'expected' : 'comparison'} value for ' + 'equalsIgnoringHashCodes must be a String or an Iterable, ' + 'not a ${value.runtimeType}'); } @override bool matches(dynamic object, Map matchState) { - final String description = _normalize(object as String); - if (_value != description) { - matchState[_mismatchedValueKey] = description; + final Object normalized = _normalize(object as Object, expected: false); + if (!equals(_value).matches(normalized, matchState)) { + matchState[_mismatchedValueKey] = normalized; return false; } return true; @@ -1078,7 +1094,10 @@ class _EqualsIgnoringHashCodes extends Matcher { @override Description describe(Description description) { - return description.add('multi line description equals $_value'); + if (_value is String) { + return description.add('normalized value matches $_value'); + } + return description.add('normalized value matches\n').addDescriptionOf(_value); } @override @@ -1089,14 +1108,14 @@ class _EqualsIgnoringHashCodes extends Matcher { bool verbose, ) { if (matchState.containsKey(_mismatchedValueKey)) { - final String actualValue = matchState[_mismatchedValueKey] as String; + 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('expected normalized value\n ') + .add('was expected to be normalized value\n') .addDescriptionOf(_value) - .add('\nbut got\n ') + .add('\nbut got\n') .addDescriptionOf(actualValue); } return mismatchDescription; @@ -1164,11 +1183,11 @@ class _HasGoodToStringDeep extends Matcher { for (int i = 0; i < lines.length; ++i) { final String line = lines[i]; if (line.isEmpty) { - issues.add('Line ${i+1} is empty.'); + issues.add('Line ${i + 1} is empty.'); } if (line.trimRight() != line) { - issues.add('Line ${i+1} has trailing whitespace.'); + issues.add('Line ${i + 1} has trailing whitespace.'); } } @@ -1179,11 +1198,11 @@ class _HasGoodToStringDeep extends Matcher { // 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 prefixLineOne = 'PREFIX_LINE_ONE____'; const String prefixOtherLines = 'PREFIX_OTHER_LINES_'; final List prefixIssues = []; - String descriptionWithPrefixes = - object.toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines) as String; // ignore: avoid_dynamic_calls + // ignore: avoid_dynamic_calls + String descriptionWithPrefixes = object.toStringDeep(prefixLineOne: prefixLineOne, prefixOtherLines: prefixOtherLines) as String; if (descriptionWithPrefixes.endsWith('\n')) { // Trim off trailing \n as the remaining calculations assume // the description does not end with a trailing \n. @@ -1197,7 +1216,7 @@ class _HasGoodToStringDeep extends Matcher { for (int i = 1; i < linesWithPrefixes.length; ++i) { if (!linesWithPrefixes[i].startsWith(prefixOtherLines)) { - prefixIssues.add('Line ${i+1} does not contain the expected prefix.'); + prefixIssues.add('Line ${i + 1} does not contain the expected prefix.'); } } @@ -1979,9 +1998,9 @@ int _countDifferentPixels(Uint8List imageA, Uint8List imageB) { int delta = 0; for (int i = 0; i < imageA.length; i+=4) { if (imageA[i] != imageB[i] || - imageA[i+1] != imageB[i+1] || - imageA[i+2] != imageB[i+2] || - imageA[i+3] != imageB[i+3]) { + imageA[i + 1] != imageB[i + 1] || + imageA[i + 2] != imageB[i + 2] || + imageA[i + 3] != imageB[i + 3]) { delta++; } } diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index 5e6f3d786621..b4b37b63d3c1 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -135,7 +135,7 @@ void main() { ); }); - test('normalizeHashCodesEquals', () { + test('equalsIgnoringHashCodes', () { expect('Foo#34219', equalsIgnoringHashCodes('Foo#00000')); expect('Foo#34219', equalsIgnoringHashCodes('Foo#12345')); expect('Foo#34219', equalsIgnoringHashCodes('Foo#abcdf')); @@ -173,6 +173,24 @@ void main() { expect('Foo#', isNot(equalsIgnoringHashCodes('Foo#00000'))); expect('Foo#3421', isNot(equalsIgnoringHashCodes('Foo#00000'))); expect('Foo#342193', isNot(equalsIgnoringHashCodes('Foo#00000'))); + expect(['Foo#a3b4d'], equalsIgnoringHashCodes(['Foo#12345'])); + expect( + ['Foo#a3b4d', 'Foo#12345'], + equalsIgnoringHashCodes(['Foo#00000', 'Foo#00000']), + ); + expect( + ['Foo#a3b4d', 'Bar#12345'], + equalsIgnoringHashCodes(['Foo#00000', 'Bar#00000']), + ); + expect( + ['Foo#a3b4d', 'Bar#12345'], + isNot(equalsIgnoringHashCodes(['Bar#00000', 'Foo#00000'])), + ); + expect(['Foo#a3b4d'], isNot(equalsIgnoringHashCodes(['Foo']))); + expect( + ['Foo#a3b4d'], + isNot(equalsIgnoringHashCodes(['Foo#00000', 'Bar#00000'])), + ); }); test('moreOrLessEquals', () {