Skip to content

Commit 50862bc

Browse files
authored
Fix SemanticsFinder for multi-view (#143485)
Fixes flutter/flutter#143405. It was counter-intuitive that a SemanticsFinder without specifying a FlutterView would only search the nodes in the default view. This change makes it so that when no view is specified the semantics trees of all known FlutterViews are searched.
1 parent 9a6bda8 commit 50862bc

File tree

3 files changed

+122
-17
lines changed

3 files changed

+122
-17
lines changed

packages/flutter_test/lib/src/finders.dart

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ class CommonSemanticsFinders {
553553
return _PredicateSemanticsFinder(
554554
predicate,
555555
describeMatch,
556-
_rootFromView(view),
556+
view,
557557
);
558558
}
559559

@@ -689,15 +689,6 @@ class CommonSemanticsFinders {
689689
return pattern == target;
690690
}
691691
}
692-
693-
SemanticsNode _rootFromView(FlutterView? view) {
694-
view ??= TestWidgetsFlutterBinding.instance.platformDispatcher.implicitView;
695-
assert(view != null, 'The given view was not available. Ensure WidgetTester.view is available or pass in a specific view using WidgetTester.viewOf.');
696-
final RenderView renderView = TestWidgetsFlutterBinding.instance.renderViews
697-
.firstWhere((RenderView r) => r.flutterView == view);
698-
699-
return renderView.owner!.semanticsOwner!.rootSemanticsNode!;
700-
}
701692
}
702693

