diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index 47fd86e703329..0f9817356bef7 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -2315,7 +2315,7 @@ class SemanticsNode extends AbstractNode with DiagnosticableTreeMixin { ); assert( !_canPerformAction(SemanticsAction.decrease) || (value == '') == (decreasedValue == ''), - 'A SemanticsNode with action "increase" needs to be annotated with either both "value" and "decreasedValue" or neither', + 'A SemanticsNode with action "decrease" needs to be annotated with either both "value" and "decreasedValue" or neither', ); } diff --git a/packages/flutter_test/lib/src/matchers.dart b/packages/flutter_test/lib/src/matchers.dart index c50db35456414..cb9932f894e8a 100644 --- a/packages/flutter_test/lib/src/matchers.dart +++ b/packages/flutter_test/lib/src/matchers.dart @@ -514,6 +514,7 @@ AsyncMatcher matchesReferenceImage(ui.Image image) { /// See also: /// /// * [WidgetTester.getSemantics], the tester method which retrieves semantics. +/// * [containsSemantics], a similar matcher without default values for flags or actions. Matcher matchesSemantics({ String? label, AttributedString? attributedLabel, @@ -524,8 +525,8 @@ Matcher matchesSemantics({ String? increasedValue, AttributedString? attributedIncreasedValue, String? decreasedValue, - String? tooltip, AttributedString? attributedDecreasedValue, + String? tooltip, TextDirection? textDirection, Rect? rect, Size? size, @@ -588,67 +589,6 @@ Matcher matchesSemantics({ List? customActions, List? children, }) { - final List flags = [ - if (hasCheckedState) SemanticsFlag.hasCheckedState, - if (isChecked) SemanticsFlag.isChecked, - if (isSelected) SemanticsFlag.isSelected, - if (isButton) SemanticsFlag.isButton, - if (isSlider) SemanticsFlag.isSlider, - if (isKeyboardKey) SemanticsFlag.isKeyboardKey, - if (isLink) SemanticsFlag.isLink, - if (isTextField) SemanticsFlag.isTextField, - if (isReadOnly) SemanticsFlag.isReadOnly, - if (isFocused) SemanticsFlag.isFocused, - if (isFocusable) SemanticsFlag.isFocusable, - if (hasEnabledState) SemanticsFlag.hasEnabledState, - if (isEnabled) SemanticsFlag.isEnabled, - if (isInMutuallyExclusiveGroup) SemanticsFlag.isInMutuallyExclusiveGroup, - if (isHeader) SemanticsFlag.isHeader, - if (isObscured) SemanticsFlag.isObscured, - if (isMultiline) SemanticsFlag.isMultiline, - if (namesRoute) SemanticsFlag.namesRoute, - if (scopesRoute) SemanticsFlag.scopesRoute, - if (isHidden) SemanticsFlag.isHidden, - if (isImage) SemanticsFlag.isImage, - if (isLiveRegion) SemanticsFlag.isLiveRegion, - if (hasToggledState) SemanticsFlag.hasToggledState, - if (isToggled) SemanticsFlag.isToggled, - if (hasImplicitScrolling) SemanticsFlag.hasImplicitScrolling, - if (isSlider) SemanticsFlag.isSlider, - ]; - - final List actions = [ - if (hasTapAction) SemanticsAction.tap, - if (hasLongPressAction) SemanticsAction.longPress, - if (hasScrollLeftAction) SemanticsAction.scrollLeft, - if (hasScrollRightAction) SemanticsAction.scrollRight, - if (hasScrollUpAction) SemanticsAction.scrollUp, - if (hasScrollDownAction) SemanticsAction.scrollDown, - if (hasIncreaseAction) SemanticsAction.increase, - if (hasDecreaseAction) SemanticsAction.decrease, - if (hasShowOnScreenAction) SemanticsAction.showOnScreen, - if (hasMoveCursorForwardByCharacterAction) SemanticsAction.moveCursorForwardByCharacter, - if (hasMoveCursorBackwardByCharacterAction) SemanticsAction.moveCursorBackwardByCharacter, - if (hasSetSelectionAction) SemanticsAction.setSelection, - if (hasCopyAction) SemanticsAction.copy, - if (hasCutAction) SemanticsAction.cut, - if (hasPasteAction) SemanticsAction.paste, - if (hasDidGainAccessibilityFocusAction) SemanticsAction.didGainAccessibilityFocus, - if (hasDidLoseAccessibilityFocusAction) SemanticsAction.didLoseAccessibilityFocus, - if (customActions != null && customActions.isNotEmpty) SemanticsAction.customAction, - if (hasDismissAction) SemanticsAction.dismiss, - if (hasMoveCursorForwardByWordAction) SemanticsAction.moveCursorForwardByWord, - if (hasMoveCursorBackwardByWordAction) SemanticsAction.moveCursorBackwardByWord, - if (hasSetTextAction) SemanticsAction.setText, - ]; - SemanticsHintOverrides? hintOverrides; - if (onTapHint != null || onLongPressHint != null) { - hintOverrides = SemanticsHintOverrides( - onTapHint: onTapHint, - onLongPressHint: onLongPressHint, - ); - } - return _MatchesSemanticsData( label: label, attributedLabel: attributedLabel, @@ -657,12 +597,10 @@ Matcher matchesSemantics({ value: value, attributedValue: attributedValue, increasedValue: increasedValue, - tooltip: tooltip, attributedIncreasedValue: attributedIncreasedValue, decreasedValue: decreasedValue, attributedDecreasedValue: attributedDecreasedValue, - actions: actions, - flags: flags, + tooltip: tooltip, textDirection: textDirection, rect: rect, size: size, @@ -670,10 +608,231 @@ Matcher matchesSemantics({ thickness: thickness, platformViewId: platformViewId, customActions: customActions, - hintOverrides: hintOverrides, + maxValueLength: maxValueLength, currentValueLength: currentValueLength, + // Flags + hasCheckedState: hasCheckedState, + isChecked: isChecked, + isSelected: isSelected, + isButton: isButton, + isSlider: isSlider, + isKeyboardKey: isKeyboardKey, + isLink: isLink, + isFocused: isFocused, + isFocusable: isFocusable, + isTextField: isTextField, + isReadOnly: isReadOnly, + hasEnabledState: hasEnabledState, + isEnabled: isEnabled, + isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup, + isHeader: isHeader, + isObscured: isObscured, + isMultiline: isMultiline, + namesRoute: namesRoute, + scopesRoute: scopesRoute, + isHidden: isHidden, + isImage: isImage, + isLiveRegion: isLiveRegion, + hasToggledState: hasToggledState, + isToggled: isToggled, + hasImplicitScrolling: hasImplicitScrolling, + // Actions + hasTapAction: hasTapAction, + hasLongPressAction: hasLongPressAction, + hasScrollLeftAction: hasScrollLeftAction, + hasScrollRightAction: hasScrollRightAction, + hasScrollUpAction: hasScrollUpAction, + hasScrollDownAction: hasScrollDownAction, + hasIncreaseAction: hasIncreaseAction, + hasDecreaseAction: hasDecreaseAction, + hasShowOnScreenAction: hasShowOnScreenAction, + hasMoveCursorForwardByCharacterAction: hasMoveCursorForwardByCharacterAction, + hasMoveCursorBackwardByCharacterAction: hasMoveCursorBackwardByCharacterAction, + hasMoveCursorForwardByWordAction: hasMoveCursorForwardByWordAction, + hasMoveCursorBackwardByWordAction: hasMoveCursorBackwardByWordAction, + hasSetTextAction: hasSetTextAction, + hasSetSelectionAction: hasSetSelectionAction, + hasCopyAction: hasCopyAction, + hasCutAction: hasCutAction, + hasPasteAction: hasPasteAction, + hasDidGainAccessibilityFocusAction: hasDidGainAccessibilityFocusAction, + hasDidLoseAccessibilityFocusAction: hasDidLoseAccessibilityFocusAction, + hasDismissAction: hasDismissAction, + // Custom actions and overrides + children: children, + onLongPressHint: onLongPressHint, + onTapHint: onTapHint, + ); +} + +/// Asserts that a [SemanticsNode] contains the specified information. +/// +/// There are no default expected values, so no unspecified values will be +/// validated. +/// +/// To retrieve the semantics data of a widget, use [WidgetTester.getSemantics] +/// with a [Finder] that returns a single widget. Semantics must be enabled +/// in order to use this method. +/// +/// ## Sample code +/// +/// ```dart +/// final SemanticsHandle handle = tester.ensureSemantics(); +/// expect(tester.getSemantics(find.text('hello')), hasSemantics(label: 'hello')); +/// handle.dispose(); +/// ``` +/// +/// See also: +/// +/// * [WidgetTester.getSemantics], the tester method which retrieves semantics. +/// * [matchesSemantics], a similar matcher with default values for flags and actions. +Matcher containsSemantics({ + String? label, + AttributedString? attributedLabel, + String? hint, + AttributedString? attributedHint, + String? value, + AttributedString? attributedValue, + String? increasedValue, + AttributedString? attributedIncreasedValue, + String? decreasedValue, + AttributedString? attributedDecreasedValue, + String? tooltip, + TextDirection? textDirection, + Rect? rect, + Size? size, + double? elevation, + double? thickness, + int? platformViewId, + int? maxValueLength, + int? currentValueLength, + // Flags + bool? hasCheckedState, + bool? isChecked, + bool? isSelected, + bool? isButton, + bool? isSlider, + bool? isKeyboardKey, + bool? isLink, + bool? isFocused, + bool? isFocusable, + bool? isTextField, + bool? isReadOnly, + bool? hasEnabledState, + bool? isEnabled, + bool? isInMutuallyExclusiveGroup, + bool? isHeader, + bool? isObscured, + bool? isMultiline, + bool? namesRoute, + bool? scopesRoute, + bool? isHidden, + bool? isImage, + bool? isLiveRegion, + bool? hasToggledState, + bool? isToggled, + bool? hasImplicitScrolling, + // Actions + bool? hasTapAction, + bool? hasLongPressAction, + bool? hasScrollLeftAction, + bool? hasScrollRightAction, + bool? hasScrollUpAction, + bool? hasScrollDownAction, + bool? hasIncreaseAction, + bool? hasDecreaseAction, + bool? hasShowOnScreenAction, + bool? hasMoveCursorForwardByCharacterAction, + bool? hasMoveCursorBackwardByCharacterAction, + bool? hasMoveCursorForwardByWordAction, + bool? hasMoveCursorBackwardByWordAction, + bool? hasSetTextAction, + bool? hasSetSelectionAction, + bool? hasCopyAction, + bool? hasCutAction, + bool? hasPasteAction, + bool? hasDidGainAccessibilityFocusAction, + bool? hasDidLoseAccessibilityFocusAction, + bool? hasDismissAction, + // Custom actions and overrides + String? onTapHint, + String? onLongPressHint, + List? customActions, + List? children, +}) { + return _MatchesSemanticsData( + label: label, + attributedLabel: attributedLabel, + hint: hint, + attributedHint: attributedHint, + value: value, + attributedValue: attributedValue, + increasedValue: increasedValue, + attributedIncreasedValue: attributedIncreasedValue, + decreasedValue: decreasedValue, + attributedDecreasedValue: attributedDecreasedValue, + tooltip: tooltip, + textDirection: textDirection, + rect: rect, + size: size, + elevation: elevation, + thickness: thickness, + platformViewId: platformViewId, + customActions: customActions, maxValueLength: maxValueLength, + currentValueLength: currentValueLength, + // Flags + hasCheckedState: hasCheckedState, + isChecked: isChecked, + isSelected: isSelected, + isButton: isButton, + isSlider: isSlider, + isKeyboardKey: isKeyboardKey, + isLink: isLink, + isFocused: isFocused, + isFocusable: isFocusable, + isTextField: isTextField, + isReadOnly: isReadOnly, + hasEnabledState: hasEnabledState, + isEnabled: isEnabled, + isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup, + isHeader: isHeader, + isObscured: isObscured, + isMultiline: isMultiline, + namesRoute: namesRoute, + scopesRoute: scopesRoute, + isHidden: isHidden, + isImage: isImage, + isLiveRegion: isLiveRegion, + hasToggledState: hasToggledState, + isToggled: isToggled, + hasImplicitScrolling: hasImplicitScrolling, + // Actions + hasTapAction: hasTapAction, + hasLongPressAction: hasLongPressAction, + hasScrollLeftAction: hasScrollLeftAction, + hasScrollRightAction: hasScrollRightAction, + hasScrollUpAction: hasScrollUpAction, + hasScrollDownAction: hasScrollDownAction, + hasIncreaseAction: hasIncreaseAction, + hasDecreaseAction: hasDecreaseAction, + hasShowOnScreenAction: hasShowOnScreenAction, + hasMoveCursorForwardByCharacterAction: hasMoveCursorForwardByCharacterAction, + hasMoveCursorBackwardByCharacterAction: hasMoveCursorBackwardByCharacterAction, + hasMoveCursorForwardByWordAction: hasMoveCursorForwardByWordAction, + hasMoveCursorBackwardByWordAction: hasMoveCursorBackwardByWordAction, + hasSetTextAction: hasSetTextAction, + hasSetSelectionAction: hasSetSelectionAction, + hasCopyAction: hasCopyAction, + hasCutAction: hasCutAction, + hasPasteAction: hasPasteAction, + hasDidGainAccessibilityFocusAction: hasDidGainAccessibilityFocusAction, + hasDidLoseAccessibilityFocusAction: hasDidLoseAccessibilityFocusAction, + hasDismissAction: hasDismissAction, + // Custom actions and overrides children: children, + onLongPressHint: onLongPressHint, + onTapHint: onTapHint, ); } @@ -1885,31 +2044,136 @@ class _MatchesReferenceImage extends AsyncMatcher { class _MatchesSemanticsData extends Matcher { _MatchesSemanticsData({ - this.label, - this.attributedLabel, - this.hint, - this.attributedHint, - this.value, - this.attributedValue, - this.increasedValue, - this.attributedIncreasedValue, - this.decreasedValue, - this.attributedDecreasedValue, - this.tooltip, - this.flags, - this.actions, - this.textDirection, - this.rect, - this.size, - this.elevation, - this.thickness, - this.platformViewId, - this.maxValueLength, - this.currentValueLength, - this.customActions, - this.hintOverrides, - this.children, - }); + required this.label, + required this.attributedLabel, + required this.hint, + required this.attributedHint, + required this.value, + required this.attributedValue, + required this.increasedValue, + required this.attributedIncreasedValue, + required this.decreasedValue, + required this.attributedDecreasedValue, + required this.tooltip, + required this.textDirection, + required this.rect, + required this.size, + required this.elevation, + required this.thickness, + required this.platformViewId, + required this.maxValueLength, + required this.currentValueLength, + // Flags + required bool? hasCheckedState, + required bool? isChecked, + required bool? isSelected, + required bool? isButton, + required bool? isSlider, + required bool? isKeyboardKey, + required bool? isLink, + required bool? isFocused, + required bool? isFocusable, + required bool? isTextField, + required bool? isReadOnly, + required bool? hasEnabledState, + required bool? isEnabled, + required bool? isInMutuallyExclusiveGroup, + required bool? isHeader, + required bool? isObscured, + required bool? isMultiline, + required bool? namesRoute, + required bool? scopesRoute, + required bool? isHidden, + required bool? isImage, + required bool? isLiveRegion, + required bool? hasToggledState, + required bool? isToggled, + required bool? hasImplicitScrolling, + // Actions + required bool? hasTapAction, + required bool? hasLongPressAction, + required bool? hasScrollLeftAction, + required bool? hasScrollRightAction, + required bool? hasScrollUpAction, + required bool? hasScrollDownAction, + required bool? hasIncreaseAction, + required bool? hasDecreaseAction, + required bool? hasShowOnScreenAction, + required bool? hasMoveCursorForwardByCharacterAction, + required bool? hasMoveCursorBackwardByCharacterAction, + required bool? hasMoveCursorForwardByWordAction, + required bool? hasMoveCursorBackwardByWordAction, + required bool? hasSetTextAction, + required bool? hasSetSelectionAction, + required bool? hasCopyAction, + required bool? hasCutAction, + required bool? hasPasteAction, + required bool? hasDidGainAccessibilityFocusAction, + required bool? hasDidLoseAccessibilityFocusAction, + required bool? hasDismissAction, + // Custom actions and overrides + required String? onTapHint, + required String? onLongPressHint, + required this.customActions, + required this.children, + }) : flags = { + if (hasCheckedState != null) SemanticsFlag.hasCheckedState: hasCheckedState, + if (isChecked != null) SemanticsFlag.isChecked: isChecked, + if (isSelected != null) SemanticsFlag.isSelected: isSelected, + if (isButton != null) SemanticsFlag.isButton: isButton, + if (isSlider != null) SemanticsFlag.isSlider: isSlider, + if (isKeyboardKey != null) SemanticsFlag.isKeyboardKey: isKeyboardKey, + if (isLink != null) SemanticsFlag.isLink: isLink, + if (isTextField != null) SemanticsFlag.isTextField: isTextField, + if (isReadOnly != null) SemanticsFlag.isReadOnly: isReadOnly, + if (isFocused != null) SemanticsFlag.isFocused: isFocused, + if (isFocusable != null) SemanticsFlag.isFocusable: isFocusable, + if (hasEnabledState != null) SemanticsFlag.hasEnabledState: hasEnabledState, + if (isEnabled != null) SemanticsFlag.isEnabled: isEnabled, + if (isInMutuallyExclusiveGroup != null) SemanticsFlag.isInMutuallyExclusiveGroup: isInMutuallyExclusiveGroup, + if (isHeader != null) SemanticsFlag.isHeader: isHeader, + if (isObscured != null) SemanticsFlag.isObscured: isObscured, + if (isMultiline != null) SemanticsFlag.isMultiline: isMultiline, + if (namesRoute != null) SemanticsFlag.namesRoute: namesRoute, + if (scopesRoute != null) SemanticsFlag.scopesRoute: scopesRoute, + if (isHidden != null) SemanticsFlag.isHidden: isHidden, + if (isImage != null) SemanticsFlag.isImage: isImage, + if (isLiveRegion != null) SemanticsFlag.isLiveRegion: isLiveRegion, + if (hasToggledState != null) SemanticsFlag.hasToggledState: hasToggledState, + if (isToggled != null) SemanticsFlag.isToggled: isToggled, + if (hasImplicitScrolling != null) SemanticsFlag.hasImplicitScrolling: hasImplicitScrolling, + if (isSlider != null) SemanticsFlag.isSlider: isSlider, + }, + actions = { + if (hasTapAction != null) SemanticsAction.tap: hasTapAction, + if (hasLongPressAction != null) SemanticsAction.longPress: hasLongPressAction, + if (hasScrollLeftAction != null) SemanticsAction.scrollLeft: hasScrollLeftAction, + if (hasScrollRightAction != null) SemanticsAction.scrollRight: hasScrollRightAction, + if (hasScrollUpAction != null) SemanticsAction.scrollUp: hasScrollUpAction, + if (hasScrollDownAction != null) SemanticsAction.scrollDown: hasScrollDownAction, + if (hasIncreaseAction != null) SemanticsAction.increase: hasIncreaseAction, + if (hasDecreaseAction != null) SemanticsAction.decrease: hasDecreaseAction, + if (hasShowOnScreenAction != null) SemanticsAction.showOnScreen: hasShowOnScreenAction, + if (hasMoveCursorForwardByCharacterAction != null) SemanticsAction.moveCursorForwardByCharacter: hasMoveCursorForwardByCharacterAction, + if (hasMoveCursorBackwardByCharacterAction != null) SemanticsAction.moveCursorBackwardByCharacter: hasMoveCursorBackwardByCharacterAction, + if (hasSetSelectionAction != null) SemanticsAction.setSelection: hasSetSelectionAction, + if (hasCopyAction != null) SemanticsAction.copy: hasCopyAction, + if (hasCutAction != null) SemanticsAction.cut: hasCutAction, + if (hasPasteAction != null) SemanticsAction.paste: hasPasteAction, + if (hasDidGainAccessibilityFocusAction != null) SemanticsAction.didGainAccessibilityFocus: hasDidGainAccessibilityFocusAction, + if (hasDidLoseAccessibilityFocusAction != null) SemanticsAction.didLoseAccessibilityFocus: hasDidLoseAccessibilityFocusAction, + if (customActions != null) SemanticsAction.customAction: customActions.isNotEmpty, + if (hasDismissAction != null) SemanticsAction.dismiss: hasDismissAction, + if (hasMoveCursorForwardByWordAction != null) SemanticsAction.moveCursorForwardByWord: hasMoveCursorForwardByWordAction, + if (hasMoveCursorBackwardByWordAction != null) SemanticsAction.moveCursorBackwardByWord: hasMoveCursorBackwardByWordAction, + if (hasSetTextAction != null) SemanticsAction.setText: hasSetTextAction, + }, + hintOverrides = onTapHint == null && onLongPressHint == null + ? null + : SemanticsHintOverrides( + onTapHint: onTapHint, + onLongPressHint: onLongPressHint, + ); final String? label; final AttributedString? attributedLabel; @@ -1923,9 +2187,7 @@ class _MatchesSemanticsData extends Matcher { final AttributedString? attributedDecreasedValue; final String? tooltip; final SemanticsHintOverrides? hintOverrides; - final List? actions; final List? customActions; - final List? flags; final TextDirection? textDirection; final Rect? rect; final Size? size; @@ -1936,6 +2198,14 @@ class _MatchesSemanticsData extends Matcher { final int? currentValueLength; final List? children; + /// There are three possible states for these two maps: + /// + /// 1. If the flag/action maps to `true`, then it must be present in the SemanticData + /// 2. If the flag/action maps to `false`, then it must not be present in the SemanticData + /// 3. If the flag/action is not in the map, then it will not be validated against + final Map actions; + final Map flags; + @override Description describe(Description description) { description.add('has semantics'); @@ -1972,11 +2242,39 @@ class _MatchesSemanticsData extends Matcher { if (tooltip != null) { description.add(' with tooltip: $tooltip'); } - if (actions != null) { - description.add(' with actions: ').addDescriptionOf(actions); + if (actions.isNotEmpty) { + final List expectedActions = actions.entries + .where((MapEntry e) => e.value) + .map((MapEntry e) => e.key) + .toList(); + final List notExpectedActions = actions.entries + .where((MapEntry e) => !e.value) + .map((MapEntry e) => e.key) + .toList(); + + if (expectedActions.isNotEmpty) { + description.add(' with actions: ').addDescriptionOf(expectedActions); + } + if (notExpectedActions.isNotEmpty) { + description.add(' without actions: ').addDescriptionOf(notExpectedActions); + } } - if (flags != null) { - description.add(' with flags: ').addDescriptionOf(flags); + if (flags.isNotEmpty) { + final List expectedFlags = flags.entries + .where((MapEntry e) => e.value) + .map((MapEntry e) => e.key) + .toList(); + final List notExpectedFlags = flags.entries + .where((MapEntry e) => !e.value) + .map((MapEntry e) => e.key) + .toList(); + + if (expectedFlags.isNotEmpty) { + description.add(' with flags: ').addDescriptionOf(expectedFlags); + } + if (notExpectedFlags.isNotEmpty) { + description.add(' without flags: ').addDescriptionOf(notExpectedFlags); + } } if (textDirection != null) { description.add(' with textDirection: $textDirection '); @@ -2116,18 +2414,18 @@ class _MatchesSemanticsData extends Matcher { if (maxValueLength != null && maxValueLength != data.maxValueLength) { return failWithDescription(matchState, 'maxValueLength was: ${data.maxValueLength}'); } - if (actions != null) { - int actionBits = 0; - for (final SemanticsAction action in actions!) { - actionBits |= action.index; - } - if (actionBits != data.actions) { - final List actionSummary = [ - for (final SemanticsAction action in SemanticsAction.values.values) - if ((data.actions & action.index) != 0) - describeEnum(action), - ]; - return failWithDescription(matchState, 'actions were: $actionSummary'); + if (actions.isNotEmpty) { + 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 (customActions != null || hintOverrides != null) { @@ -2142,7 +2440,7 @@ class _MatchesSemanticsData extends Matcher { expectedCustomActions.add(CustomSemanticsAction.overridingAction(hint: hintOverrides!.onLongPressHint!, action: SemanticsAction.longPress)); } if (expectedCustomActions.length != providedCustomActions.length) { - return failWithDescription(matchState, 'custom actions where: $providedCustomActions'); + return failWithDescription(matchState, 'custom actions were: $providedCustomActions'); } int sortActions(CustomSemanticsAction left, CustomSemanticsAction right) { return CustomSemanticsAction.getIdentifier(left) - CustomSemanticsAction.getIdentifier(right); @@ -2151,22 +2449,22 @@ class _MatchesSemanticsData extends Matcher { providedCustomActions.sort(sortActions); for (int i = 0; i < expectedCustomActions.length; i++) { if (expectedCustomActions[i] != providedCustomActions[i]) { - return failWithDescription(matchState, 'custom actions where: $providedCustomActions'); + return failWithDescription(matchState, 'custom actions were: $providedCustomActions'); } } } - if (flags != null) { - int flagBits = 0; - for (final SemanticsFlag flag in flags!) { - flagBits |= flag.index; - } - if (flagBits != data.flags) { - final List flagSummary = [ - for (final SemanticsFlag flag in SemanticsFlag.values.values) - if ((data.flags & flag.index) != 0) - describeEnum(flag), - ]; - return failWithDescription(matchState, 'flags were: $flagSummary'); + if (flags.isNotEmpty) { + 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'); + } } } bool allMatched = true; diff --git a/packages/flutter_test/test/matchers_test.dart b/packages/flutter_test/test/matchers_test.dart index 844a256463808..5e6f3d7866211 100644 --- a/packages/flutter_test/test/matchers_test.dart +++ b/packages/flutter_test/test/matchers_test.dart @@ -599,10 +599,7 @@ void main() { actions |= index; } for (final int index in SemanticsFlag.values.keys) { - // TODO(mdebbar): Remove this if after https://github.com/flutter/engine/pull/9894 - if (SemanticsFlag.values[index] != SemanticsFlag.isMultiline) { - flags |= index; - } + flags |= index; } final SemanticsData data = SemanticsData( flags: flags, @@ -655,8 +652,7 @@ void main() { isInMutuallyExclusiveGroup: true, isHeader: true, isObscured: true, - // TODO(mdebbar): Uncomment after https://github.com/flutter/engine/pull/9894 - //isMultiline: true, + isMultiline: true, namesRoute: true, scopesRoute: true, isHidden: true, @@ -721,6 +717,446 @@ void main() { }); }); + group('containsSemantics', () { + testWidgets('matches SemanticsData', (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'): () { }, + }, + )); + + 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: true, + onTapHint: 'scan', + onLongPressHint: 'fill', + customActions: [ + const CustomSemanticsAction(label: 'foo'), + const CustomSemanticsAction(label: 'bar'), + ], + ), + ); + + expect( + tester.getSemantics(find.byKey(key)), + isNot(containsSemantics( + label: 'foo', + hint: 'bar', + value: 'baz', + textDirection: TextDirection.rtl, + hasTapAction: true, + hasLongPressAction: true, + isButton: true, + isLink: true, + isHeader: true, + namesRoute: true, + onTapHint: 'scan', + onLongPressHint: 'fill', + customActions: [ + const CustomSemanticsAction(label: 'foo'), + const CustomSemanticsAction(label: 'barz'), + ], + )), + reason: 'CustomSemanticsAction "barz" should not have matched "bar".' + ); + + expect( + tester.getSemantics(find.byKey(key)), + isNot(matchesSemantics( + label: 'foo', + hint: 'bar', + value: 'baz', + textDirection: TextDirection.rtl, + hasTapAction: true, + hasLongPressAction: true, + isButton: true, + isLink: true, + isHeader: true, + namesRoute: true, + onTapHint: 'scans', + onLongPressHint: 'fills', + customActions: [ + const CustomSemanticsAction(label: 'foo'), + const CustomSemanticsAction(label: 'bar'), + ], + )), + reason: 'onTapHint "scans" should not have matched "scan".', + ); + }); + + testWidgets('can match all semantics flags and actions enabled', (WidgetTester tester) async { + int actions = 0; + int flags = 0; + const CustomSemanticsAction action = CustomSemanticsAction(label: 'test'); + for (final int index in SemanticsAction.values.keys) { + actions |= index; + } + for (final int index in SemanticsFlag.values.keys) { + flags |= index; + } + final SemanticsData data = SemanticsData( + flags: flags, + actions: actions, + attributedLabel: AttributedString('a'), + attributedIncreasedValue: AttributedString('b'), + attributedValue: AttributedString('c'), + attributedDecreasedValue: AttributedString('d'), + attributedHint: AttributedString('e'), + tooltip: 'f', + textDirection: TextDirection.ltr, + rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + elevation: 3.0, + thickness: 4.0, + textSelection: null, + scrollIndex: null, + scrollChildCount: null, + scrollPosition: null, + scrollExtentMax: null, + scrollExtentMin: null, + platformViewId: 105, + customSemanticsActionIds: [CustomSemanticsAction.getIdentifier(action)], + currentValueLength: 10, + maxValueLength: 15, + ); + final _FakeSemanticsNode node = _FakeSemanticsNode(data); + + expect( + node, + containsSemantics( + rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + size: const Size(10.0, 10.0), + elevation: 3.0, + thickness: 4.0, + platformViewId: 105, + currentValueLength: 10, + maxValueLength: 15, + /* Flags */ + hasCheckedState: true, + isChecked: true, + isSelected: true, + isButton: true, + isSlider: true, + isKeyboardKey: true, + isLink: true, + isTextField: true, + isReadOnly: true, + hasEnabledState: true, + isFocused: true, + isFocusable: true, + isEnabled: true, + isInMutuallyExclusiveGroup: true, + isHeader: true, + isObscured: true, + isMultiline: true, + namesRoute: true, + scopesRoute: true, + isHidden: true, + isImage: true, + isLiveRegion: true, + hasToggledState: true, + isToggled: true, + hasImplicitScrolling: true, + /* Actions */ + hasTapAction: true, + hasLongPressAction: true, + hasScrollLeftAction: true, + hasScrollRightAction: true, + hasScrollUpAction: true, + hasScrollDownAction: true, + hasIncreaseAction: true, + hasDecreaseAction: true, + hasShowOnScreenAction: true, + hasMoveCursorForwardByCharacterAction: true, + hasMoveCursorBackwardByCharacterAction: true, + hasMoveCursorForwardByWordAction: true, + hasMoveCursorBackwardByWordAction: true, + hasSetTextAction: true, + hasSetSelectionAction: true, + hasCopyAction: true, + hasCutAction: true, + hasPasteAction: true, + hasDidGainAccessibilityFocusAction: true, + hasDidLoseAccessibilityFocusAction: true, + hasDismissAction: true, + customActions: [action], + ), + ); + }); + + testWidgets('can match all flags and actions disabled', (WidgetTester tester) async { + final SemanticsData data = SemanticsData( + flags: 0, + actions: 0, + attributedLabel: AttributedString('a'), + attributedIncreasedValue: AttributedString('b'), + attributedValue: AttributedString('c'), + attributedDecreasedValue: AttributedString('d'), + attributedHint: AttributedString('e'), + tooltip: 'f', + textDirection: TextDirection.ltr, + rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + elevation: 3.0, + thickness: 4.0, + textSelection: null, + scrollIndex: null, + scrollChildCount: null, + scrollPosition: null, + scrollExtentMax: null, + scrollExtentMin: null, + platformViewId: 105, + currentValueLength: 10, + maxValueLength: 15, + ); + final _FakeSemanticsNode node = _FakeSemanticsNode(data); + + expect( + node, + containsSemantics( + rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + size: const Size(10.0, 10.0), + elevation: 3.0, + thickness: 4.0, + platformViewId: 105, + currentValueLength: 10, + maxValueLength: 15, + /* Flags */ + hasCheckedState: false, + isChecked: false, + isSelected: false, + isButton: false, + isSlider: false, + isKeyboardKey: false, + isLink: false, + isTextField: false, + isReadOnly: false, + hasEnabledState: false, + isFocused: false, + isFocusable: false, + isEnabled: false, + isInMutuallyExclusiveGroup: false, + isHeader: false, + isObscured: false, + isMultiline: false, + namesRoute: false, + scopesRoute: false, + isHidden: false, + isImage: false, + isLiveRegion: false, + hasToggledState: false, + isToggled: false, + hasImplicitScrolling: false, + /* Actions */ + hasTapAction: false, + hasLongPressAction: false, + hasScrollLeftAction: false, + hasScrollRightAction: false, + hasScrollUpAction: false, + hasScrollDownAction: false, + hasIncreaseAction: false, + hasDecreaseAction: false, + hasShowOnScreenAction: false, + hasMoveCursorForwardByCharacterAction: false, + hasMoveCursorBackwardByCharacterAction: false, + hasMoveCursorForwardByWordAction: false, + hasMoveCursorBackwardByWordAction: false, + hasSetTextAction: false, + hasSetSelectionAction: false, + hasCopyAction: false, + hasCutAction: false, + hasPasteAction: false, + hasDidGainAccessibilityFocusAction: false, + hasDidLoseAccessibilityFocusAction: false, + hasDismissAction: false, + ), + ); + }); + + testWidgets('only matches given flags and actions', (WidgetTester tester) async { + int allActions = 0; + int allFlags = 0; + for (final int index in SemanticsAction.values.keys) { + allActions |= index; + } + for (final int index in SemanticsFlag.values.keys) { + allFlags |= index; + } + final SemanticsData emptyData = SemanticsData( + flags: 0, + actions: 0, + attributedLabel: AttributedString('a'), + attributedIncreasedValue: AttributedString('b'), + attributedValue: AttributedString('c'), + attributedDecreasedValue: AttributedString('d'), + attributedHint: AttributedString('e'), + tooltip: 'f', + textDirection: TextDirection.ltr, + rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + elevation: 3.0, + thickness: 4.0, + textSelection: null, + scrollIndex: null, + scrollChildCount: null, + scrollPosition: null, + scrollExtentMax: null, + scrollExtentMin: null, + platformViewId: 105, + currentValueLength: 10, + maxValueLength: 15, + ); + final _FakeSemanticsNode emptyNode = _FakeSemanticsNode(emptyData); + + const CustomSemanticsAction action = CustomSemanticsAction(label: 'test'); + final SemanticsData fullData = SemanticsData( + flags: allFlags, + actions: allActions, + attributedLabel: AttributedString('a'), + attributedIncreasedValue: AttributedString('b'), + attributedValue: AttributedString('c'), + attributedDecreasedValue: AttributedString('d'), + attributedHint: AttributedString('e'), + tooltip: 'f', + textDirection: TextDirection.ltr, + rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + elevation: 3.0, + thickness: 4.0, + textSelection: null, + scrollIndex: null, + scrollChildCount: null, + scrollPosition: null, + scrollExtentMax: null, + scrollExtentMin: null, + platformViewId: 105, + currentValueLength: 10, + maxValueLength: 15, + customSemanticsActionIds: [CustomSemanticsAction.getIdentifier(action)], + ); + final _FakeSemanticsNode fullNode = _FakeSemanticsNode(fullData); + + expect( + emptyNode, + containsSemantics( + rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + size: const Size(10.0, 10.0), + elevation: 3.0, + thickness: 4.0, + platformViewId: 105, + currentValueLength: 10, + maxValueLength: 15, + ), + ); + + expect( + fullNode, + containsSemantics( + rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + size: const Size(10.0, 10.0), + elevation: 3.0, + thickness: 4.0, + platformViewId: 105, + currentValueLength: 10, + maxValueLength: 15, + customActions: [action], + ), + ); + }); + + testWidgets('can match child semantics', (WidgetTester tester) async { + final SemanticsHandle handle = tester.ensureSemantics(); + const Key key = Key('a'); + await tester.pumpWidget(Semantics( + key: key, + label: 'Foo', + container: true, + explicitChildNodes: true, + textDirection: TextDirection.ltr, + child: Semantics( + label: 'Bar', + textDirection: TextDirection.ltr, + ), + )); + final SemanticsNode node = tester.getSemantics(find.byKey(key)); + + expect( + node, + containsSemantics( + label: 'Foo', + textDirection: TextDirection.ltr, + children: [ + containsSemantics( + label: 'Bar', + textDirection: TextDirection.ltr, + ), + ], + ), + ); + + handle.dispose(); + }); + + testWidgets('can match only custom actions', (WidgetTester tester) async { + const CustomSemanticsAction action = CustomSemanticsAction(label: 'test'); + final SemanticsData data = SemanticsData( + flags: 0, + actions: SemanticsAction.customAction.index, + attributedLabel: AttributedString('a'), + attributedIncreasedValue: AttributedString('b'), + attributedValue: AttributedString('c'), + attributedDecreasedValue: AttributedString('d'), + attributedHint: AttributedString('e'), + tooltip: 'f', + textDirection: TextDirection.ltr, + rect: const Rect.fromLTRB(0.0, 0.0, 10.0, 10.0), + elevation: 3.0, + thickness: 4.0, + textSelection: null, + scrollIndex: null, + scrollChildCount: null, + scrollPosition: null, + scrollExtentMax: null, + scrollExtentMin: null, + platformViewId: 105, + currentValueLength: 10, + maxValueLength: 15, + customSemanticsActionIds: [CustomSemanticsAction.getIdentifier(action)], + ); + final _FakeSemanticsNode node = _FakeSemanticsNode(data); + + expect(node, containsSemantics(customActions: [action])); + }); + }); + group('findsAtLeastNWidgets', () { Widget boilerplate(Widget child) { return Directionality(