From 50862bc04acdb1aca62125cfc5d9d41ba436fd57 Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Fri, 16 Feb 2024 14:24:55 -0800 Subject: [PATCH] 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. --- packages/flutter_test/lib/src/finders.dart | 53 +++++++++----- .../test/multi_view_controller_test.dart | 15 ++++ .../test/semantics_finder_test.dart | 71 +++++++++++++++++++ 3 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 packages/flutter_test/test/semantics_finder_test.dart diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart index d3a9584a6e0..98a23c40185 100644 --- a/packages/flutter_test/lib/src/finders.dart +++ b/packages/flutter_test/lib/src/finders.dart @@ -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 with _LegacyFinderMixin { /// A base class for creating finders that search the semantics tree. abstract class SemanticsFinder extends FinderBase { - /// 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 get roots { + if (view == null) { + return _allRoots; + } + final RenderView renderView = TestWidgetsFlutterBinding.instance.renderViews + .firstWhere((RenderView r) => r.flutterView == view); + return [ + renderView.owner!.semanticsOwner!.rootSemanticsNode! + ]; + } @override Iterable get allCandidates { - return collectAllSemanticsNodesFrom(root); + return roots.expand((SemanticsNode root) => collectAllSemanticsNodesFrom(root)); + } + + static Iterable get _allRoots { + final List roots = []; + 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 { - _PredicateSemanticsFinder(this.predicate, DescribeMatchCallback? describeMatch, super.root) + _PredicateSemanticsFinder(this.predicate, DescribeMatchCallback? describeMatch, super.view) : _describeMatch = describeMatch; final SemanticsNodePredicate predicate; diff --git a/packages/flutter_test/test/multi_view_controller_test.dart b/packages/flutter_test/test/multi_view_controller_test.dart index fc9ec176095..6aedf2e4a00 100644 --- a/packages/flutter_test/test/multi_view_controller_test.dart +++ b/packages/flutter_test/test/multi_view_controller_test.dart @@ -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), + [ + 'View2Child1', + 'View2Child2', + 'View2Child3', + ], + ); + }); } Future pumpViews({required WidgetTester tester}) { diff --git a/packages/flutter_test/test/semantics_finder_test.dart b/packages/flutter_test/test/semantics_finder_test.dart new file mode 100644 index 00000000000..5ebabfbdca4 --- /dev/null +++ b/packages/flutter_test/test/semantics_finder_test.dart @@ -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 views = [ + 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 views = [ + 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 pumpViews({required WidgetTester tester, required List views}) { + final List viewWidgets = [ + for (int i = 0; i < 3; i++) + View( + view: views[i], + child: Center( + child: Column( + children: [ + 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, + ), + ), + ); +}