simulatedAccessibilityTraversal method

Iterable<SemanticsNode> simulatedAccessibilityTraversal({
  1. @Deprecated('Use startNode instead. ' 'This method was originally created before semantics finders were available. ' 'Semantics finders avoid edge cases where some nodes are not discoverable by widget finders and should be preferred for semantics testing. ' 'This feature was deprecated after v3.15.0-15.2.pre.') FinderBase<Element>? start,
  2. @Deprecated('Use endNode instead. ' 'This method was originally created before semantics finders were available. ' 'Semantics finders avoid edge cases where some nodes are not discoverable by widget finders and should be preferred for semantics testing. ' 'This feature was deprecated after v3.15.0-15.2.pre.') FinderBase<Element>? end,
  3. FinderBase<SemanticsNode>? startNode,
  4. FinderBase<SemanticsNode>? endNode,
  5. FlutterView? view,
})

Simulates a traversal of the currently visible semantics tree as if by assistive technologies.

Starts at the node for startNode. If startNode is not provided, then the traversal begins with the first accessible node in the tree. If startNode finds zero elements or more than one element, a StateError will be thrown.

Ends at the node for endNode, inclusive. If endNode is not provided, then the traversal ends with the last accessible node in the currently available tree. If endNode finds zero elements or more than one element, a StateError will be thrown.

If provided, the nodes for endNode and startNode must be part of the same semantics tree, i.e. they must be part of the same view.

If neither startNode or endNode 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({
  @Deprecated(
    'Use startNode instead. '
    'This method was originally created before semantics finders were available. '
    'Semantics finders avoid edge cases where some nodes are not discoverable by widget finders and should be preferred for semantics testing. '
    'This feature was deprecated after v3.15.0-15.2.pre.'
  )
  finders.FinderBase<Element>? start,
  @Deprecated(
    'Use endNode instead. '
    'This method was originally created before semantics finders were available. '
    'Semantics finders avoid edge cases where some nodes are not discoverable by widget finders and should be preferred for semantics testing. '
    'This feature was deprecated after v3.15.0-15.2.pre.'
  )
  finders.FinderBase<Element>? end,
  finders.FinderBase<SemanticsNode>? startNode,
  finders.FinderBase<SemanticsNode>? endNode,
  FlutterView? view,
}) {
  TestAsyncUtils.guardSync();
  assert(
    start == null || startNode == null,
    'Cannot provide both start and startNode. Prefer startNode as start is deprecated.',
  );
  assert(
    end == null || endNode == null,
    'Cannot provide both end and endNode. Prefer endNode as end is deprecated.',
  );

  FlutterView? startView;
  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'
      );
    }
  } else if (startNode != null) {
    final SemanticsOwner owner = startNode.evaluate().single.owner!;
    final RenderView renderView = _controller.binding.renderViews.firstWhere(
      (RenderView render) => render.owner!.semanticsOwner == owner,
    );
    startView = renderView.flutterView;
    if (view != null && startView != view) {
      throw StateError(
        'The start node is not part of the provided view.\n'
        'Finder: ${startNode.toString(describeSelf: true)}\n'
        'View of start node: $startView\n'
        'Specified view: $view'
      );
    }
  }

  FlutterView? endView;
  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'
      );
    }
  } else if (endNode != null) {
    final SemanticsOwner owner = endNode.evaluate().single.owner!;
    final RenderView renderView = _controller.binding.renderViews.firstWhere(
      (RenderView render) => render.owner!.semanticsOwner == owner,
    );
    endView = renderView.flutterView;
    if (view != null && endView != view) {
      throw StateError(
        'The end node is not part of the provided view.\n'
        'Finder: ${endNode.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>[];
  _accessibilityTraversal(
    renderView.owner!.semanticsOwner!.rootSemanticsNode!,
    traversal,
  );

  // Setting the range
  SemanticsNode? node;
  String? errorString;

  int startIndex;
  if (start != null) {
    node = find(start);
    startIndex = traversal.indexOf(node);
    errorString = start.toString(describeSelf: true);
  } else if (startNode != null) {
    node = startNode.evaluate().single;
    startIndex = traversal.indexOf(node);
    errorString = startNode.toString(describeSelf: true);
  } else {
    startIndex = 0;
  }
  if (startIndex == -1) {
    throw StateError(
      'The expected starting node was not found.\n'
      'Finder: $errorString\n\n'
      'Expected Start Node: $node\n\n'
      'Traversal: [\n  ${traversal.join('\n  ')}\n]');
  }

  int? endIndex;
  if (end != null) {
    node = find(end);
    endIndex = traversal.indexOf(node);
    errorString = end.toString(describeSelf: true);
  } else if (endNode != null) {
    node = endNode.evaluate().single;
    endIndex = traversal.indexOf(node);
    errorString = endNode.toString(describeSelf: true);
  }
  if (endIndex == -1) {
    throw StateError(
      'The expected ending node was not found.\n'
      'Finder: $errorString\n\n'
      'Expected End Node: $node\n\n'
      'Traversal: [\n  ${traversal.join('\n  ')}\n]');
  }
  endIndex ??= traversal.length - 1;

  return traversal.getRange(startIndex, endIndex + 1);
}