703694
/// Provides lightweight syntax for getting frequently used text range finders.
@@ -1065,16 +1056,44 @@ abstract class Finder extends FinderBase<Element> with _LegacyFinderMixin {
10651056

10661057
/// A base class for creating finders that search the semantics tree.
10671058
abstract class SemanticsFinder extends FinderBase<SemanticsNode> {
1068-
/// Creates a new [SemanticsFinder] that will search starting at the given
1069-
/// `root`.
1070-
SemanticsFinder(this.root);
1059+
/// Creates a new [SemanticsFinder] that will search within the given [view] or
1060+
/// within all views if [view] is null.
1061+
SemanticsFinder(this.view);
10711062

1072-
/// The root of the semantics tree that this finder will search.
1073-
final SemanticsNode root;
1063+
/// The [FlutterView] whose semantics tree this finder will search.
1064+
///
1065+
/// If null, the finder will search within all views.
1066+
final FlutterView? view;
1067+
1068+
/// Returns the root [SemanticsNode]s of all the semantics trees that this
1069+
/// finder will search.
1070+
Iterable<SemanticsNode> get roots {
1071+
if (view == null) {
1072+
return _allRoots;
1073+
}
1074+
final RenderView renderView = TestWidgetsFlutterBinding.instance.renderViews
1075+
.firstWhere((RenderView r) => r.flutterView == view);
1076+
return <SemanticsNode>[
1077+
renderView.owner!.semanticsOwner!.rootSemanticsNode!
1078+
];
1079+
}
10741080

10751081
@override
10761082
Iterable<SemanticsNode> get allCandidates {
1077-
return collectAllSemanticsNodesFrom(root);
1083+
return roots.expand((SemanticsNode root) => collectAllSemanticsNodesFrom(root));
1084+
}
1085+
1086+
static Iterable<SemanticsNode> get _allRoots {
1087+
final List<SemanticsNode> roots = <SemanticsNode>[];
1088+
void collectSemanticsRoots(PipelineOwner owner) {
1089+
final SemanticsNode? root = owner.semanticsOwner?.rootSemanticsNode;
1090+
if (root != null) {
1091+
roots.add(root);
1092+
}
1093+
owner.visitChildren(collectSemanticsRoots);
1094+
}
1095+
collectSemanticsRoots(TestWidgetsFlutterBinding.instance.rootPipelineOwner);
1096+
return roots;
10781097
}
10791098
}
10801099

@@ -1539,7 +1558,7 @@ class _ElementPredicateWidgetFinder extends MatchFinder {
15391558

15401559
class _PredicateSemanticsFinder extends SemanticsFinder
15411560
with MatchFinderMixin<SemanticsNode> {
1542-
_PredicateSemanticsFinder(this.predicate, DescribeMatchCallback? describeMatch, super.root)
1561+
_PredicateSemanticsFinder(this.predicate, DescribeMatchCallback? describeMatch, super.view)
15431562
: _describeMatch = describeMatch;
15441563

15451564
final SemanticsNodePredicate predicate;

packages/flutter_test/test/multi_view_controller_test.dart

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,21 @@ void main() {
190190
expect((find.text('View1Child1').hitTestable().evaluate().single.widget as Text).data, 'View1Child1');
191191
expect((find.text('View2Child2').hitTestable().evaluate().single.widget as Text).data, 'View2Child2');
192192
});
193+
194+
testWidgets('simulatedAccessibilityTraversal - startNode and endNode in same view', (WidgetTester tester) async {
195+
await pumpViews(tester: tester);
196+
expect(
197+
tester.semantics.simulatedAccessibilityTraversal(
198+
startNode: find.semantics.byLabel('View2Child1'),
199+
endNode: find.semantics.byLabel('View2Child3'),
200+
).map((SemanticsNode node) => node.label),
201+
<String>[
202+
'View2Child1',
203+
'View2Child2',
204+
'View2Child3',
205+
],
206+
);
207+
});
193208
}
194209

195210
Future<void> pumpViews({required WidgetTester tester}) {
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:ui';
6+
7+
import 'package:flutter/material.dart';
8+
import 'package:flutter_test/flutter_test.dart';
9+
10+
import 'multi_view_testing.dart';
11+
12+
void main() {
13+
testWidgets('can find nodes in an view when no view is specified', (WidgetTester tester) async {
14+
final List<FlutterView> views = <FlutterView>[
15+
for (int i = 0; i < 3; i++)
16+
FakeView(tester.view, viewId: i + 100)
17+
];
18+
await pumpViews(tester: tester, views: views);
19+
20+
expect(find.semantics.byLabel('View0Child0'), findsOne);
21+
expect(find.semantics.byLabel('View1Child1'), findsOne);
22+
expect(find.semantics.byLabel('View2Child2'), findsOne);
23+
});
24+
25+
testWidgets('can find nodes only in specified view', (WidgetTester tester) async {
26+
final List<FlutterView> views = <FlutterView>[
27+
for (int i = 0; i < 3; i++)
28+
FakeView(tester.view, viewId: i + 100)
29+
];
30+
await pumpViews(tester: tester, views: views);
31+
32+
expect(find.semantics.byLabel('View0Child0', view: views[0]), findsOne);
33+
expect(find.semantics.byLabel('View0Child0', view: views[1]), findsNothing);
34+
expect(find.semantics.byLabel('View0Child0', view: views[2]), findsNothing);
35+
36+
expect(find.semantics.byLabel('View1Child1', view: views[0]), findsNothing);
37+
expect(find.semantics.byLabel('View1Child1', view: views[1]), findsOne);
38+
expect(find.semantics.byLabel('View1Child1', view: views[2]), findsNothing);
39+
40+
expect(find.semantics.byLabel('View2Child2', view: views[0]), findsNothing);
41+
expect(find.semantics.byLabel('View2Child2', view: views[1]), findsNothing);
42+
expect(find.semantics.byLabel('View2Child2', view: views[2]), findsOne);
43+
});
44+
}
45+
46+
Future<void> pumpViews({required WidgetTester tester, required List<FlutterView> views}) {
47+
final List<Widget> viewWidgets = <Widget>[
48+
for (int i = 0; i < 3; i++)
49+
View(
50+
view: views[i],
51+
child: Center(
52+
child: Column(
53+
children: <Widget>[
54+
for (int c = 0; c < 5; c++)
55+
Semantics(container: true, child: Text('View${i}Child$c')),
56+
],
57+
),
58+
),
59+
),
60+
];
61+
62+
return tester.pumpWidget(
63+
wrapWithView: false,
64+
Directionality(
65+
textDirection: TextDirection.ltr,
66+
child: ViewCollection(
67+
views: viewWidgets,
68+
),
69+
),
70+
);
71+
}

0 commit comments

Comments
 (0)