simulatedAccessibilityTraversal method
- {FinderBase<
Element> ? start, - FinderBase<
Element> ? end, - FlutterView? view}
Simulates a traversal of the currently visible semantics tree as if by assistive technologies.
Starts at the node for start
. If start
is not provided, then the
traversal begins with the first accessible node in the tree. If start
finds zero elements or more than one element, a StateError will be
thrown.
Ends at the node for end
, inclusive. If end
is not provided, then the
traversal ends with the last accessible node in the currently available
tree. If end
finds zero elements or more than one element, a
StateError will be thrown.
If provided, the nodes for end
and start
must be part of the same
semantics tree, i.e. they must be part of the same view.
If neither start
or end
is provided, view
can be provided to specify
the semantics tree to traverse. If view
is left unspecified,
WidgetTester.view is traversed by default.
Since the order is simulated, edge cases that differ between platforms (such as how the last visible item in a scrollable list is handled) may be inconsistent with platform behavior, but are expected to be sufficient for testing order, availability to assistive technologies, and interactions.
Sample Code
testWidgets('MyWidget', (WidgetTester tester) async {
await tester.pumpWidget(const MyWidget());
expect(
tester.semantics.simulatedAccessibilityTraversal(),
containsAllInOrder(<Matcher>[
containsSemantics(label: 'My Widget'),
containsSemantics(label: 'is awesome!', isChecked: true),
]),
);
});
See also:
- containsSemantics and matchesSemantics, which can be used to match against a single node in the traversal.
- containsAllInOrder, which can be given an Iterable<Matcher> to fuzzy match the order allowing extra nodes before after and between matching parts of the traversal.
- orderedEquals, which can be given an Iterable<Matcher> to exactly match the order of the traversal.
Implementation
Iterable<SemanticsNode> simulatedAccessibilityTraversal({FinderBase<Element>? start, FinderBase<Element>? end, FlutterView? view}) {
TestAsyncUtils.guardSync();
FlutterView? startView;
FlutterView? endView;
if (start != null) {
startView = _controller.viewOf(start);
if (view != null && startView != view) {
throw StateError(
'The start node is not part of the provided view.\n'
'Finder: ${start.toString(describeSelf: true)}\n'
'View of start node: $startView\n'
'Specified view: $view'
);
}
}
if (end != null) {
endView = _controller.viewOf(end);
if (view != null && endView != view) {
throw StateError(
'The end node is not part of the provided view.\n'
'Finder: ${end.toString(describeSelf: true)}\n'
'View of end node: $endView\n'
'Specified view: $view'
);
}
}
if (endView != null && startView != null && endView != startView) {
throw StateError(
'The start and end node are in different views.\n'
'Start finder: ${start!.toString(describeSelf: true)}\n'
'End finder: ${end!.toString(describeSelf: true)}\n'
'View of start node: $startView\n'
'View of end node: $endView'
);
}
final FlutterView actualView = view ?? startView ?? endView ?? _controller.view;
final RenderView renderView = _controller.binding.renderViews.firstWhere((RenderView r) => r.flutterView == actualView);
final List<SemanticsNode> traversal = <SemanticsNode>[];
_traverse(renderView.owner!.semanticsOwner!.rootSemanticsNode!, traversal);
int startIndex = 0;
int endIndex = traversal.length - 1;
if (start != null) {
final SemanticsNode startNode = find(start);
startIndex = traversal.indexOf(startNode);
if (startIndex == -1) {
throw StateError(
'The expected starting node was not found.\n'
'Finder: ${start.toString(describeSelf: true)}\n\n'
'Expected Start Node: $startNode\n\n'
'Traversal: [\n ${traversal.join('\n ')}\n]');
}
}
if (end != null) {
final SemanticsNode endNode = find(end);
endIndex = traversal.indexOf(endNode);
if (endIndex == -1) {
throw StateError(
'The expected ending node was not found.\n'
'Finder: ${end.toString(describeSelf: true)}\n\n'
'Expected End Node: $endNode\n\n'
'Traversal: [\n ${traversal.join('\n ')}\n]');
}
}
return traversal.getRange(startIndex, endIndex + 1);
}