Reland simulatedAccessibilityTraversal fix (#143527)
Relands https://github.com/flutter/flutter/pull/143386
Which was reverted in https://github.com/flutter/flutter/pull/143523
Fixes https://github.com/flutter/flutter/issues/143173
Unblocks https://github.com/flutter/flutter/pull/143485
â â¡ï¸ Update from the revert is in this commit: 1e6853291e
This commit is contained in:
parent
178898e45d
commit
ea3d066237
@ -0,0 +1,79 @@
|
|||||||
|
# Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
# Use of this source code is governed by a BSD-style license that can be
|
||||||
|
# found in the LICENSE file.
|
||||||
|
|
||||||
|
# For details regarding the *Flutter Fix* feature, see
|
||||||
|
# https://flutter.dev/docs/development/tools/flutter-fix
|
||||||
|
|
||||||
|
# Please add new fixes to the top of the file, separated by one blank line
|
||||||
|
# from other fixes. In a comment, include a link to the PR where the change
|
||||||
|
# requiring the fix was made.
|
||||||
|
|
||||||
|
# Every fix must be tested. See the
|
||||||
|
# flutter/packages/flutter_test/test_fixes/README.md file for instructions
|
||||||
|
# on testing these data driven fixes.
|
||||||
|
|
||||||
|
# For documentation about this file format, see
|
||||||
|
# https://dart.dev/go/data-driven-fixes.
|
||||||
|
|
||||||
|
# * Fixes in this file are for the flutter_test/controller.dart file. *
|
||||||
|
|
||||||
|
version: 1
|
||||||
|
transforms:
|
||||||
|
# Changes made in TBD
|
||||||
|
- title: "Migrate to startNode and endNode."
|
||||||
|
date: 2024-02-13
|
||||||
|
element:
|
||||||
|
uris: [ 'flutter_test.dart' ]
|
||||||
|
method: simulatedAccessibilityTraversal
|
||||||
|
inClass: SemanticsController
|
||||||
|
oneOf:
|
||||||
|
- if: "start != '' && end != ''"
|
||||||
|
changes:
|
||||||
|
- kind: 'addParameter'
|
||||||
|
index: 2
|
||||||
|
name: 'startNode'
|
||||||
|
style: optional_named
|
||||||
|
argumentValue:
|
||||||
|
expression: '{% start %}'
|
||||||
|
requiredIf: "start != '' && end != ''"
|
||||||
|
- kind: 'addParameter'
|
||||||
|
index: 3
|
||||||
|
name: 'endNode'
|
||||||
|
style: optional_named
|
||||||
|
argumentValue:
|
||||||
|
expression: '{% end %}'
|
||||||
|
requiredIf: "start != '' && end != ''"
|
||||||
|
- kind: 'removeParameter'
|
||||||
|
name: 'start'
|
||||||
|
- kind: 'removeParameter'
|
||||||
|
name: 'end'
|
||||||
|
- if: "start != '' && end == ''"
|
||||||
|
changes:
|
||||||
|
- kind: 'addParameter'
|
||||||
|
index: 2
|
||||||
|
name: 'startNode'
|
||||||
|
style: optional_named
|
||||||
|
argumentValue:
|
||||||
|
expression: '{% start %}'
|
||||||
|
requiredIf: "start != '' && end == ''"
|
||||||
|
- kind: 'removeParameter'
|
||||||
|
name: 'start'
|
||||||
|
- if: "start == '' && end != ''"
|
||||||
|
changes:
|
||||||
|
- kind: 'addParameter'
|
||||||
|
index: 2
|
||||||
|
name: 'endNode'
|
||||||
|
style: optional_named
|
||||||
|
argumentValue:
|
||||||
|
expression: '{% end %}'
|
||||||
|
requiredIf: "start == '' && end != ''"
|
||||||
|
- kind: 'removeParameter'
|
||||||
|
name: 'end'
|
||||||
|
variables:
|
||||||
|
start:
|
||||||
|
kind: 'fragment'
|
||||||
|
value: 'arguments[start]'
|
||||||
|
end:
|
||||||
|
kind: 'fragment'
|
||||||
|
value: 'arguments[end]'
|
@ -183,8 +183,14 @@ class SemanticsController {
|
|||||||
FlutterView? view,
|
FlutterView? view,
|
||||||
}) {
|
}) {
|
||||||
TestAsyncUtils.guardSync();
|
TestAsyncUtils.guardSync();
|
||||||
assert(start == null || startNode == null, 'Cannot provide both start and startNode. Prefer startNode as start is deprecated.');
|
assert(
|
||||||
assert(end == null || endNode == null, 'Cannot provide both end and endNode. Prefer endNode as end is deprecated.');
|
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;
|
FlutterView? startView;
|
||||||
if (start != null) {
|
if (start != null) {
|
||||||
@ -197,8 +203,7 @@ class SemanticsController {
|
|||||||
'Specified view: $view'
|
'Specified view: $view'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
} else if (startNode != null) {
|
||||||
if (startNode != null) {
|
|
||||||
final SemanticsOwner owner = startNode.evaluate().single.owner!;
|
final SemanticsOwner owner = startNode.evaluate().single.owner!;
|
||||||
final RenderView renderView = _controller.binding.renderViews.firstWhere(
|
final RenderView renderView = _controller.binding.renderViews.firstWhere(
|
||||||
(RenderView render) => render.owner!.semanticsOwner == owner,
|
(RenderView render) => render.owner!.semanticsOwner == owner,
|
||||||
@ -206,9 +211,9 @@ class SemanticsController {
|
|||||||
startView = renderView.flutterView;
|
startView = renderView.flutterView;
|
||||||
if (view != null && startView != view) {
|
if (view != null && startView != view) {
|
||||||
throw StateError(
|
throw StateError(
|
||||||
'The end node is not part of the provided view.\n'
|
'The start node is not part of the provided view.\n'
|
||||||
'Finder: ${startNode.toString(describeSelf: true)}\n'
|
'Finder: ${startNode.toString(describeSelf: true)}\n'
|
||||||
'View of end node: $startView\n'
|
'View of start node: $startView\n'
|
||||||
'Specified view: $view'
|
'Specified view: $view'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -225,8 +230,7 @@ class SemanticsController {
|
|||||||
'Specified view: $view'
|
'Specified view: $view'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
} else if (endNode != null) {
|
||||||
if (endNode != null) {
|
|
||||||
final SemanticsOwner owner = endNode.evaluate().single.owner!;
|
final SemanticsOwner owner = endNode.evaluate().single.owner!;
|
||||||
final RenderView renderView = _controller.binding.renderViews.firstWhere(
|
final RenderView renderView = _controller.binding.renderViews.firstWhere(
|
||||||
(RenderView render) => render.owner!.semanticsOwner == owner,
|
(RenderView render) => render.owner!.semanticsOwner == owner,
|
||||||
@ -261,32 +265,48 @@ class SemanticsController {
|
|||||||
traversal,
|
traversal,
|
||||||
);
|
);
|
||||||
|
|
||||||
int startIndex = 0;
|
// Setting the range
|
||||||
int endIndex = traversal.length - 1;
|
SemanticsNode? node;
|
||||||
|
String? errorString;
|
||||||
|
|
||||||
|
int startIndex;
|
||||||
if (start != null) {
|
if (start != null) {
|
||||||
final SemanticsNode startNode = find(start);
|
node = find(start);
|
||||||
startIndex = traversal.indexOf(startNode);
|
startIndex = traversal.indexOf(node);
|
||||||
if (startIndex == -1) {
|
errorString = start.toString(describeSelf: true);
|
||||||
throw StateError(
|
} else if (startNode != null) {
|
||||||
'The expected starting node was not found.\n'
|
node = startNode.evaluate().single;
|
||||||
'Finder: ${start.toString(describeSelf: true)}\n\n'
|
startIndex = traversal.indexOf(node);
|
||||||
'Expected Start Node: $startNode\n\n'
|
errorString = startNode.toString(describeSelf: true);
|
||||||
'Traversal: [\n ${traversal.join('\n ')}\n]');
|
} 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) {
|
if (end != null) {
|
||||||
final SemanticsNode endNode = find(end);
|
node = find(end);
|
||||||
endIndex = traversal.indexOf(endNode);
|
endIndex = traversal.indexOf(node);
|
||||||
if (endIndex == -1) {
|
errorString = end.toString(describeSelf: true);
|
||||||
throw StateError(
|
} else if (endNode != null) {
|
||||||
'The expected ending node was not found.\n'
|
node = endNode.evaluate().single;
|
||||||
'Finder: ${end.toString(describeSelf: true)}\n\n'
|
endIndex = traversal.indexOf(node);
|
||||||
'Expected End Node: $endNode\n\n'
|
errorString = endNode.toString(describeSelf: true);
|
||||||
'Traversal: [\n ${traversal.join('\n ')}\n]');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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);
|
return traversal.getRange(startIndex, endIndex + 1);
|
||||||
}
|
}
|
||||||
|
@ -920,6 +920,42 @@ void main() {
|
|||||||
orderedEquals(expectedMatchers));
|
orderedEquals(expectedMatchers));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('simulatedAccessibilityTraversal end Index supports empty traversal', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: Column(), // No nodes!
|
||||||
|
),
|
||||||
|
));
|
||||||
|
expect(
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal().map((SemanticsNode node) => node.label),
|
||||||
|
<String>[],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('starts traversal at semantics node for `startNode`', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
for (int c = 0; c < 5; c++)
|
||||||
|
Semantics(container: true, child: Text('Child$c')),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
expect(
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal(
|
||||||
|
startNode: find.semantics.byLabel('Child1'),
|
||||||
|
).map((SemanticsNode node) => node.label),
|
||||||
|
<String>[
|
||||||
|
'Child1',
|
||||||
|
'Child2',
|
||||||
|
'Child3',
|
||||||
|
'Child4',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('throws StateError if `start` not found in traversal', (WidgetTester tester) async {
|
testWidgets('throws StateError if `start` not found in traversal', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||||
|
|
||||||
@ -931,6 +967,23 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('throws StateError if `startNode` not found in traversal', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
for (int c = 0; c < 5; c++)
|
||||||
|
Semantics(container: true, child: Text('Child$c')),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
expect(
|
||||||
|
() => tester.semantics.simulatedAccessibilityTraversal(startNode: find.semantics.byLabel('Child20')),
|
||||||
|
throwsA(isA<StateError>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('ends traversal at semantics node for `end`', (WidgetTester tester) async {
|
testWidgets('ends traversal at semantics node for `end`', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||||
|
|
||||||
@ -942,6 +995,28 @@ void main() {
|
|||||||
orderedEquals(expectedMatchers));
|
orderedEquals(expectedMatchers));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('ends traversal at semantics node for `endNode`', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
for (int c = 0; c < 5; c++)
|
||||||
|
Semantics(container: true, child: Text('Child$c')),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
expect(
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal(
|
||||||
|
endNode: find.semantics.byLabel('Child1'),
|
||||||
|
).map((SemanticsNode node) => node.label),
|
||||||
|
<String>[
|
||||||
|
'Child0',
|
||||||
|
'Child1',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('throws StateError if `end` not found in traversal', (WidgetTester tester) async {
|
testWidgets('throws StateError if `end` not found in traversal', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||||
|
|
||||||
@ -953,6 +1028,23 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('throws StateError if `endNode` not found in traversal', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
for (int c = 0; c < 5; c++)
|
||||||
|
Semantics(container: true, child: Text('Child$c')),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
expect(
|
||||||
|
() => tester.semantics.simulatedAccessibilityTraversal(endNode: find.semantics.byLabel('Child20')),
|
||||||
|
throwsA(isA<StateError>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('returns traversal between `start` and `end` if both are provided', (WidgetTester tester) async {
|
testWidgets('returns traversal between `start` and `end` if both are provided', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||||
|
|
||||||
@ -967,6 +1059,30 @@ void main() {
|
|||||||
orderedEquals(expectedMatchers));
|
orderedEquals(expectedMatchers));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('returns traversal between `startNode` and `endNode` if both are provided', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(MaterialApp(
|
||||||
|
home: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
for (int c = 0; c < 5; c++)
|
||||||
|
Semantics(container: true, child: Text('Child$c')),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
expect(
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal(
|
||||||
|
startNode: find.semantics.byLabel('Child1'),
|
||||||
|
endNode: find.semantics.byLabel('Child3'),
|
||||||
|
).map((SemanticsNode node) => node.label),
|
||||||
|
<String>[
|
||||||
|
'Child1',
|
||||||
|
'Child2',
|
||||||
|
'Child3',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('can do fuzzy traversal match with `containsAllInOrder`', (WidgetTester tester) async {
|
testWidgets('can do fuzzy traversal match with `containsAllInOrder`', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget()));
|
||||||
|
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Generic reference variables.
|
||||||
|
finders.FinderBase<Element> theStart;
|
||||||
|
finders.FinderBase<Element> theEnd;
|
||||||
|
|
||||||
|
testWidgets('simulatedAccessibilityTraversal', (WidgetTester tester) async {
|
||||||
|
// Changes made in https://github.com/flutter/flutter/pull/143386
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal();
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal(start: theStart);
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal(end: theEnd);
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal(start: theStart, end: theEnd);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Generic reference variables.
|
||||||
|
finders.FinderBase<Element> theStart;
|
||||||
|
finders.FinderBase<Element> theEnd;
|
||||||
|
|
||||||
|
testWidgets('simulatedAccessibilityTraversal', (WidgetTester tester) async {
|
||||||
|
// Changes made in https://github.com/flutter/flutter/pull/143386
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal();
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal(startNode: theStart);
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal(endNode: theEnd);
|
||||||
|
tester.semantics.simulatedAccessibilityTraversal(startNode: theStart, endNode: theEnd);
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user