Thanks to visit codestin.com
Credit goes to github.com

Skip to content

107866: Add support for verifying SemanticsNode ordering in widget tests #113133

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/flutter/lib/src/widgets/basic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6847,7 +6847,7 @@ class MetaData extends SingleChildRenderObjectWidget {
/// A widget that annotates the widget tree with a description of the meaning of
/// the widgets.
///
/// Used by accessibility tools, search engines, and other semantic analysis
/// Used by assitive technologies, search engines, and other semantic analysis
/// software to determine the meaning of the application.
///
/// {@youtube 560 315 https://www.youtube.com/watch?v=NvtMt_DtFrQ}
Expand Down
242 changes: 218 additions & 24 deletions packages/flutter_test/lib/src/controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,202 @@ const double kDragSlopDefault = 20.0;

const String _defaultPlatform = kIsWeb ? 'web' : 'android';

/// Class that programatically interacts with the [Semantics] tree.
///
/// Allows for testing of the [Semantics] tree, which is used by assistive
/// technology, search engines, and other analysis software to determine the
/// meaning of an application.
///
/// Should be accessed through [WidgetController.semantics]. If no custom
/// implementation is provided, a default [SemanticsController] will be created.
class SemanticsController {
/// Creates a [SemanticsController] that uses the given binding. Will be
/// automatically created as part of instantiating a [WidgetController], but
/// a custom implementation can be passed via the [WidgetController] constructor.
SemanticsController._(WidgetsBinding binding) : _binding = binding;

static final int _scrollingActions =
SemanticsAction.scrollUp.index |
SemanticsAction.scrollDown.index |
SemanticsAction.scrollLeft.index |
SemanticsAction.scrollRight.index;

/// Based on Android's FOCUSABLE_FLAGS. See [flutter/engine/AccessibilityBridge.java](https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/view/AccessibilityBridge.java).
static final int _importantFlagsForAccessibility =
SemanticsFlag.hasCheckedState.index |
SemanticsFlag.hasToggledState.index |
SemanticsFlag.hasEnabledState.index |
SemanticsFlag.isButton.index |
SemanticsFlag.isTextField.index |
SemanticsFlag.isFocusable.index |
SemanticsFlag.isSlider.index |
SemanticsFlag.isInMutuallyExclusiveGroup.index;

final WidgetsBinding _binding;

/// Attempts to find the [SemanticsNode] of first result from `finder`.
///
/// If the object identified by the finder doesn't own its semantic node,
/// 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.
///
/// If the [SemanticsNode] of the object identified by the finder is
/// force-merged into an ancestor (e.g. via the [MergeSemantics] widget)
/// the node into which it is merged is returned. That node will include
/// all the semantics information of the nodes merged into it.
///
/// Will throw a [StateError] if the finder returns more than one element or
/// if no semantics are found or are not enabled.
SemanticsNode find(Finder finder) {
TestAsyncUtils.guardSync();
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 || result.isMergedIntoParent)) {
renderObject = renderObject.parent as RenderObject?;
result = renderObject?.debugSemantics;
}
if (result == null) {
throw StateError('No Semantics data found.');
}
return result;
}

/// 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.
///
/// 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(MyWidget());
///
/// expect(
/// tester.semantics.simulatedAccessibilityTraversal(),
/// containsAllInOrder([
/// 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
Iterable<SemanticsNode> simulatedAccessibilityTraversal({Finder? start, Finder? end}) {
TestAsyncUtils.guardSync();
final List<SemanticsNode> traversal = <SemanticsNode>[];
_traverse(_binding.pipelineOwner.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.description}\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.description}\n\n'
'Expected End Node: $endNode\n\n'
'Traversal: [\n ${traversal.join('\n ')}\n]');
}
}

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

/// Recursive depth first traversal of the specified `node`, adding nodes
/// that are important for semantics to the `traversal` list.
void _traverse(SemanticsNode node, List<SemanticsNode> traversal){
if (_isImportantForAccessibility(node)) {
traversal.add(node);
}

final List<SemanticsNode> children = node.debugListChildrenInOrder(DebugSemanticsDumpOrder.traversalOrder);
for (final SemanticsNode child in children) {
_traverse(child, traversal);
}
}

/// Whether or not the node is important for semantics. Should match most cases
/// on the platforms, but certain edge cases will be inconsisent.
///
/// Based on:
///
/// * [flutter/engine/AccessibilityBridge.java#SemanticsNode.isFocusable()](https://github.com/flutter/engine/blob/main/shell/platform/android/io/flutter/view/AccessibilityBridge.java#L2641)
/// * [flutter/engine/SemanticsObject.mm#SemanticsObject.isAccessibilityElement](https://github.com/flutter/engine/blob/main/shell/platform/darwin/ios/framework/Source/SemanticsObject.mm#L449)
bool _isImportantForAccessibility(SemanticsNode node) {
// If the node scopes a route, it doesn't matter what other flags/actions it
// has, it is _not_ important for accessibility, so we short circuit.
if (node.hasFlag(SemanticsFlag.scopesRoute)) {
return false;
}

final bool hasNonScrollingAction = node.getSemanticsData().actions & ~_scrollingActions != 0;
if (hasNonScrollingAction) {
return true;
}

final bool hasImportantFlag = node.getSemanticsData().flags & _importantFlagsForAccessibility != 0;
if (hasImportantFlag) {
return true;
}

final bool hasContent = node.label.isNotEmpty || node.value.isNotEmpty || node.hint.isNotEmpty;
if (hasContent) {
return true;
}

return false;
}
}

/// Class that programmatically interacts with widgets.
///
/// For a variant of this class suited specifically for unit tests, see
Expand All @@ -32,11 +228,30 @@ const String _defaultPlatform = kIsWeb ? 'web' : 'android';
/// Concrete subclasses must implement the [pump] method.
abstract class WidgetController {
/// Creates a widget controller that uses the given binding.
WidgetController(this.binding);
WidgetController(this.binding)
: _semantics = SemanticsController._(binding);

/// A reference to the current instance of the binding.
final WidgetsBinding binding;

/// Provides access to a [SemanticsController] for testing anything related to
/// the [Semantics] tree.
///
/// Assistive technologies, search engines, and other analysis tools all make
/// use of the [Semantics] tree to determine the meaning of an application.
/// If semantics has been disabled for the test, this will throw a [StateError].
SemanticsController get semantics {
if (binding.pipelineOwner.semanticsOwner == null) {
throw StateError(
'Semantics are not enabled. Enable them by passing '
'`semanticsEnabled: true` to `testWidgets`, or by manually creating a '
'`SemanticsHandle` with `WidgetController.ensureSemantics()`.');
}

return _semantics;
}
final SemanticsController _semantics;

// FINDER API

// TODO(ianh): verify that the return values are of type T and throw
Expand Down Expand Up @@ -1257,29 +1472,8 @@ abstract class WidgetController {
///
/// 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 || result.isMergedIntoParent)) {
renderObject = renderObject.parent as RenderObject?;
result = renderObject?.debugSemantics;
}
if (result == null) {
throw StateError('No Semantics data found.');
}
return result;
}
// TODO(pdblasi-google): Deprecate this and point references to semantics.find. See https://github.com/flutter/flutter/issues/112670.
SemanticsNode getSemantics(Finder finder) => semantics.find(finder);

/// Enable semantics in a test by creating a [SemanticsHandle].
///
Expand Down
Loading