107866: Add support for verifying SemanticsNode ordering in widget tests (#113133)
This commit is contained in:
parent
e334ac1122
commit
671c532019
@ -6848,7 +6848,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}
|
||||
|
@ -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
|
||||
@ -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
|
||||
@ -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].
|
||||
///
|
||||
|
@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/semantics.dart';
|
||||
@ -20,136 +21,6 @@ class TestDragData {
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('getSemanticsData', () {
|
||||
testWidgets('throws when there are no semantics', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Text('hello'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(() => tester.getSemantics(find.text('hello')), throwsStateError);
|
||||
}, semanticsEnabled: false);
|
||||
|
||||
testWidgets('throws when there are multiple results from the finder', (WidgetTester tester) async {
|
||||
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Row(
|
||||
children: const <Widget>[
|
||||
Text('hello'),
|
||||
Text('hello'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(() => tester.getSemantics(find.text('hello')), throwsStateError);
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Returns the correct SemanticsData', (WidgetTester tester) async {
|
||||
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: OutlinedButton(
|
||||
onPressed: () { },
|
||||
child: const 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);
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Can enable semantics for tests via semanticsEnabled', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: OutlinedButton(
|
||||
onPressed: () { },
|
||||
child: const 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);
|
||||
});
|
||||
|
||||
testWidgets('Returns merged SemanticsData', (WidgetTester tester) async {
|
||||
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
|
||||
const Key key = Key('test');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Semantics(
|
||||
label: 'A',
|
||||
child: Semantics(
|
||||
label: 'B',
|
||||
child: Semantics(
|
||||
key: key,
|
||||
label: 'C',
|
||||
child: Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
||||
final SemanticsData semantics = node.getSemanticsData();
|
||||
expect(semantics.label, 'A\nB\nC');
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
|
||||
testWidgets('Does not return partial semantics', (WidgetTester tester) async {
|
||||
final SemanticsHandle semanticsHandle = tester.ensureSemantics();
|
||||
final Key key = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: MergeSemantics(
|
||||
child: Semantics(
|
||||
container: true,
|
||||
label: 'A',
|
||||
child: Semantics(
|
||||
container: true,
|
||||
key: key,
|
||||
label: 'B',
|
||||
child: Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final SemanticsNode node = tester.getSemantics(find.byKey(key));
|
||||
final SemanticsData semantics = node.getSemanticsData();
|
||||
expect(semantics.label, 'A\nB');
|
||||
semanticsHandle.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'WidgetTester.drag must break the offset into multiple parallel components if '
|
||||
'the drag goes outside the touch slop values',
|
||||
@ -805,4 +676,308 @@ void main() {
|
||||
expect(find.text('Item b-45'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('SemanticsController', () {
|
||||
group('find', () {
|
||||
testWidgets('throws when there are no semantics', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Text('hello'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(() => tester.semantics.find(find.text('hello')), throwsStateError);
|
||||
}, semanticsEnabled: false);
|
||||
|
||||
testWidgets('throws when there are multiple results from the finder', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Row(
|
||||
children: const <Widget>[
|
||||
Text('hello'),
|
||||
Text('hello'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(() => tester.semantics.find(find.text('hello')), throwsStateError);
|
||||
});
|
||||
|
||||
testWidgets('Returns the correct SemanticsData', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: OutlinedButton(
|
||||
onPressed: () { },
|
||||
child: const Text('hello'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final SemanticsNode node = tester.semantics.find(find.text('hello'));
|
||||
final SemanticsData semantics = node.getSemanticsData();
|
||||
expect(semantics.label, 'hello');
|
||||
expect(semantics.hasAction(SemanticsAction.tap), true);
|
||||
expect(semantics.hasFlag(SemanticsFlag.isButton), true);
|
||||
});
|
||||
|
||||
testWidgets('Can enable semantics for tests via semanticsEnabled', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: OutlinedButton(
|
||||
onPressed: () { },
|
||||
child: const Text('hello'),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final SemanticsNode node = tester.semantics.find(find.text('hello'));
|
||||
final SemanticsData semantics = node.getSemanticsData();
|
||||
expect(semantics.label, 'hello');
|
||||
expect(semantics.hasAction(SemanticsAction.tap), true);
|
||||
expect(semantics.hasFlag(SemanticsFlag.isButton), true);
|
||||
});
|
||||
|
||||
testWidgets('Returns merged SemanticsData', (WidgetTester tester) async {
|
||||
const Key key = Key('test');
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: Semantics(
|
||||
label: 'A',
|
||||
child: Semantics(
|
||||
label: 'B',
|
||||
child: Semantics(
|
||||
key: key,
|
||||
label: 'C',
|
||||
child: Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final SemanticsNode node = tester.semantics.find(find.byKey(key));
|
||||
final SemanticsData semantics = node.getSemanticsData();
|
||||
expect(semantics.label, 'A\nB\nC');
|
||||
});
|
||||
|
||||
testWidgets('Does not return partial semantics', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Scaffold(
|
||||
body: MergeSemantics(
|
||||
child: Semantics(
|
||||
container: true,
|
||||
label: 'A',
|
||||
child: Semantics(
|
||||
container: true,
|
||||
key: key,
|
||||
label: 'B',
|
||||
child: Container(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final SemanticsNode node = tester.semantics.find(find.byKey(key));
|
||||
final SemanticsData semantics = node.getSemanticsData();
|
||||
expect(semantics.label, 'A\nB');
|
||||
});
|
||||
});
|
||||
|
||||
group('simulatedTraversal', () {
|
||||
final List<Matcher> fullTraversalMatchers = <Matcher>[
|
||||
containsSemantics(isHeader: true, label: 'Semantics Test'),
|
||||
containsSemantics(isTextField: true),
|
||||
containsSemantics(label: 'Off Switch'),
|
||||
containsSemantics(hasToggledState: true),
|
||||
containsSemantics(label: 'On Switch'),
|
||||
containsSemantics(hasToggledState: true, isToggled: true),
|
||||
containsSemantics(label: "Multiline\nIt's a\nmultiline label!"),
|
||||
containsSemantics(label: 'Slider'),
|
||||
containsSemantics(isSlider: true, value: '50%'),
|
||||
containsSemantics(label: 'Enabled Button'),
|
||||
containsSemantics(isButton: true, label: 'Tap'),
|
||||
containsSemantics(label: 'Disabled Button'),
|
||||
containsSemantics(isButton: true, label: "Don't Tap"),
|
||||
containsSemantics(label: 'Checked Radio'),
|
||||
containsSemantics(hasCheckedState: true, isChecked: true),
|
||||
containsSemantics(label: 'Unchecked Radio'),
|
||||
containsSemantics(hasCheckedState: true, isChecked: false),
|
||||
];
|
||||
|
||||
testWidgets('produces expected traversal', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||
|
||||
expect(
|
||||
tester.semantics.simulatedAccessibilityTraversal(),
|
||||
orderedEquals(fullTraversalMatchers));
|
||||
});
|
||||
|
||||
testWidgets('starts traversal at semantics node for `start`', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||
|
||||
// We're expecting the traversal to start where the slider is.
|
||||
final List<Matcher> expectedMatchers = <Matcher>[...fullTraversalMatchers]..removeRange(0, 8);
|
||||
|
||||
expect(
|
||||
tester.semantics.simulatedAccessibilityTraversal(start: find.byType(Slider)),
|
||||
orderedEquals(expectedMatchers));
|
||||
});
|
||||
|
||||
testWidgets('throws StateError if `start` not found in traversal', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||
|
||||
// We look for a SingleChildScrollView since the view itself isn't
|
||||
// important for accessiblity, so it won't show up in the traversal
|
||||
expect(
|
||||
() => tester.semantics.simulatedAccessibilityTraversal(start: find.byType(SingleChildScrollView)),
|
||||
throwsA(isA<StateError>()),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('ends traversal at semantics node for `end`', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||
|
||||
// We're expecting the traversal to end where the slider is, inclusive.
|
||||
final Iterable<Matcher> expectedMatchers = <Matcher>[...fullTraversalMatchers].getRange(0, 9);
|
||||
|
||||
expect(
|
||||
tester.semantics.simulatedAccessibilityTraversal(end: find.byType(Slider)),
|
||||
orderedEquals(expectedMatchers));
|
||||
});
|
||||
|
||||
testWidgets('throws StateError if `end` not found in traversal', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||
|
||||
// We look for a SingleChildScrollView since the view itself isn't
|
||||
// important for semantics, so it won't show up in the traversal
|
||||
expect(
|
||||
() => tester.semantics.simulatedAccessibilityTraversal(end: find.byType(SingleChildScrollView)),
|
||||
throwsA(isA<StateError>()),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('returns traversal between `start` and `end` if both are provided', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||
|
||||
// We're expecting the traversal to start at the text field and end at the slider.
|
||||
final Iterable<Matcher> expectedMatchers = <Matcher>[...fullTraversalMatchers].getRange(1, 9);
|
||||
|
||||
expect(
|
||||
tester.semantics.simulatedAccessibilityTraversal(
|
||||
start: find.byType(TextField),
|
||||
end: find.byType(Slider),
|
||||
),
|
||||
orderedEquals(expectedMatchers));
|
||||
});
|
||||
|
||||
testWidgets('can do fuzzy traversal match with `containsAllInOrder`', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||
|
||||
// Grab a sample of the matchers to validate that not every matcher is
|
||||
// needed to validate a traversal when using `containsAllInOrder`.
|
||||
final Iterable<Matcher> expectedMatchers = <Matcher>[...fullTraversalMatchers]
|
||||
..removeAt(0)
|
||||
..removeLast()
|
||||
..mapIndexed<Matcher?>((int i, Matcher element) => i.isEven ? element : null)
|
||||
.whereNotNull();
|
||||
|
||||
expect(
|
||||
tester.semantics.simulatedAccessibilityTraversal(),
|
||||
containsAllInOrder(expectedMatchers));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _SemanticsTestWidget extends StatelessWidget {
|
||||
const _SemanticsTestWidget();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Semantics Test')),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
const _SemanticsTestCard(
|
||||
label: 'TextField',
|
||||
widget: TextField(),
|
||||
),
|
||||
_SemanticsTestCard(
|
||||
label: 'Off Switch',
|
||||
widget: Switch(value: false, onChanged: (bool value) {}),
|
||||
),
|
||||
_SemanticsTestCard(
|
||||
label: 'On Switch',
|
||||
widget: Switch(value: true, onChanged: (bool value) {}),
|
||||
),
|
||||
const _SemanticsTestCard(
|
||||
label: 'Multiline',
|
||||
widget: Text("It's a\nmultiline label!", maxLines: 2),
|
||||
),
|
||||
_SemanticsTestCard(
|
||||
label: 'Slider',
|
||||
widget: Slider(value: .5, onChanged: (double value) {}),
|
||||
),
|
||||
_SemanticsTestCard(
|
||||
label: 'Enabled Button',
|
||||
widget: TextButton(onPressed: () {}, child: const Text('Tap')),
|
||||
),
|
||||
const _SemanticsTestCard(
|
||||
label: 'Disabled Button',
|
||||
widget: TextButton(onPressed: null, child: Text("Don't Tap")),
|
||||
),
|
||||
_SemanticsTestCard(
|
||||
label: 'Checked Radio',
|
||||
widget: Radio<String>(
|
||||
value: 'checked',
|
||||
groupValue: 'checked',
|
||||
onChanged: (String? value) {},
|
||||
),
|
||||
),
|
||||
_SemanticsTestCard(
|
||||
label: 'Unchecked Radio',
|
||||
widget: Radio<String>(
|
||||
value: 'unchecked',
|
||||
groupValue: 'checked',
|
||||
onChanged: (String? value) {},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SemanticsTestCard extends StatelessWidget {
|
||||
const _SemanticsTestCard({required this.label, required this.widget});
|
||||
|
||||
final String label;
|
||||
final Widget widget;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(label),
|
||||
trailing: SizedBox(width: 200, child: widget),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user