Fix SemanticsFinder for multi-view (#143485)

Fixes https://github.com/flutter/flutter/issues/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.
This commit is contained in:
Michael Goderbauer 2024-02-16 14:24:55 -08:00 committed by GitHub
parent 9a6bda87d9
commit 50862bc04a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 122 additions and 17 deletions

View file

@ -553,7 +553,7 @@ class CommonSemanticsFinders {
return _PredicateSemanticsFinder(
predicate,
describeMatch,
_rootFromView(view),
view,
);
}
@ -689,15 +689,6 @@ class CommonSemanticsFinders {
return pattern == target;
}
}
SemanticsNode _rootFromView(FlutterView? view) {
view ??= TestWidgetsFlutterBinding.instance.platformDispatcher.implicitView;
assert(view != null, 'The given view was not available. Ensure WidgetTester.view is available or pass in a specific view using WidgetTester.viewOf.');
final RenderView renderView = TestWidgetsFlutterBinding.instance.renderViews
.firstWhere((RenderView r) => r.flutterView == view);
return renderView.owner!.semanticsOwner!.rootSemanticsNode!;
}
}
/// Provides lightweight syntax for getting frequently used text range finders.
@ -1065,16 +1056,44 @@ abstract class Finder extends FinderBase<Element> with _LegacyFinderMixin {
/// A base class for creating finders that search the semantics tree.
abstract class SemanticsFinder extends FinderBase<SemanticsNode> {
/// Creates a new [SemanticsFinder] that will search starting at the given
/// `root`.
SemanticsFinder(this.root);
/// Creates a new [SemanticsFinder] that will search within the given [view] or
/// within all views if [view] is null.
SemanticsFinder(this.view);
/// The root of the semantics tree that this finder will search.
final SemanticsNode root;
/// The [FlutterView] whose semantics tree this finder will search.
///
/// If null, the finder will search within all views.
final FlutterView? view;
/// Returns the root [SemanticsNode]s of all the semantics trees that this
/// finder will search.
Iterable<SemanticsNode> get roots {
if (view == null) {
return _allRoots;
}
final RenderView renderView = TestWidgetsFlutterBinding.instance.renderViews
.firstWhere((RenderView r) => r.flutterView == view);
return <SemanticsNode>[
renderView.owner!.semanticsOwner!.rootSemanticsNode!
];
}
@override
Iterable<SemanticsNode> get allCandidates {
return collectAllSemanticsNodesFrom(root);
return roots.expand((SemanticsNode root) => collectAllSemanticsNodesFrom(root));
}
static Iterable<SemanticsNode> get _allRoots {
final List<SemanticsNode> roots = <SemanticsNode>[];
void collectSemanticsRoots(PipelineOwner owner) {
final SemanticsNode? root = owner.semanticsOwner?.rootSemanticsNode;
if (root != null) {
roots.add(root);
}
owner.visitChildren(collectSemanticsRoots);
}
collectSemanticsRoots(TestWidgetsFlutterBinding.instance.rootPipelineOwner);
return roots;
}
}
@ -1539,7 +1558,7 @@ class _ElementPredicateWidgetFinder extends MatchFinder {
class _PredicateSemanticsFinder extends SemanticsFinder
with MatchFinderMixin<SemanticsNode> {
_PredicateSemanticsFinder(this.predicate, DescribeMatchCallback? describeMatch, super.root)
_PredicateSemanticsFinder(this.predicate, DescribeMatchCallback? describeMatch, super.view)
: _describeMatch = describeMatch;
final SemanticsNodePredicate predicate;

View file

@ -190,6 +190,21 @@ void main() {
expect((find.text('View1Child1').hitTestable().evaluate().single.widget as Text).data, 'View1Child1');
expect((find.text('View2Child2').hitTestable().evaluate().single.widget as Text).data, 'View2Child2');
});
testWidgets('simulatedAccessibilityTraversal - startNode and endNode in same view', (WidgetTester tester) async {
await pumpViews(tester: tester);
expect(
tester.semantics.simulatedAccessibilityTraversal(
startNode: find.semantics.byLabel('View2Child1'),
endNode: find.semantics.byLabel('View2Child3'),
).map((SemanticsNode node) => node.label),
<String>[
'View2Child1',
'View2Child2',
'View2Child3',
],
);
});
}
Future<void> pumpViews({required WidgetTester tester}) {

View file

@ -0,0 +1,71 @@
// 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 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'multi_view_testing.dart';
void main() {
testWidgets('can find nodes in an view when no view is specified', (WidgetTester tester) async {
final List<FlutterView> views = <FlutterView>[
for (int i = 0; i < 3; i++)
FakeView(tester.view, viewId: i + 100)
];
await pumpViews(tester: tester, views: views);
expect(find.semantics.byLabel('View0Child0'), findsOne);
expect(find.semantics.byLabel('View1Child1'), findsOne);
expect(find.semantics.byLabel('View2Child2'), findsOne);
});
testWidgets('can find nodes only in specified view', (WidgetTester tester) async {
final List<FlutterView> views = <FlutterView>[
for (int i = 0; i < 3; i++)
FakeView(tester.view, viewId: i + 100)
];
await pumpViews(tester: tester, views: views);
expect(find.semantics.byLabel('View0Child0', view: views[0]), findsOne);
expect(find.semantics.byLabel('View0Child0', view: views[1]), findsNothing);
expect(find.semantics.byLabel('View0Child0', view: views[2]), findsNothing);
expect(find.semantics.byLabel('View1Child1', view: views[0]), findsNothing);
expect(find.semantics.byLabel('View1Child1', view: views[1]), findsOne);
expect(find.semantics.byLabel('View1Child1', view: views[2]), findsNothing);
expect(find.semantics.byLabel('View2Child2', view: views[0]), findsNothing);
expect(find.semantics.byLabel('View2Child2', view: views[1]), findsNothing);
expect(find.semantics.byLabel('View2Child2', view: views[2]), findsOne);
});
}
Future<void> pumpViews({required WidgetTester tester, required List<FlutterView> views}) {
final List<Widget> viewWidgets = <Widget>[
for (int i = 0; i < 3; i++)
View(
view: views[i],
child: Center(
child: Column(
children: <Widget>[
for (int c = 0; c < 5; c++)
Semantics(container: true, child: Text('View${i}Child$c')),
],
),
),
),
];
return tester.pumpWidget(
wrapWithView: false,
Directionality(
textDirection: TextDirection.ltr,
child: ViewCollection(
views: viewWidgets,
),
),
);
}