Unverified Commit cdf1cec9 authored by Jonah Williams's avatar Jonah Williams Committed by GitHub

add ability to match child semantics data (#22001)

parent 21a32fdd
......@@ -14,7 +14,7 @@ void main() {
home: ChipDemo(),
));
expect(tester.getSemanticsData(find.byIcon(Icons.vignette)), matchesSemanticsData(
expect(tester.getSemantics(find.byIcon(Icons.vignette)), matchesSemantics(
isButton: true,
hasEnabledState: true,
isEnabled: true,
......@@ -22,7 +22,7 @@ void main() {
label: 'Update border shape',
));
expect(tester.getSemanticsData(find.byIcon(Icons.refresh)), matchesSemanticsData(
expect(tester.getSemantics(find.byIcon(Icons.refresh)), matchesSemantics(
isButton: true,
hasEnabledState: true,
isEnabled: true,
......
......@@ -330,7 +330,7 @@ void main() {
),
));
expect(tester.getSemanticsData(find.byType(CupertinoSlider)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(CupertinoSlider)), matchesSemantics(
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '50%',
......@@ -348,7 +348,7 @@ void main() {
),
));
expect(tester.getSemanticsData(find.byType(CupertinoSlider)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(CupertinoSlider)), matchesSemantics(
hasIncreaseAction: true,
hasDecreaseAction: true,
value: '60%',
......
......@@ -82,7 +82,7 @@ void main() {
await tester.pumpAndSettle();
expect(tester.getSemanticsData(find.byType(BackButton)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(BackButton)), matchesSemantics(
label: 'Back',
isButton: true,
hasEnabledState: true,
......
......@@ -66,7 +66,7 @@ void main() {
),
));
expect(tester.getSemanticsData(find.byType(Checkbox)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isEnabled: true,
......@@ -80,7 +80,7 @@ void main() {
),
));
expect(tester.getSemanticsData(find.byType(Checkbox)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isChecked: true,
......@@ -95,7 +95,7 @@ void main() {
),
));
expect(tester.getSemanticsData(find.byType(Checkbox)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
));
......@@ -107,7 +107,7 @@ void main() {
),
));
expect(tester.getSemanticsData(find.byType(Checkbox)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
hasCheckedState: true,
hasEnabledState: true,
isChecked: true,
......@@ -129,7 +129,7 @@ void main() {
),
));
expect(tester.getSemanticsData(find.byType(Checkbox)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(Checkbox)), matchesSemantics(
label: 'foo',
textDirection: TextDirection.ltr,
hasCheckedState: true,
......
......@@ -692,7 +692,7 @@ void main() {
));
// By default the hint contributes the label.
expect(tester.getSemanticsData(find.byKey(key)), matchesSemanticsData(
expect(tester.getSemantics(find.byKey(key)), matchesSemantics(
isButton: true,
label: 'test',
hasTapAction: true,
......@@ -707,7 +707,7 @@ void main() {
));
// Displays label of select item and is no longer tappable.
expect(tester.getSemanticsData(find.byKey(key)), matchesSemanticsData(
expect(tester.getSemantics(find.byKey(key)), matchesSemantics(
isButton: true,
label: 'three',
hasTapAction: true,
......
......@@ -95,7 +95,7 @@ void main() {
)
));
expect(tester.getSemanticsData(find.byType(ExpandIcon)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics(
hasTapAction: true,
hasEnabledState: true,
isEnabled: true,
......@@ -110,7 +110,7 @@ void main() {
)
));
expect(tester.getSemanticsData(find.byType(ExpandIcon)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(ExpandIcon)), matchesSemantics(
hasTapAction: true,
hasEnabledState: true,
isEnabled: true,
......
......@@ -382,7 +382,7 @@ void main() {
),
);
expect(tester.getSemanticsData(find.byKey(expandedKey)), matchesSemanticsData(
expect(tester.getSemantics(find.byKey(expandedKey)), matchesSemantics(
label: 'Expanded',
isButton: true,
hasEnabledState: true,
......@@ -391,7 +391,7 @@ void main() {
onTapHint: localizations.expandedIconTapHint,
));
expect(tester.getSemanticsData(find.byKey(collapsedKey)), matchesSemanticsData(
expect(tester.getSemantics(find.byKey(collapsedKey)), matchesSemantics(
label: 'Collapsed',
isButton: true,
hasEnabledState: true,
......
......@@ -462,10 +462,10 @@ void main() {
));
// Get the switch tile's semantics:
final SemanticsData semanticsData = tester.getSemanticsData(find.byKey(const Key('Switch tile')));
final SemanticsNode semanticsNode = tester.getSemantics(find.byKey(const Key('Switch tile')));
// Check for properties of both SwitchTile semantics and the ReorderableListView custom semantics actions.
expect(semanticsData, matchesSemanticsData(
expect(semanticsNode, matchesSemantics(
hasToggledState: true,
isToggled: true,
isEnabled: true,
......
......@@ -555,7 +555,7 @@ void main() {
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(tester.getSemanticsData(find.text('snack')), matchesSemanticsData(
expect(tester.getSemantics(find.text('snack')), matchesSemantics(
isLiveRegion: true,
hasDismissAction: true,
hasScrollDownAction: true,
......
......@@ -378,17 +378,17 @@ void main() {
final SemanticsHandle handle = tester.ensureSemantics();
await pumpTestWidget(tester);
expect(tester.getSemanticsData(find.text('B')), matchesSemanticsData(
expect(tester.getSemantics(find.text('B')), matchesSemantics(
label: 'B',
size: const Size(48.0, 48.0),
));
expect(tester.getSemanticsData(find.text('C')), matchesSemanticsData(
expect(tester.getSemantics(find.text('C')), matchesSemantics(
label: 'C',
size: const Size(48.0, 48.0),
));
expect(tester.getSemanticsData(find.text('D')), matchesSemanticsData(
expect(tester.getSemantics(find.text('D')), matchesSemantics(
label: 'D',
size: const Size(48.0, 48.0),
));
......
......@@ -111,7 +111,7 @@ void main() {
),
);
expect(tester.getSemanticsData(find.byType(ImageIcon)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(ImageIcon)), matchesSemantics(
label: 'test',
textDirection: TextDirection.ltr,
));
......
......@@ -643,7 +643,7 @@ void main() {
onTapHint: 'test',
));
expect(tester.getSemanticsData(find.byType(Semantics)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
hasTapAction: true,
onTapHint: 'test'
));
......@@ -654,7 +654,7 @@ void main() {
onLongPressHint: 'foo',
));
expect(tester.getSemanticsData(find.byType(Semantics)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
hasLongPressAction: true,
onLongPressHint: 'foo'
));
......@@ -671,7 +671,7 @@ void main() {
},
));
expect(tester.getSemanticsData(find.byType(Semantics)), matchesSemanticsData(
expect(tester.getSemantics(find.byType(Semantics)), matchesSemantics(
customActions: <CustomSemanticsAction>[
const CustomSemanticsAction(label: 'bar'),
const CustomSemanticsAction(label: 'foo'),
......
......@@ -325,13 +325,13 @@ AsyncMatcher matchesReferenceImage(ui.Image image) {
return _MatchesReferenceImage(image);
}
/// Asserts that a [SemanticsData] contains the specified information.
/// Asserts that a [SemanticsNode] contains the specified information.
///
/// If either the label, hint, value, textDirection, or rect fields are not
/// provided, then they are not part of the comparison. All of the boolean
/// flag and action fields must match, and default to false.
///
/// To retrieve the semantics data of a widget, use [tester.getSemanticsData]
/// To retrieve the semantics data of a widget, use [tester.getSemantics]
/// with a [Finder] that returns a single widget. Semantics must be enabled
/// in order to use this method.
///
......@@ -339,15 +339,14 @@ AsyncMatcher matchesReferenceImage(ui.Image image) {
///
/// ```dart
/// final SemanticsHandle handle = tester.ensureSemantics();
/// final SemanticsData data = tester.getSemanticsData(find.text('hello'));
/// expect(data, matchesSemanticsData(label: 'hello'));
/// expect(tester.getSemantics(find.text('hello')), matchesSemanticsNode(label: 'hello'));
/// handle.dispose();
/// ```
///
/// See also:
///
/// * [WidgetTester.getSemanticsData], the tester method which retrieves data.
Matcher matchesSemanticsData({
/// * [WidgetTester.getSemantics], the tester method which retrieves semantics.
Matcher matchesSemantics({
String label,
String hint,
String value,
......@@ -401,6 +400,7 @@ Matcher matchesSemanticsData({
String onTapHint,
String onLongPressHint,
List<CustomSemanticsAction> customActions,
List<Matcher> children,
}) {
final List<SemanticsFlag> flags = <SemanticsFlag>[];
if (hasCheckedState)
......@@ -492,6 +492,170 @@ Matcher matchesSemanticsData({
onLongPressHint: onLongPressHint,
);
return _MatchesSemanticsData(
label: label,
hint: hint,
value: value,
increasedValue: increasedValue,
decreasedValue: decreasedValue,
actions: actions,
flags: flags,
textDirection: textDirection,
rect: rect,
size: size,
customActions: customActions,
hintOverrides: hintOverrides,
children: children,
);
}
/// DEPRECATED: use [matchesSemantics] instead.
@Deprecated('use matchesSemantics instead')
Matcher matchesSemanticsData({
String label,
String hint,
String value,
String increasedValue,
String decreasedValue,
TextDirection textDirection,
Rect rect,
Size size,
// Flags //
bool hasCheckedState = false,
bool isChecked = false,
bool isSelected = false,
bool isButton = false,
bool isFocused = false,
bool isTextField = false,
bool hasEnabledState = false,
bool isEnabled = false,
bool isInMutuallyExclusiveGroup = false,
bool isHeader = false,
bool isObscured = false,
bool namesRoute = false,
bool scopesRoute = false,
bool isHidden = false,
bool isImage = false,
bool isLiveRegion = false,
bool hasToggledState = false,
bool isToggled = false,
bool hasImplicitScrolling = false,
// Actions //
bool hasTapAction = false,
bool hasLongPressAction = false,
bool hasScrollLeftAction = false,
bool hasScrollRightAction = false,
bool hasScrollUpAction = false,
bool hasScrollDownAction = false,
bool hasIncreaseAction = false,
bool hasDecreaseAction = false,
bool hasShowOnScreenAction = false,
bool hasMoveCursorForwardByCharacterAction = false,
bool hasMoveCursorBackwardByCharacterAction = false,
bool hasMoveCursorForwardByWordAction = false,
bool hasMoveCursorBackwardByWordAction = false,
bool hasSetSelectionAction = false,
bool hasCopyAction = false,
bool hasCutAction = false,
bool hasPasteAction = false,
bool hasDidGainAccessibilityFocusAction = false,
bool hasDidLoseAccessibilityFocusAction = false,
bool hasDismissAction = false,
// Custom actions and overrides
String onTapHint,
String onLongPressHint,
List<CustomSemanticsAction> customActions,
}) {
final List<SemanticsFlag> flags = <SemanticsFlag>[];
if (hasCheckedState)
flags.add(SemanticsFlag.hasCheckedState);
if (isChecked)
flags.add(SemanticsFlag.isChecked);
if (isSelected)
flags.add(SemanticsFlag.isSelected);
if (isButton)
flags.add(SemanticsFlag.isButton);
if (isTextField)
flags.add(SemanticsFlag.isTextField);
if (isFocused)
flags.add(SemanticsFlag.isFocused);
if (hasEnabledState)
flags.add(SemanticsFlag.hasEnabledState);
if (isEnabled)
flags.add(SemanticsFlag.isEnabled);
if (isInMutuallyExclusiveGroup)
flags.add(SemanticsFlag.isInMutuallyExclusiveGroup);
if (isHeader)
flags.add(SemanticsFlag.isHeader);
if (isObscured)
flags.add(SemanticsFlag.isObscured);
if (namesRoute)
flags.add(SemanticsFlag.namesRoute);
if (scopesRoute)
flags.add(SemanticsFlag.scopesRoute);
if (isHidden)
flags.add(SemanticsFlag.isHidden);
if (isImage)
flags.add(SemanticsFlag.isImage);
if (isLiveRegion)
flags.add(SemanticsFlag.isLiveRegion);
if (hasToggledState)
flags.add(SemanticsFlag.hasToggledState);
if (isToggled)
flags.add(SemanticsFlag.isToggled);
if (hasImplicitScrolling)
flags.add(SemanticsFlag.hasImplicitScrolling);
final List<SemanticsAction> actions = <SemanticsAction>[];
if (hasTapAction)
actions.add(SemanticsAction.tap);
if (hasLongPressAction)
actions.add(SemanticsAction.longPress);
if (hasScrollLeftAction)
actions.add(SemanticsAction.scrollLeft);
if (hasScrollRightAction)
actions.add(SemanticsAction.scrollRight);
if (hasScrollUpAction)
actions.add(SemanticsAction.scrollUp);
if (hasScrollDownAction)
actions.add(SemanticsAction.scrollDown);
if (hasIncreaseAction)
actions.add(SemanticsAction.increase);
if (hasDecreaseAction)
actions.add(SemanticsAction.decrease);
if (hasShowOnScreenAction)
actions.add(SemanticsAction.showOnScreen);
if (hasMoveCursorForwardByCharacterAction)
actions.add(SemanticsAction.moveCursorForwardByCharacter);
if (hasMoveCursorBackwardByCharacterAction)
actions.add(SemanticsAction.moveCursorBackwardByCharacter);
if (hasSetSelectionAction)
actions.add(SemanticsAction.setSelection);
if (hasCopyAction)
actions.add(SemanticsAction.copy);
if (hasCutAction)
actions.add(SemanticsAction.cut);
if (hasPasteAction)
actions.add(SemanticsAction.paste);
if (hasDidGainAccessibilityFocusAction)
actions.add(SemanticsAction.didGainAccessibilityFocus);
if (hasDidLoseAccessibilityFocusAction)
actions.add(SemanticsAction.didLoseAccessibilityFocus);
if (customActions != null && customActions.isNotEmpty)
actions.add(SemanticsAction.customAction);
if (hasDismissAction)
actions.add(SemanticsAction.dismiss);
if (hasMoveCursorForwardByWordAction)
actions.add(SemanticsAction.moveCursorForwardByWord);
if (hasMoveCursorBackwardByWordAction)
actions.add(SemanticsAction.moveCursorBackwardByWord);
SemanticsHintOverrides hintOverrides;
if (onTapHint != null || onLongPressHint != null)
hintOverrides = SemanticsHintOverrides(
onTapHint: onTapHint,
onLongPressHint: onLongPressHint,
);
return _MatchesSemanticsData(
label: label,
hint: hint,
......@@ -1686,6 +1850,7 @@ class _MatchesSemanticsData extends Matcher {
this.size,
this.customActions,
this.hintOverrides,
this.children,
});
final String label;
......@@ -1700,43 +1865,51 @@ class _MatchesSemanticsData extends Matcher {
final TextDirection textDirection;
final Rect rect;
final Size size;
final List<Matcher> children;
@override
Description describe(Description description) {
description.add('has semantics');
if (label != null)
description.add('with label: $label ');
description.add(' with label: $label');
if (value != null)
description.add('with value: $value ');
description.add(' with value: $value');
if (hint != null)
description.add('with hint: $hint ');
description.add(' with hint: $hint');
if (increasedValue != null)
description.add('with increasedValue: $increasedValue');
description.add(' with increasedValue: $increasedValue ');
if (decreasedValue != null)
description.add('with decreasedValue: $decreasedValue');
description.add(' with decreasedValue: $decreasedValue ');
if (actions != null)
description.add('with actions:').addDescriptionOf(actions);
description.add(' with actions: ').addDescriptionOf(actions);
if (flags != null)
description.add('with flags:').addDescriptionOf(flags);
description.add(' with flags: ').addDescriptionOf(flags);
if (textDirection != null)
description.add('with textDirection: $textDirection ');
description.add(' with textDirection: $textDirection ');
if (rect != null)
description.add('with rect: $rect');
description.add(' with rect: $rect');
if (size != null)
description.add('with size: $size');
description.add(' with size: $size');
if (customActions != null)
description.add('with custom actions: $customActions');
description.add(' with custom actions: $customActions');
if (hintOverrides != null)
description.add('with custom hints: $hintOverrides');
description.add(' with custom hints: $hintOverrides');
if (children != null) {
description.add(' with children:\n');
for (_MatchesSemanticsData child in children)
child.describe(description);
}
return description;
}
@override
bool matches(covariant SemanticsData data, Map<dynamic, dynamic> matchState) {
if (data == null)
bool matches(dynamic node, Map<dynamic, dynamic> matchState) {
// TODO(jonahwilliams): remove dynamic once we have removed getSemanticsData.
if (node == null)
return failWithDescription(matchState, 'No SemanticsData provided. '
'Maybe you forgot to enabled semantics?');
'Maybe you forgot to enable semantics?');
final SemanticsData data = node is SemanticsNode ? node.getSemanticsData() : node;
if (label != null && label != data.label)
return failWithDescription(matchState, 'label was: ${data.label}');
if (hint != null && hint != data.hint)
......@@ -1800,7 +1973,16 @@ class _MatchesSemanticsData extends Matcher {
return failWithDescription(matchState, 'flags were: $flagSummary');
}
}
return true;
bool allMatched = true;
if (children != null) {
int i = 0;
node.visitChildren((SemanticsNode child) {
allMatched = children[i].matches(child, matchState) && allMatched;
i += 1;
return allMatched;
});
}
return allMatched;
}
bool failWithDescription(Map<dynamic, dynamic> matchState, String description) {
......
......@@ -617,15 +617,39 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker
});
}
/// Attempts to find the [SemanticsData] of first result from `finder`.
/// Attempts to find the [SemanticsNode] of first result from `finder`.
///
/// If the object identified by the finder doesn't own it's semantic node,
/// this will return the semantics data of the first ancestor with semantics
/// data. The ancestor's semantic data will include the child's as well as
/// this will return the semantics data of the first ancestor with semantics.
/// The ancestor's semantic data will include the child's as well as
/// other nodes that have been merged together.
///
/// Will throw a [StateError] if the finder returns more than one element or
/// if no semantics are found or are not enabled.
SemanticsNode getSemantics(Finder finder) {
if (binding.pipelineOwner.semanticsOwner == null)
throw StateError('Semantics are not enabled.');
final Iterable<Element> candidates = finder.evaluate();
if (candidates.isEmpty) {
throw StateError('Finder returned no matching elements.');
}
if (candidates.length > 1) {
throw StateError('Finder returned more than one element.');
}
final Element element = candidates.single;
RenderObject renderObject = element.findRenderObject();
SemanticsNode result = renderObject.debugSemantics;
while (renderObject != null && result == null) {
renderObject = renderObject?.parent;
result = renderObject?.debugSemantics;
}
if (result == null)
throw StateError('No Semantics data found.');
return result;
}
/// DEPRECATED: use [getSemantics] instead.
@Deprecated('use getSemantics instead')
SemanticsData getSemanticsData(Finder finder) {
if (binding.pipelineOwner.semanticsOwner == null)
throw StateError('Semantics are not enabled.');
......
......@@ -409,8 +409,8 @@ void main() {
},
));
expect(tester.getSemanticsData(find.byKey(key)),
matchesSemanticsData(
expect(tester.getSemantics(find.byKey(key)),
matchesSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
......@@ -432,8 +432,8 @@ void main() {
);
// Doesn't match custom actions
expect(tester.getSemanticsData(find.byKey(key)),
isNot(matchesSemanticsData(
expect(tester.getSemantics(find.byKey(key)),
isNot(matchesSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
......@@ -453,8 +453,8 @@ void main() {
);
// Doesn't match wrong hints
expect(tester.getSemanticsData(find.byKey(key)),
isNot(matchesSemanticsData(
expect(tester.getSemantics(find.byKey(key)),
isNot(matchesSemantics(
label: 'foo',
hint: 'bar',
value: 'baz',
......@@ -500,8 +500,10 @@ void main() {
scrollExtentMin: null,
customSemanticsActionIds: <int>[CustomSemanticsAction.getIdentifier(action)],
);
final _FakeSemanticsNode node = _FakeSemanticsNode();
node.data = data;
expect(data, matchesSemanticsData(
expect(node, matchesSemantics(
rect: Rect.fromLTRB(0.0, 0.0, 10.0, 10.0),
size: const Size(10.0, 10.0),
/* Flags */
......@@ -548,6 +550,35 @@ void main() {
customActions: <CustomSemanticsAction>[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, matchesSemantics(
label: 'Foo',
textDirection: TextDirection.ltr,
children: <Matcher>[
matchesSemantics(
label: 'Bar',
textDirection: TextDirection.ltr,
),
],
));
handle.dispose();
});
});
}
......@@ -592,3 +623,9 @@ class _FakeComparator implements GoldenFileComparator {
return Future<void>.value();
}
}
class _FakeSemanticsNode extends SemanticsNode {
SemanticsData data;
@override
SemanticsData getSemanticsData() => data;
}
\ No newline at end of file
......@@ -540,7 +540,7 @@ void main() {
),
);
expect(() => tester.getSemanticsData(find.text('hello')),
expect(() => tester.getSemantics(find.text('hello')),
throwsA(isInstanceOf<StateError>()));
});
......@@ -560,7 +560,7 @@ void main() {
),
);
expect(() => tester.getSemanticsData(find.text('hello')),
expect(() => tester.getSemantics(find.text('hello')),
throwsA(isInstanceOf<StateError>()));
semanticsHandle.dispose();
});
......@@ -581,7 +581,8 @@ void main() {
),
);
final SemanticsData semantics = tester.getSemanticsData(find.text('hello'));
final SemanticsNode node = tester.getSemantics(find.text('hello'));
final SemanticsData semantics = node.getSemanticsData();
expect(semantics.label, 'hello');
expect(semantics.hasAction(SemanticsAction.tap), true);
expect(semantics.hasFlag(SemanticsFlag.isButton), true);
......@@ -609,7 +610,8 @@ void main() {
),
);
final SemanticsData semantics = tester.getSemanticsData(find.byKey(key));
final SemanticsNode node = tester.getSemantics(find.byKey(key));
final SemanticsData semantics = node.getSemanticsData();
expect(semantics.label, 'A\nB\nC');
semanticsHandle.dispose();
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment