diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index cad66d3e003b5..934c9177932d1 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -2272,10 +2272,10 @@ class _MatchesSemanticsData extends Matcher { .toList(); if (expectedActions.isNotEmpty) { - description.add(' with actions: ').addDescriptionOf(expectedActions); + description.add(' with actions: ${_createEnumsSummary(expectedActions)} '); } if (notExpectedActions.isNotEmpty) { - description.add(' without actions: ').addDescriptionOf(notExpectedActions); + description.add(' without actions: ${_createEnumsSummary(notExpectedActions)} '); } } if (flags.isNotEmpty) { @@ -2289,10 +2289,10 @@ class _MatchesSemanticsData extends Matcher { .toList(); if (expectedFlags.isNotEmpty) { - description.add(' with flags: ').addDescriptionOf(expectedFlags); + description.add(' with flags: ${_createEnumsSummary(expectedFlags)} '); } if (notExpectedFlags.isNotEmpty) { - description.add(' without flags: ').addDescriptionOf(notExpectedFlags); + description.add(' without flags: ${_createEnumsSummary(notExpectedFlags)} '); } } if (textDirection != null) { @@ -2434,18 +2434,24 @@ class _MatchesSemanticsData extends Matcher { return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}'); } if (actions.isNotEmpty) { + final List unexpectedActions = []; + final List missingActions = []; for (final MapEntry actionEntry in actions.entries) { final ui.SemanticsAction action = actionEntry.key; final bool actionExpected = actionEntry.value; final bool actionPresent = (action.index & data.actions) == action.index; if (actionPresent != actionExpected) { - final List actionSummary = [ - for (final int action in SemanticsAction.values.keys) - if ((data.actions & action) != 0) describeEnum(action), - ]; - return failWithDescription(matchState, 'actions were: $actionSummary'); + if(actionExpected) { + missingActions.add(action); + } else { + unexpectedActions.add(action); + } } } + + if (unexpectedActions.isNotEmpty || missingActions.isNotEmpty) { + return failWithDescription(matchState, 'missing actions: ${_createEnumsSummary(missingActions)} unexpected actions: ${_createEnumsSummary(unexpectedActions)}'); + } } if (customActions != null || hintOverrides != null) { final List providedCustomActions = data.customSemanticsActionIds?.map((int id) { @@ -2473,18 +2479,24 @@ class _MatchesSemanticsData extends Matcher { } } if (flags.isNotEmpty) { + final List unexpectedFlags = []; + final List missingFlags = []; for (final MapEntry flagEntry in flags.entries) { final ui.SemanticsFlag flag = flagEntry.key; final bool flagExpected = flagEntry.value; final bool flagPresent = flag.index & data.flags == flag.index; if (flagPresent != flagExpected) { - final List flagSummary = [ - for (final int flag in SemanticsFlag.values.keys) - if ((data.flags & flag) != 0) describeEnum(flag), - ]; - return failWithDescription(matchState, 'flags were: $flagSummary'); + if(flagExpected) { + missingFlags.add(flag); + } else { + unexpectedFlags.add(flag); + } } } + + if (unexpectedFlags.isNotEmpty || missingFlags.isNotEmpty) { + return failWithDescription(matchState, 'missing flags: ${_createEnumsSummary(missingFlags)} unexpected flags: ${_createEnumsSummary(unexpectedFlags)}'); + } } bool allMatched = true; if (children != null) { @@ -2512,6 +2524,11 @@ class _MatchesSemanticsData extends Matcher { ) { return mismatchDescription.add(matchState['failure'] as String); } + + static String _createEnumsSummary(List enums) { + assert(T == SemanticsAction || T == SemanticsFlag, 'This method is only intended for lists of SemanticsActions or SemanticsFlags.'); + return '[${enums.map(describeEnum).join(', ')}]'; + } } class _MatchesAccessibilityGuideline extends AsyncMatcher { diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index b4b37b63d3c17..6bbaf5650e4eb 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -733,6 +733,62 @@ void main() { )); handle.dispose(); }); + + testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + addTearDown(() => handle.dispose()); + + const Key key = Key('semantics'); + await tester.pumpWidget(Semantics( + key: key, + namesRoute: true, + header: true, + button: true, + link: true, + onTap: () { }, + onLongPress: () { }, + label: 'foo', + hint: 'bar', + value: 'baz', + increasedValue: 'a', + decreasedValue: 'b', + textDirection: TextDirection.rtl, + onTapHint: 'scan', + onLongPressHint: 'fill', + customSemanticsActions: { + const CustomSemanticsAction(label: 'foo'): () { }, + const CustomSemanticsAction(label: 'bar'): () { }, + }, + )); + + // This should fail due to the mis-match between the `namesRoute` value. + void failedExpectation() => expect(tester.getSemantics(find.byKey(key)), + matchesSemantics( + // Adding the explicit `false` for test readability + // ignore: avoid_redundant_argument_values + namesRoute: false, + label: 'foo', + hint: 'bar', + value: 'baz', + increasedValue: 'a', + decreasedValue: 'b', + textDirection: TextDirection.rtl, + hasTapAction: true, + hasLongPressAction: true, + isButton: true, + isLink: true, + isHeader: true, + onTapHint: 'scan', + onLongPressHint: 'fill', + customActions: [ + const CustomSemanticsAction(label: 'foo'), + const CustomSemanticsAction(label: 'bar'), + ], + ), + ); + + expect(failedExpectation, throwsA(isA())); + }); }); group('containsSemantics', () { @@ -1173,6 +1229,60 @@ void main() { expect(node, containsSemantics(customActions: [action])); }); + + testWidgets('failure does not throw unexpected errors', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + addTearDown(() => handle.dispose()); + + const Key key = Key('semantics'); + await tester.pumpWidget(Semantics( + key: key, + namesRoute: true, + header: true, + button: true, + link: true, + onTap: () { }, + onLongPress: () { }, + label: 'foo', + hint: 'bar', + value: 'baz', + increasedValue: 'a', + decreasedValue: 'b', + textDirection: TextDirection.rtl, + onTapHint: 'scan', + onLongPressHint: 'fill', + customSemanticsActions: { + const CustomSemanticsAction(label: 'foo'): () { }, + const CustomSemanticsAction(label: 'bar'): () { }, + }, + )); + + // This should fail due to the mis-match between the `namesRoute` value. + void failedExpectation() => expect(tester.getSemantics(find.byKey(key)), + containsSemantics( + label: 'foo', + hint: 'bar', + value: 'baz', + increasedValue: 'a', + decreasedValue: 'b', + textDirection: TextDirection.rtl, + hasTapAction: true, + hasLongPressAction: true, + isButton: true, + isLink: true, + isHeader: true, + namesRoute: false, + onTapHint: 'scan', + onLongPressHint: 'fill', + customActions: [ + const CustomSemanticsAction(label: 'foo'), + const CustomSemanticsAction(label: 'bar'), + ], + ), + ); + + expect(failedExpectation, throwsA(isA())); + }); }); group('findsAtLeastNWidgets', () {