simulatedAccessibilityTraversal method

Iterable<SemanticsNode> simulatedAccessibilityTraversal(
  1. {FinderBase<Element>? start,
  2. FinderBase<Element>? end,
  3. 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:

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);
}