From b1b7284a7238ae114192e77e83449d8201a8aa6b Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Fri, 24 Feb 2023 11:30:57 -0800 Subject: [PATCH] Move semantic-related bindings to SemanticsBinding (#121289) Move semantic-related bindings to SemanticsBinding --- .../flutter_gallery/test/smoke_test.dart | 4 +- .../flutterapp/lib/main.dart | 63 ++++++- .../flutter/lib/src/rendering/binding.dart | 40 ++-- .../flutter/lib/src/rendering/object.dart | 37 +--- .../flutter/lib/src/semantics/binding.dart | 174 ++++++++++++++++-- .../flutter/lib/src/semantics/semantics.dart | 6 +- packages/flutter/lib/src/widgets/binding.dart | 1 - .../semantics/semantics_binding_test.dart | 84 +++++++++ .../binding_cannot_schedule_frame_test.dart | 4 +- .../lib/src/common/handler_factory.dart | 4 +- packages/flutter_test/lib/src/controller.dart | 6 +- packages/flutter_test/lib/src/finders.dart | 4 +- 12 files changed, 339 insertions(+), 88 deletions(-) create mode 100644 packages/flutter/test/semantics/semantics_binding_test.dart diff --git a/dev/integration_tests/flutter_gallery/test/smoke_test.dart b/dev/integration_tests/flutter_gallery/test/smoke_test.dart index 1f58e5b52d..f2154c00f0 100644 --- a/dev/integration_tests/flutter_gallery/test/smoke_test.dart +++ b/dev/integration_tests/flutter_gallery/test/smoke_test.dart @@ -184,8 +184,8 @@ void main() { ); testWidgets('Flutter Gallery app smoke test with semantics', (WidgetTester tester) async { - RendererBinding.instance.setSemanticsEnabled(true); + final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics(); await smokeGallery(tester); - RendererBinding.instance.setSemanticsEnabled(false); + handle.dispose(); }); } diff --git a/dev/integration_tests/ios_add2app_life_cycle/flutterapp/lib/main.dart b/dev/integration_tests/ios_add2app_life_cycle/flutterapp/lib/main.dart index 358f8ef0d0..d2dec8ee44 100644 --- a/dev/integration_tests/ios_add2app_life_cycle/flutterapp/lib/main.dart +++ b/dev/integration_tests/ios_add2app_life_cycle/flutterapp/lib/main.dart @@ -3,19 +3,16 @@ // found in the LICENSE file. import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -VoidCallback? originalSemanticsListener; - void main() { - WidgetsFlutterBinding.ensureInitialized(); // Disconnects semantics listener for testing purposes. - originalSemanticsListener = WidgetsBinding.instance.platformDispatcher.onSemanticsEnabledChanged; - RendererBinding.instance.platformDispatcher.onSemanticsEnabledChanged = null; - RendererBinding.instance.setSemanticsEnabled(false); // If the test passes, LifeCycleSpy will rewire the semantics listener back. + SwitchableSemanticsBinding.ensureInitialized(); + assert(!SwitchableSemanticsBinding.instance.semanticsEnabled); + runApp(const LifeCycleSpy()); } @@ -68,8 +65,7 @@ class _LifeCycleSpyState extends State with WidgetsBindingObserver Widget build(BuildContext context) { if (const ListEquality().equals(_actualLifeCycleSequence, _expectedLifeCycleSequence)) { // Rewires the semantics harness if test passes. - RendererBinding.instance.setSemanticsEnabled(true); - RendererBinding.instance.platformDispatcher.onSemanticsEnabledChanged = originalSemanticsListener; + SwitchableSemanticsBinding.instance.semanticsEnabled = true; } return const MaterialApp( title: 'Flutter View', @@ -77,3 +73,52 @@ class _LifeCycleSpyState extends State with WidgetsBindingObserver ); } } + +class SwitchableSemanticsBinding extends WidgetsFlutterBinding { + static SwitchableSemanticsBinding get instance => BindingBase.checkInstance(_instance); + static SwitchableSemanticsBinding? _instance; + + static SwitchableSemanticsBinding ensureInitialized() { + if (_instance == null) { + SwitchableSemanticsBinding(); + } + return SwitchableSemanticsBinding.instance; + } + + VoidCallback? _originalSemanticsListener; + + @override + void initInstances() { + super.initInstances(); + _instance = this; + _updateHandler(); + } + + @override + bool get semanticsEnabled => _semanticsEnabled.value; + final ValueNotifier _semanticsEnabled = ValueNotifier(false); + set semanticsEnabled(bool value) { + _semanticsEnabled.value = value; + _updateHandler(); + } + + void _updateHandler() { + if (_semanticsEnabled.value) { + platformDispatcher.onSemanticsEnabledChanged = _originalSemanticsListener; + _originalSemanticsListener = null; + } else { + _originalSemanticsListener = platformDispatcher.onSemanticsEnabledChanged; + platformDispatcher.onSemanticsEnabledChanged = null; + } + } + + @override + void addSemanticsEnabledListener(VoidCallback listener) { + _semanticsEnabled.addListener(listener); + } + + @override + void removeSemanticsEnabledListener(VoidCallback listener) { + _semanticsEnabled.removeListener(listener); + } +} diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index ff03598794..b459dc55ac 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -38,16 +38,15 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture platformDispatcher ..onMetricsChanged = handleMetricsChanged ..onTextScaleFactorChanged = handleTextScaleFactorChanged - ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged - ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged - ..onSemanticsAction = _handleSemanticsAction; + ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged; initRenderView(); - _handleSemanticsEnabledChanged(); addPersistentFrameCallback(_handlePersistentFrameCallback); initMouseTracker(); if (kIsWeb) { addPostFrameCallback(_handleWebFirstFrame); } + addSemanticsEnabledListener(_handleSemanticsEnabledChanged); + _handleSemanticsEnabledChanged(); } /// The current [RendererBinding], if one has been created. @@ -308,8 +307,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture ); } - SemanticsHandle? _semanticsHandle; - /// Creates a [MouseTracker] which manages state about currently connected /// mice, for hover notification. /// @@ -333,14 +330,10 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture super.dispatchEvent(event, hitTestResult); } - void _handleSemanticsEnabledChanged() { - setSemanticsEnabled(platformDispatcher.semanticsEnabled); - } + SemanticsHandle? _semanticsHandle; - /// Whether the render tree associated with this binding should produce a tree - /// of [SemanticsNode] objects. - void setSemanticsEnabled(bool enabled) { - if (enabled) { + void _handleSemanticsEnabledChanged() { + if (semanticsEnabled) { _semanticsHandle ??= _pipelineOwner.ensureSemantics(); } else { _semanticsHandle?.dispose(); @@ -348,18 +341,9 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture } } - void _handleWebFirstFrame(Duration _) { - assert(kIsWeb); - const MethodChannel methodChannel = MethodChannel('flutter/service_worker'); - methodChannel.invokeMethod('first-frame'); - } - - void _handleSemanticsAction(int id, SemanticsAction action, ByteData? args) { - _pipelineOwner.semanticsOwner?.performAction( - id, - action, - args != null ? const StandardMessageCodec().decodeMessage(args) : null, - ); + @override + void performSemanticsAction(SemanticsActionEvent action) { + _pipelineOwner.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments); } void _handleSemanticsOwnerCreated() { @@ -374,6 +358,12 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture renderView.clearSemantics(); } + void _handleWebFirstFrame(Duration _) { + assert(kIsWeb); + const MethodChannel methodChannel = MethodChannel('flutter/service_worker'); + methodChannel.invokeMethod('first-frame'); + } + void _handlePersistentFrameCallback(Duration timeStamp) { drawFrame(); _scheduleMouseTrackerUpdate(); diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index c59e9fb22b..3051283c1f 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -808,24 +808,8 @@ typedef RenderObjectVisitor = void Function(RenderObject child); /// Used by [RenderObject.invokeLayoutCallback]. typedef LayoutCallback = void Function(T constraints); -/// A reference to the semantics tree. -/// -/// The framework maintains the semantics tree (used for accessibility and -/// indexing) only when there is at least one client holding an open -/// [SemanticsHandle]. -/// -/// The framework notifies the client that it has updated the semantics tree by -/// calling the [listener] callback. When the client no longer needs the -/// semantics tree, the client can call [dispose] on the [SemanticsHandle], -/// which stops these callbacks and closes the [SemanticsHandle]. When all the -/// outstanding [SemanticsHandle] objects are closed, the framework stops -/// updating the semantics tree. -/// -/// To obtain a [SemanticsHandle], call [PipelineOwner.ensureSemantics] on the -/// [PipelineOwner] for the render tree from which you wish to read semantics. -/// You can obtain the [PipelineOwner] using the [RenderObject.owner] property. -class SemanticsHandle { - SemanticsHandle._(PipelineOwner owner, this.listener) +class _LocalSemanticsHandle implements SemanticsHandle { + _LocalSemanticsHandle._(PipelineOwner owner, this.listener) : _owner = owner { if (listener != null) { _owner.semanticsOwner!.addListener(listener!); @@ -837,13 +821,7 @@ class SemanticsHandle { /// The callback that will be notified when the semantics tree updates. final VoidCallback? listener; - /// Closes the semantics handle and stops calling [listener] when the - /// semantics updates. - /// - /// When all the outstanding [SemanticsHandle] objects for a given - /// [PipelineOwner] are closed, the [PipelineOwner] will stop updating the - /// semantics tree. - @mustCallSuper + @override void dispose() { if (listener != null) { _owner.semanticsOwner!.removeListener(listener!); @@ -1171,7 +1149,12 @@ class PipelineOwner { int _outstandingSemanticsHandles = 0; /// Opens a [SemanticsHandle] and calls [listener] whenever the semantics tree - /// updates. + /// generated from the render tree owned by this [PipelineOwner] updates. + /// + /// Calling this method only ensures that this particular [PipelineOwner] will + /// generate a semantics tree. Consider calling + /// [SemanticsBinding.ensureSemantics] instead to turn on semantics globally + /// for the entire app. /// /// The [PipelineOwner] updates the semantics tree only when there are clients /// that wish to use the semantics tree. These clients express their interest @@ -1190,7 +1173,7 @@ class PipelineOwner { _semanticsOwner = SemanticsOwner(onSemanticsUpdate: onSemanticsUpdate!); onSemanticsOwnerCreated?.call(); } - return SemanticsHandle._(this, listener); + return _LocalSemanticsHandle._(this, listener); } void _didDisposeSemanticsHandle() { diff --git a/packages/flutter/lib/src/semantics/binding.dart b/packages/flutter/lib/src/semantics/binding.dart index f62bbad2d6..fb10a7aba0 100644 --- a/packages/flutter/lib/src/semantics/binding.dart +++ b/packages/flutter/lib/src/semantics/binding.dart @@ -2,22 +2,27 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' as ui show AccessibilityFeatures, SemanticsUpdateBuilder; +import 'dart:ui' as ui show AccessibilityFeatures, SemanticsAction, SemanticsUpdateBuilder; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'debug.dart'; export 'dart:ui' show AccessibilityFeatures, SemanticsUpdateBuilder; /// The glue between the semantics layer and the Flutter engine. -// TODO(zanderso): move the remaining semantic related bindings here. mixin SemanticsBinding on BindingBase { @override void initInstances() { super.initInstances(); _instance = this; _accessibilityFeatures = platformDispatcher.accessibilityFeatures; + platformDispatcher + ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged + ..onSemanticsAction = _handleSemanticsAction + ..onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged; + _handleSemanticsEnabledChanged(); } /// The current [SemanticsBinding], if one has been created. @@ -28,10 +33,118 @@ mixin SemanticsBinding on BindingBase { static SemanticsBinding get instance => BindingBase.checkInstance(_instance); static SemanticsBinding? _instance; + /// Whether semantics information must be collected. + /// + /// Returns true if either the platform has requested semantics information + /// to be generated or if [ensureSemantics] has been called otherwise. + /// + /// To get notified when this value changes register a listener with + /// [addSemanticsEnabledListener]. + bool get semanticsEnabled { + assert(_semanticsEnabled.value == (_outstandingHandles > 0)); + return _semanticsEnabled.value; + } + late final ValueNotifier _semanticsEnabled = ValueNotifier(platformDispatcher.semanticsEnabled); + + /// Adds a `listener` to be called when [semanticsEnabled] changes. + /// + /// See also: + /// + /// * [removeSemanticsEnabledListener] to remove the listener again. + /// * [ValueNotifier.addListener], which documents how and when listeners are + /// called. + void addSemanticsEnabledListener(VoidCallback listener) { + _semanticsEnabled.addListener(listener); + } + + /// Removes a `listener` added by [addSemanticsEnabledListener]. + /// + /// See also: + /// + /// * [ValueNotifier.removeListener], which documents how listeners are + /// removed. + void removeSemanticsEnabledListener(VoidCallback listener) { + _semanticsEnabled.removeListener(listener); + } + + int _outstandingHandles = 0; + + /// Creates a new [SemanticsHandle] and requests the collection of semantics + /// information. + /// + /// Semantics information are only collected when there are clients interested + /// in them. These clients express their interest by holding a + /// [SemanticsHandle]. + /// + /// Clients can close their [SemanticsHandle] by calling + /// [SemanticsHandle.dispose]. Once all outstanding [SemanticsHandle] objects + /// are closed, semantics information are no longer collected. + SemanticsHandle ensureSemantics() { + assert(_outstandingHandles >= 0); + _outstandingHandles++; + assert(_outstandingHandles > 0); + _semanticsEnabled.value = true; + return SemanticsHandle._(_didDisposeSemanticsHandle); + } + + void _didDisposeSemanticsHandle() { + assert(_outstandingHandles > 0); + _outstandingHandles--; + assert(_outstandingHandles >= 0); + _semanticsEnabled.value = _outstandingHandles > 0; + } + + // Handle for semantics request from the platform. + SemanticsHandle? _semanticsHandle; + + void _handleSemanticsEnabledChanged() { + if (platformDispatcher.semanticsEnabled) { + _semanticsHandle ??= ensureSemantics(); + } else { + _semanticsHandle?.dispose(); + _semanticsHandle = null; + } + } + + void _handleSemanticsAction(int id, ui.SemanticsAction action, ByteData? args) { + performSemanticsAction(SemanticsActionEvent( + nodeId: id, + type: action, + arguments: args != null ? const StandardMessageCodec().decodeMessage(args) : null, + )); + } + + /// Called whenever the platform requests an action to be performed on a + /// [SemanticsNode]. + /// + /// This callback is invoked when a user interacts with the app via an + /// accessibility service (e.g. TalkBack and VoiceOver) and initiates an + /// action on the focused node. + /// + /// Bindings that mixin the [SemanticsBinding] must implement this method and + /// perform the given `action` on the [SemanticsNode] specified by + /// [SemanticsActionEvent.nodeId]. + /// + /// See [dart:ui.PlatformDispatcher.onSemanticsAction]. + @protected + void performSemanticsAction(SemanticsActionEvent action); + + /// The currently active set of [AccessibilityFeatures]. + /// + /// This is set when the binding is first initialized and updated whenever a + /// flag is changed. + /// + /// To listen to changes to accessibility features, create a + /// [WidgetsBindingObserver] and listen to + /// [WidgetsBindingObserver.didChangeAccessibilityFeatures]. + ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures; + late ui.AccessibilityFeatures _accessibilityFeatures; + /// Called when the platform accessibility features change. /// /// See [dart:ui.PlatformDispatcher.onAccessibilityFeaturesChanged]. @protected + @mustCallSuper void handleAccessibilityFeaturesChanged() { _accessibilityFeatures = platformDispatcher.accessibilityFeatures; } @@ -46,17 +159,6 @@ mixin SemanticsBinding on BindingBase { return ui.SemanticsUpdateBuilder(); } - /// The currently active set of [AccessibilityFeatures]. - /// - /// This is initialized the first time [runApp] is called and updated whenever - /// a flag is changed. - /// - /// To listen to changes to accessibility features, create a - /// [WidgetsBindingObserver] and listen to - /// [WidgetsBindingObserver.didChangeAccessibilityFeatures]. - ui.AccessibilityFeatures get accessibilityFeatures => _accessibilityFeatures; - late ui.AccessibilityFeatures _accessibilityFeatures; - /// The platform is requesting that animations be disabled or simplified. /// /// This setting can be overridden for testing or debugging by setting @@ -72,3 +174,49 @@ mixin SemanticsBinding on BindingBase { return value; } } + +/// An event to request a [SemanticsAction] of [type] to be performed on the +/// [SemanticsNode] identified by [nodeId]. +/// +/// Used by [SemanticsBinding.performSemanticsAction]. +@immutable +class SemanticsActionEvent { + /// Creates a [SemanticsActionEvent]. + /// + /// The [type] and [nodeId] are required. + const SemanticsActionEvent({required this.type, required this.nodeId, this.arguments}); + + /// The type of action to be performed. + final ui.SemanticsAction type; + + /// The id of the [SemanticsNode] on which the action is to be performed. + final int nodeId; + + /// Optional arguments for the action. + final Object? arguments; +} + +/// A reference to the semantics information generated by the framework. +/// +/// Semantics information are only collected when there are clients interested +/// in them. These clients express their interest by holding a +/// [SemanticsHandle]. When the client no longer needs the +/// semantics information, it must call [dispose] on the [SemanticsHandle] to +/// close it. When all open [SemanticsHandle]s are disposed, the framework will +/// stop updating the semantics information. +/// +/// To obtain a [SemanticsHandle], call [SemanticsBinding.ensureSemantics]. +class SemanticsHandle { + SemanticsHandle._(this._onDispose); + + final VoidCallback _onDispose; + + /// Closes the semantics handle. + /// + /// When all the outstanding [SemanticsHandle] objects are closed, the + /// framework will stop generating semantics information. + @mustCallSuper + void dispose() { + _onDispose(); + } +} diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index d62b9f7a26..66e0785bf0 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -3129,9 +3129,9 @@ class _TraversalSortNode implements Comparable<_TraversalSortNode> { /// Owns [SemanticsNode] objects and notifies listeners of changes to the /// render tree semantics. /// -/// To listen for semantic updates, call [PipelineOwner.ensureSemantics] to -/// obtain a [SemanticsHandle]. This will create a [SemanticsOwner] if -/// necessary. +/// To listen for semantic updates, call [SemanticsBinding.ensureSemantics] or +/// [PipelineOwner.ensureSemantics] to obtain a [SemanticsHandle]. This will +/// create a [SemanticsOwner] if necessary. class SemanticsOwner extends ChangeNotifier { /// Creates a [SemanticsOwner] that manages zero or more [SemanticsNode] objects. SemanticsOwner({ diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart index ee7fb9aaae..98124fbbe3 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -260,7 +260,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB _buildOwner = BuildOwner(); buildOwner!.onBuildScheduled = _handleBuildScheduled; platformDispatcher.onLocaleChanged = handleLocaleChanged; - platformDispatcher.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged; SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation); assert(() { FlutterErrorDetails.propertiesTransformers.add(debugTransformDebugCreator); diff --git a/packages/flutter/test/semantics/semantics_binding_test.dart b/packages/flutter/test/semantics/semantics_binding_test.dart new file mode 100644 index 0000000000..29f256ef53 --- /dev/null +++ b/packages/flutter/test/semantics/semantics_binding_test.dart @@ -0,0 +1,84 @@ +// 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/semantics.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Listeners are called when semantics are turned on with ensureSemantics', (WidgetTester tester) async { + expect(SemanticsBinding.instance.semanticsEnabled, isFalse); + + final List status = []; + void listener() { + status.add(SemanticsBinding.instance.semanticsEnabled); + } + + SemanticsBinding.instance.addSemanticsEnabledListener(listener); + expect(SemanticsBinding.instance.semanticsEnabled, isFalse); + + final SemanticsHandle handle1 = SemanticsBinding.instance.ensureSemantics(); + expect(status.single, isTrue); + expect(SemanticsBinding.instance.semanticsEnabled, isTrue); + status.clear(); + + final SemanticsHandle handle2 = SemanticsBinding.instance.ensureSemantics(); + expect(status, isEmpty); // Listener didn't fire again. + expect(SemanticsBinding.instance.semanticsEnabled, isTrue); + + expect(tester.binding.platformDispatcher.semanticsEnabled, isFalse); + tester.binding.platformDispatcher.semanticsEnabledTestValue = true; + expect(tester.binding.platformDispatcher.semanticsEnabled, isTrue); + tester.binding.platformDispatcher.clearSemanticsEnabledTestValue(); + expect(tester.binding.platformDispatcher.semanticsEnabled, isFalse); + expect(status, isEmpty); // Listener didn't fire again. + expect(SemanticsBinding.instance.semanticsEnabled, isTrue); + + handle1.dispose(); + expect(status, isEmpty); // Listener didn't fire. + expect(SemanticsBinding.instance.semanticsEnabled, isTrue); + + handle2.dispose(); + expect(status.single, isFalse); + expect(SemanticsBinding.instance.semanticsEnabled, isFalse); + }, semanticsEnabled: false); + + testWidgets('Listeners are called when semantics are turned on by platform', (WidgetTester tester) async { + expect(SemanticsBinding.instance.semanticsEnabled, isFalse); + + final List status = []; + void listener() { + status.add(SemanticsBinding.instance.semanticsEnabled); + } + + SemanticsBinding.instance.addSemanticsEnabledListener(listener); + expect(SemanticsBinding.instance.semanticsEnabled, isFalse); + + tester.binding.platformDispatcher.semanticsEnabledTestValue = true; + expect(status.single, isTrue); + expect(SemanticsBinding.instance.semanticsEnabled, isTrue); + status.clear(); + + final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics(); + handle.dispose(); + expect(status, isEmpty); // Listener didn't fire. + expect(SemanticsBinding.instance.semanticsEnabled, isTrue); + + tester.binding.platformDispatcher.clearSemanticsEnabledTestValue(); + expect(status.single, isFalse); + expect(SemanticsBinding.instance.semanticsEnabled, isFalse); + }, semanticsEnabled: false); + + testWidgets('SemanticsBinding.ensureSemantics triggers creation of semantics owner.', (WidgetTester tester) async { + expect(SemanticsBinding.instance.semanticsEnabled, isFalse); + expect(tester.binding.pipelineOwner.semanticsOwner, isNull); + + final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics(); + expect(SemanticsBinding.instance.semanticsEnabled, isTrue); + expect(tester.binding.pipelineOwner.semanticsOwner, isNotNull); + + handle.dispose(); + expect(SemanticsBinding.instance.semanticsEnabled, isFalse); + expect(tester.binding.pipelineOwner.semanticsOwner, isNull); + }, semanticsEnabled: false); +} diff --git a/packages/flutter/test/widgets/binding_cannot_schedule_frame_test.dart b/packages/flutter/test/widgets/binding_cannot_schedule_frame_test.dart index 2d19139bd0..27dfec6011 100644 --- a/packages/flutter/test/widgets/binding_cannot_schedule_frame_test.dart +++ b/packages/flutter/test/widgets/binding_cannot_schedule_frame_test.dart @@ -18,7 +18,9 @@ void main() { // Enables the semantics should not schedule any frames if the root widget // has not been attached. - binding.setSemanticsEnabled(true); + expect(binding.semanticsEnabled, isFalse); + binding.ensureSemantics(); + expect(binding.semanticsEnabled, isTrue); expect(SchedulerBinding.instance.framesEnabled, isFalse); expect(SchedulerBinding.instance.hasScheduledFrame, isFalse); diff --git a/packages/flutter_driver/lib/src/common/handler_factory.dart b/packages/flutter_driver/lib/src/common/handler_factory.dart index 5960f7557c..bed6ef4fca 100644 --- a/packages/flutter_driver/lib/src/common/handler_factory.dart +++ b/packages/flutter_driver/lib/src/common/handler_factory.dart @@ -443,13 +443,13 @@ mixin CommandHandlerFactory { } SemanticsHandle? _semantics; - bool get _semanticsIsEnabled => RendererBinding.instance.pipelineOwner.semanticsOwner != null; + bool get _semanticsIsEnabled => SemanticsBinding.instance.semanticsEnabled; Future _setSemantics(Command command) async { final SetSemantics setSemanticsCommand = command as SetSemantics; final bool semanticsWasEnabled = _semanticsIsEnabled; if (setSemanticsCommand.enabled && _semantics == null) { - _semantics = RendererBinding.instance.pipelineOwner.ensureSemantics(); + _semantics = SemanticsBinding.instance.ensureSemantics(); if (!semanticsWasEnabled) { // wait for the first frame where semantics is enabled. final Completer completer = Completer(); diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index 55b226f1f0..2dbbfbb42b 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -72,7 +72,7 @@ class SemanticsController { /// if no semantics are found or are not enabled. SemanticsNode find(Finder finder) { TestAsyncUtils.guardSync(); - if (_binding.pipelineOwner.semanticsOwner == null) { + if (!_binding.semanticsEnabled) { throw StateError('Semantics are not enabled.'); } final Iterable candidates = finder.evaluate(); @@ -241,7 +241,7 @@ abstract class WidgetController { /// 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) { + if (!binding.semanticsEnabled) { throw StateError( 'Semantics are not enabled. Enable them by passing ' '`semanticsEnabled: true` to `testWidgets`, or by manually creating a ' @@ -1491,7 +1491,7 @@ abstract class WidgetController { /// /// The handle must be disposed at the end of the test. SemanticsHandle ensureSemantics() { - return binding.pipelineOwner.ensureSemantics(); + return binding.ensureSemantics(); } /// Given a widget `W` specified by [finder] and a [Scrollable] widget `S` in diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart index de3a1134ea..33cac7eb06 100644 --- a/packages/flutter_test/lib/src/finders.dart +++ b/packages/flutter_test/lib/src/finders.dart @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show Tooltip; +import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'all_elements.dart'; @@ -435,7 +435,7 @@ class CommonFinders { /// If the `skipOffstage` argument is true (the default), then this skips /// nodes that are [Offstage] or that are from inactive [Route]s. Finder bySemanticsLabel(Pattern label, { bool skipOffstage = true }) { - if (WidgetsBinding.instance.pipelineOwner.semanticsOwner == null) { + if (!SemanticsBinding.instance.semanticsEnabled) { throw StateError('Semantics are not enabled. ' 'Make sure to call tester.ensureSemantics() before using ' 'this finder, and call dispose on its return value after.');