diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index 4c49a91c994..389d29c4a51 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -2564,37 +2564,41 @@ class _MatchesSemanticsData extends Matcher { Description describe(Description description, [String? index]) { description.add('${index == null ? '' : 'Child $index '}has semantics'); if (label != null) { - description.add(' with label: $label'); + description.add(' with label: ${_escape(label!)}'); } if (attributedLabel != null) { - description.add(' with attributedLabel: $attributedLabel'); + description.add(' with attributedLabel: ${_escapeAttributedString(attributedLabel!)}'); } if (value != null) { - description.add(' with value: $value'); + description.add(' with value: ${_escape(value!)}'); } if (attributedValue != null) { - description.add(' with attributedValue: $attributedValue'); + description.add(' with attributedValue: ${_escapeAttributedString(attributedValue!)}'); } if (hint != null) { - description.add(' with hint: $hint'); + description.add(' with hint: ${_escape(hint!)}'); } if (attributedHint != null) { - description.add(' with attributedHint: $attributedHint'); + description.add(' with attributedHint: ${_escapeAttributedString(attributedHint!)}'); } if (increasedValue != null) { - description.add(' with increasedValue: $increasedValue '); + description.add(' with increasedValue: ${_escape(increasedValue!)} '); } if (attributedIncreasedValue != null) { - description.add(' with attributedIncreasedValue: $attributedIncreasedValue'); + description.add( + ' with attributedIncreasedValue: ${_escapeAttributedString(attributedIncreasedValue!)}', + ); } if (decreasedValue != null) { - description.add(' with decreasedValue: $decreasedValue '); + description.add(' with decreasedValue: ${_escape(decreasedValue!)} '); } if (attributedDecreasedValue != null) { - description.add(' with attributedDecreasedValue: $attributedDecreasedValue'); + description.add( + ' with attributedDecreasedValue: ${_escapeAttributedString(attributedDecreasedValue!)}', + ); } if (tooltip != null) { - description.add(' with tooltip: $tooltip'); + description.add(' with tooltip: ${_escape(tooltip!)}'); } if (inputType != null) { description.add(' with inputType: $inputType'); @@ -2695,6 +2699,27 @@ class _MatchesSemanticsData extends Matcher { return true; } + static String _escape(String string) => string.replaceAll('\u202f', r'\u202f'); + + static AttributedString _escapeAttributedString(AttributedString attributedString) { + return AttributedString( + _escape(attributedString.string), + attributes: attributedString.attributes, + ); + } + + bool _checkStringMismatch(Map matchState, String prefixText, String actual) { + return failWithDescription(matchState, '$prefixText was ${_escape(actual)}'); + } + + bool _checkStringAttributeMismatch( + Map matchState, + String prefixText, + AttributedString actual, + ) { + return failWithDescription(matchState, '$prefixText was: ${_escapeAttributedString(actual)}'); + } + @override bool matches(dynamic node, Map matchState) { if (node == null) { @@ -2712,7 +2737,7 @@ class _MatchesSemanticsData extends Matcher { }; if (label != null && label != data.label) { - return failWithDescription(matchState, 'label was: ${data.label}'); + return _checkStringMismatch(matchState, 'label', data.label); } if (attributedLabel != null && (attributedLabel!.string != data.attributedLabel.string || @@ -2720,18 +2745,18 @@ class _MatchesSemanticsData extends Matcher { attributedLabel!.attributes, data.attributedLabel.attributes, ))) { - return failWithDescription(matchState, 'attributedLabel was: ${data.attributedLabel}'); + return _checkStringAttributeMismatch(matchState, 'attributedLabel', data.attributedLabel); } if (hint != null && hint != data.hint) { - return failWithDescription(matchState, 'hint was: ${data.hint}'); + return _checkStringMismatch(matchState, 'hint', data.hint); } if (attributedHint != null && (attributedHint!.string != data.attributedHint.string || !_stringAttributesEqual(attributedHint!.attributes, data.attributedHint.attributes))) { - return failWithDescription(matchState, 'attributedHint was: ${data.attributedHint}'); + return _checkStringAttributeMismatch(matchState, 'attributedHint', data.attributedHint); } if (value != null && value != data.value) { - return failWithDescription(matchState, 'value was: ${data.value}'); + return _checkStringMismatch(matchState, 'value', data.value); } if (attributedValue != null && (attributedValue!.string != data.attributedValue.string || @@ -2739,10 +2764,10 @@ class _MatchesSemanticsData extends Matcher { attributedValue!.attributes, data.attributedValue.attributes, ))) { - return failWithDescription(matchState, 'attributedValue was: ${data.attributedValue}'); + return _checkStringAttributeMismatch(matchState, 'attributedValue', data.attributedValue); } if (increasedValue != null && increasedValue != data.increasedValue) { - return failWithDescription(matchState, 'increasedValue was: ${data.increasedValue}'); + return _checkStringMismatch(matchState, 'increasedValue', data.increasedValue); } if (attributedIncreasedValue != null && (attributedIncreasedValue!.string != data.attributedIncreasedValue.string || @@ -2750,13 +2775,14 @@ class _MatchesSemanticsData extends Matcher { attributedIncreasedValue!.attributes, data.attributedIncreasedValue.attributes, ))) { - return failWithDescription( + return _checkStringAttributeMismatch( matchState, - 'attributedIncreasedValue was: ${data.attributedIncreasedValue}', + 'attributedIncreasedValue', + data.attributedIncreasedValue, ); } if (decreasedValue != null && decreasedValue != data.decreasedValue) { - return failWithDescription(matchState, 'decreasedValue was: ${data.decreasedValue}'); + return _checkStringMismatch(matchState, 'decreasedValue', data.decreasedValue); } if (attributedDecreasedValue != null && (attributedDecreasedValue!.string != data.attributedDecreasedValue.string || @@ -2764,13 +2790,14 @@ class _MatchesSemanticsData extends Matcher { attributedDecreasedValue!.attributes, data.attributedDecreasedValue.attributes, ))) { - return failWithDescription( + return _checkStringAttributeMismatch( matchState, - 'attributedDecreasedValue was: ${data.attributedDecreasedValue}', + 'attributedDecreasedValue', + data.attributedDecreasedValue, ); } if (tooltip != null && tooltip != data.tooltip) { - return failWithDescription(matchState, 'tooltip was: ${data.tooltip}'); + return _checkStringMismatch(matchState, 'tooltip', data.tooltip); } if (textDirection != null && textDirection != data.textDirection) { return failWithDescription(matchState, 'textDirection was: $textDirection'); diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index 0e6b42b9aa8..c8eabeb8212 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -913,6 +913,364 @@ void main() { expect(failedExpectation, throwsA(isA())); handle.dispose(); }); + + testWidgets('matchesSemantics captures NBSP in failure descriptions', ( + WidgetTester tester, + ) async { + final data = SemanticsData( + flagsCollection: SemanticsFlags.none, + actions: 0, + identifier: '', + traversalChildIdentifier: '', + traversalParentIdentifier: '', + attributedLabel: AttributedString('label\u202f0'), + attributedHint: AttributedString('hint\u202f0'), + attributedValue: AttributedString('value\u202f0'), + attributedIncreasedValue: AttributedString('increasedValue\u202f0'), + attributedDecreasedValue: AttributedString('decreasedValue\u202f0'), + tooltip: 'tooltip\u202f0', + textDirection: TextDirection.ltr, + rect: Rect.zero, + textSelection: null, + scrollIndex: null, + scrollChildCount: null, + scrollPosition: null, + scrollExtentMax: null, + scrollExtentMin: null, + platformViewId: 0, + maxValueLength: 0, + currentValueLength: 0, + headingLevel: 0, + linkUrl: null, + role: ui.SemanticsRole.none, + controlsNodes: null, + validationResult: SemanticsValidationResult.none, + inputType: ui.SemanticsInputType.none, + locale: null, + hitTestBehavior: ui.SemanticsHitTestBehavior.defer, + ); + final node = _FakeSemanticsNode(data); + + // Label + expect( + () => expect(node, matchesSemantics(label: 'label 0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r'label was label\u202f0'), + ), + ), + ); + expect( + () => expect(node, matchesSemantics(attributedLabel: AttributedString('label 0'))), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r"attributedLabel was: AttributedString('label\u202f0'"), + ), + ), + ); + + // Hint + expect( + () => expect(node, matchesSemantics(hint: 'hint 0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r'hint was hint\u202f0'), + ), + ), + ); + expect( + () => expect(node, matchesSemantics(attributedHint: AttributedString('hint 0'))), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r"attributedHint was: AttributedString('hint\u202f0'"), + ), + ), + ); + + // Value + expect( + () => expect(node, matchesSemantics(value: 'value 0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r'value was value\u202f0'), + ), + ), + ); + expect( + () => expect(node, matchesSemantics(attributedValue: AttributedString('value 0'))), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r"attributedValue was: AttributedString('value\u202f0'"), + ), + ), + ); + + // Increased Value + expect( + () => expect(node, matchesSemantics(increasedValue: 'increasedValue 0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r'increasedValue was increasedValue\u202f0'), + ), + ), + ); + expect( + () => expect( + node, + matchesSemantics(attributedIncreasedValue: AttributedString('increasedValue 0')), + ), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r"attributedIncreasedValue was: AttributedString('increasedValue\u202f0'"), + ), + ), + ); + + // Decreased Value + expect( + () => expect(node, matchesSemantics(decreasedValue: 'decreasedValue 0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r'decreasedValue was decreasedValue\u202f0'), + ), + ), + ); + expect( + () => expect( + node, + matchesSemantics(attributedDecreasedValue: AttributedString('decreasedValue 0')), + ), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r"attributedDecreasedValue was: AttributedString('decreasedValue\u202f0'"), + ), + ), + ); + + // Tooltip + expect( + () => expect(node, matchesSemantics(tooltip: 'tooltip 0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + contains(r'tooltip was tooltip\u202f0'), + ), + ), + ); + }); + + testWidgets( + 'matchesSemantics shows correct error message when actuals have normal space but expects have NBSP', + (WidgetTester tester) async { + final data = SemanticsData( + flagsCollection: SemanticsFlags.none, + actions: 0, + identifier: '', + traversalChildIdentifier: '', + traversalParentIdentifier: '', + attributedLabel: AttributedString('label 0'), + attributedHint: AttributedString('hint 0'), + attributedValue: AttributedString('value 0'), + attributedIncreasedValue: AttributedString('increasedValue 0'), + attributedDecreasedValue: AttributedString('decreasedValue 0'), + tooltip: 'tooltip 0', + textDirection: TextDirection.ltr, + rect: Rect.zero, + textSelection: null, + scrollIndex: null, + scrollChildCount: null, + scrollPosition: null, + scrollExtentMax: null, + scrollExtentMin: null, + platformViewId: 0, + maxValueLength: 0, + currentValueLength: 0, + headingLevel: 0, + linkUrl: null, + role: ui.SemanticsRole.none, + controlsNodes: null, + validationResult: SemanticsValidationResult.none, + inputType: ui.SemanticsInputType.none, + locale: null, + hitTestBehavior: ui.SemanticsHitTestBehavior.defer, + ); + final node = _FakeSemanticsNode(data); + + // Label + expect( + () => expect(node, matchesSemantics(label: 'label\u202f0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf(contains(r'with label: label\u202f0'), contains(r'label was label 0')), + ), + ), + ); + expect( + () => expect(node, matchesSemantics(attributedLabel: AttributedString('label\u202f0'))), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf( + contains(r"with attributedLabel: AttributedString('label\u202f0'"), + contains(r"attributedLabel was: AttributedString('label 0'"), + ), + ), + ), + ); + + // Hint + expect( + () => expect(node, matchesSemantics(hint: 'hint\u202f0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf(contains(r'with hint: hint\u202f0'), contains(r'hint was hint 0')), + ), + ), + ); + expect( + () => expect(node, matchesSemantics(attributedHint: AttributedString('hint\u202f0'))), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf( + contains(r"with attributedHint: AttributedString('hint\u202f0'"), + contains(r"attributedHint was: AttributedString('hint 0'"), + ), + ), + ), + ); + + // Value + expect( + () => expect(node, matchesSemantics(value: 'value\u202f0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf(contains(r'with value: value\u202f0'), contains(r'value was value 0')), + ), + ), + ); + expect( + () => expect(node, matchesSemantics(attributedValue: AttributedString('value\u202f0'))), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf( + contains(r"with attributedValue: AttributedString('value\u202f0'"), + contains(r"attributedValue was: AttributedString('value 0'"), + ), + ), + ), + ); + + // Increased Value + expect( + () => expect(node, matchesSemantics(increasedValue: 'increasedValue\u202f0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf( + contains(r'with increasedValue: increasedValue\u202f0'), + contains(r'increasedValue was increasedValue 0'), + ), + ), + ), + ); + expect( + () => expect( + node, + matchesSemantics(attributedIncreasedValue: AttributedString('increasedValue\u202f0')), + ), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf( + contains( + r"with attributedIncreasedValue: AttributedString('increasedValue\u202f0'", + ), + contains(r"attributedIncreasedValue was: AttributedString('increasedValue 0'"), + ), + ), + ), + ); + + // Decreased Value + expect( + () => expect(node, matchesSemantics(decreasedValue: 'decreasedValue\u202f0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf( + contains(r'with decreasedValue: decreasedValue\u202f0'), + contains(r'decreasedValue was decreasedValue 0'), + ), + ), + ), + ); + expect( + () => expect( + node, + matchesSemantics(attributedDecreasedValue: AttributedString('decreasedValue\u202f0')), + ), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf( + contains( + r"with attributedDecreasedValue: AttributedString('decreasedValue\u202f0'", + ), + contains(r"attributedDecreasedValue was: AttributedString('decreasedValue 0'"), + ), + ), + ), + ); + + // Tooltip + expect( + () => expect(node, matchesSemantics(tooltip: 'tooltip\u202f0')), + throwsA( + isA().having( + (TestFailure e) => e.message, + 'message', + allOf(contains(r'with tooltip: tooltip\u202f0'), contains(r'tooltip was tooltip 0')), + ), + ), + ); + }, + ); }); group('containsSemantics', () {