From 08e122f466ea6b49c266bb09869618d9e50a0119 Mon Sep 17 00:00:00 2001 From: Qun Cheng <36861262+QuncCccccc@users.noreply.github.com> Date: Mon, 22 Dec 2025 13:04:06 -0800 Subject: [PATCH] Add error description for nbsp character(\u202f) (#178895) This PR is to add a more clear error description when there is a special white space NNBSP(\u202f). This whitespace character is used to connect time and "am"/"pm". In this PR, I only updated the description in `_MatchesSemanticsData` and still not sure if it's necessary to create another custom matcher class to ignore special whitespace. I think it's valid to directly update users' test results since the expected results have indeed changed. This is my experiment for the whitespace change when we update generated_date_localization.dart: cl/831625146 I went through all Scuba diffs and all results look expected. Once this is landed, we can use the CL change (except the image change) as g3fix to update the date localizations. Fixes https://github.com/flutter/flutter/issues/177479 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. --- packages/flutter_test/lib/src/matchers.dart | 75 ++-- packages/flutter_test/test/matchers_test.dart | 358 ++++++++++++++++++ 2 files changed, 409 insertions(+), 24 deletions(-) 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', () {