diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index b459dc55ac..49e2213921 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -30,7 +30,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture super.initInstances(); _instance = this; _pipelineOwner = PipelineOwner( - onNeedVisualUpdate: ensureVisualUpdate, onSemanticsOwnerCreated: _handleSemanticsOwnerCreated, onSemanticsUpdate: _handleSemanticsUpdate, onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed, @@ -45,8 +44,7 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture if (kIsWeb) { addPostFrameCallback(_handleWebFirstFrame); } - addSemanticsEnabledListener(_handleSemanticsEnabledChanged); - _handleSemanticsEnabledChanged(); + _pipelineOwner.attach(_manifold); } /// The current [RendererBinding], if one has been created. @@ -201,6 +199,8 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture } } + late final PipelineManifold _manifold = _BindingPipelineManifold(this); + /// Creates a [RenderView] object to be the root of the /// [RenderObject] rendering tree, and initializes it so that it /// will be rendered when the next frame is requested. @@ -330,17 +330,6 @@ mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, Gesture super.dispatchEvent(event, hitTestResult); } - SemanticsHandle? _semanticsHandle; - - void _handleSemanticsEnabledChanged() { - if (semanticsEnabled) { - _semanticsHandle ??= _pipelineOwner.ensureSemantics(); - } else { - _semanticsHandle?.dispose(); - _semanticsHandle = null; - } - } - @override void performSemanticsAction(SemanticsActionEvent action) { _pipelineOwner.semanticsOwner?.performAction(action.nodeId, action.type, action.arguments); @@ -621,3 +610,26 @@ class RenderingFlutterBinding extends BindingBase with GestureBinding, Scheduler return RendererBinding.instance; } } + +/// A [PipelineManifold] implementation that is backed by the [RendererBinding]. +class _BindingPipelineManifold extends ChangeNotifier implements PipelineManifold { + _BindingPipelineManifold(this._binding) { + _binding.addSemanticsEnabledListener(notifyListeners); + } + + final RendererBinding _binding; + + @override + void requestVisualUpdate() { + _binding.ensureVisualUpdate(); + } + + @override + bool get semanticsEnabled => _binding.semanticsEnabled; + + @override + void dispose() { + _binding.removeSemanticsEnabledListener(notifyListeners); + super.dispose(); + } +} diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index 3400330d7c..22e684ebd1 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -859,6 +859,20 @@ class _LocalSemanticsHandle implements SemanticsHandle { /// are visible on screen. You can create other pipeline owners to manage /// off-screen objects, which can flush their pipelines independently of the /// on-screen render objects. +/// +/// [PipelineOwner]s can be organized in a tree to manage multiple render trees, +/// where each [PipelineOwner] is responsible for one of the render trees. To +/// build or modify the tree, call [adoptChild] or [dropChild]. During each of +/// the different flush phases described above, a [PipelineOwner] will first +/// perform the phase on the nodes it manages in its own render tree before +/// calling the same flush method on its children. No assumption must be made +/// about the order in which child [PipelineOwner]s are flushed. +/// +/// A [PipelineOwner] may also be [attach]ed to a [PipelineManifold], which +/// gives it access to platform functionality usually exposed by the bindings +/// without tying it to a specific binding implementation. All [PipelineOwner]s +/// in a given tree must be attached to the same [PipelineManifold]. This +/// happens automatically during [adoptChild]. class PipelineOwner { /// Creates a pipeline owner. /// @@ -879,6 +893,10 @@ class PipelineOwner { /// various stages of the pipeline. This function might be called multiple /// times in quick succession. Implementations should take care to discard /// duplicate calls quickly. + /// + /// When the [PipelineOwner] is attached to a [PipelineManifold] and + /// [onNeedVisualUpdate] is provided, the [onNeedVisualUpdate] callback is + /// invoked instead of calling [PipelineManifold.requestVisualUpdate]. final VoidCallback? onNeedVisualUpdate; /// Called whenever this pipeline owner creates a semantics object. @@ -903,7 +921,11 @@ class PipelineOwner { /// Used to notify the pipeline owner that an associated render object wishes /// to update its visual appearance. void requestVisualUpdate() { - onNeedVisualUpdate?.call(); + if (onNeedVisualUpdate != null) { + onNeedVisualUpdate!(); + } else { + _manifold?.requestVisualUpdate(); + } } /// The unique object managed by this pipeline that has no parent. @@ -945,6 +967,7 @@ class PipelineOwner { /// always returns false. bool get debugDoingLayout => _debugDoingLayout; bool _debugDoingLayout = false; + bool _debugDoingChildLayout = false; /// Update the layout information for all dirty render objects. /// @@ -997,10 +1020,20 @@ class PipelineOwner { // relayout boundary back. _shouldMergeDirtyNodes = false; } + + assert(() { + _debugDoingChildLayout = true; + return true; + }()); + for (final PipelineOwner child in _children) { + child.flushLayout(); + } + assert(_nodesNeedingLayout.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.'); } finally { _shouldMergeDirtyNodes = false; assert(() { _debugDoingLayout = false; + _debugDoingChildLayout = false; return true; }()); if (!kReleaseMode) { @@ -1052,6 +1085,10 @@ class PipelineOwner { } } _nodesNeedingCompositingBitsUpdate.clear(); + for (final PipelineOwner child in _children) { + child.flushCompositingBits(); + } + assert(_nodesNeedingCompositingBitsUpdate.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.'); if (!kReleaseMode) { Timeline.finishSync(); } @@ -1116,7 +1153,10 @@ class PipelineOwner { } } } - assert(_nodesNeedingPaint.isEmpty); + for (final PipelineOwner child in _children) { + child.flushPaint(); + } + assert(_nodesNeedingPaint.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.'); } finally { assert(() { _debugDoingPaint = false; @@ -1130,11 +1170,13 @@ class PipelineOwner { /// The object that is managing semantics for this pipeline owner, if any. /// - /// An owner is created by [ensureSemantics]. The owner is valid for as long - /// there are [SemanticsHandle]s returned by [ensureSemantics] that have not - /// yet been disposed. Once the last handle has been disposed, the - /// [semanticsOwner] field will revert to null, and the previous owner will be - /// disposed. + /// An owner is created by [ensureSemantics] or when the [PipelineManifold] to + /// which this owner is connected has [PipelineManifold.semanticsEnabled] set + /// to true. The owner is valid for as long as + /// [PipelineManifold.semanticsEnabled] remains true or while there are + /// outstanding [SemanticsHandle]s from calls to [ensureSemantics]. The + /// [semanticsOwner] field will revert to null once both conditions are no + /// longer met. /// /// When [semanticsOwner] is null, the [PipelineOwner] skips all steps /// relating to semantics. @@ -1167,23 +1209,28 @@ class PipelineOwner { /// maintaining the semantics tree. SemanticsHandle ensureSemantics({ VoidCallback? listener }) { _outstandingSemanticsHandles += 1; - if (_outstandingSemanticsHandles == 1) { - assert(_semanticsOwner == null); - assert(onSemanticsUpdate != null, 'Attempted to open a semantics handle without an onSemanticsUpdate callback.'); - _semanticsOwner = SemanticsOwner(onSemanticsUpdate: onSemanticsUpdate!); - onSemanticsOwnerCreated?.call(); - } + _updateSemanticsOwner(); return _LocalSemanticsHandle._(this, listener); } + void _updateSemanticsOwner() { + if ((_manifold?.semanticsEnabled ?? false) || _outstandingSemanticsHandles > 0) { + if (_semanticsOwner == null) { + assert(onSemanticsUpdate != null, 'Attempted to enable semantics without configuring an onSemanticsUpdate callback.'); + _semanticsOwner = SemanticsOwner(onSemanticsUpdate: onSemanticsUpdate!); + onSemanticsOwnerCreated?.call(); + } + } else if (_semanticsOwner != null) { + _semanticsOwner?.dispose(); + _semanticsOwner = null; + onSemanticsOwnerDisposed?.call(); + } + } + void _didDisposeSemanticsHandle() { assert(_semanticsOwner != null); _outstandingSemanticsHandles -= 1; - if (_outstandingSemanticsHandles == 0) { - _semanticsOwner!.dispose(); - _semanticsOwner = null; - onSemanticsOwnerDisposed?.call(); - } + _updateSemanticsOwner(); } bool _debugDoingSemantics = false; @@ -1222,8 +1269,11 @@ class PipelineOwner { } } _semanticsOwner!.sendSemanticsUpdate(); + for (final PipelineOwner child in _children) { + child.flushSemantics(); + } + assert(_nodesNeedingSemantics.isEmpty, 'Child PipelineOwners must not dirty nodes in their parent.'); } finally { - assert(_nodesNeedingSemantics.isEmpty); assert(() { _debugDoingSemantics = false; return true; @@ -1233,6 +1283,166 @@ class PipelineOwner { } } } + + // TREE MANAGEMENT + + final Set _children = {}; + PipelineManifold? _manifold; + + PipelineOwner? _debugParent; + bool _debugSetParent(PipelineOwner child, PipelineOwner? parent) { + child._debugParent = parent; + return true; + } + + /// Mark this [PipelineOwner] as attached to the given [PipelineManifold]. + /// + /// Typically, this is only called directly on the root [PipelineOwner]. + /// Children are automatically attached to their parent's [PipelineManifold] + /// when [adoptChild] is called. + void attach(PipelineManifold manifold) { + assert(_manifold == null); + _manifold = manifold; + _manifold!.addListener(_updateSemanticsOwner); + _updateSemanticsOwner(); + + for (final PipelineOwner child in _children) { + child.attach(manifold); + } + } + + /// Mark this [PipelineOwner] as detached. + /// + /// Typically, this is only called directly on the root [PipelineOwner]. + /// Children are automatically detached from their parent's [PipelineManifold] + /// when [dropChild] is called. + void detach() { + assert(_manifold != null); + _manifold!.removeListener(_updateSemanticsOwner); + _manifold = null; + _updateSemanticsOwner(); + + for (final PipelineOwner child in _children) { + child.detach(); + } + } + + // In theory, child list modifications are also disallowed between + // _debugDoingChildrenLayout and _debugDoingPaint as well as between + // _debugDoingPaint and _debugDoingSemantics. However, since the associated + // flush methods are usually called back to back, this gets us close enough. + bool get _debugAllowChildListModifications => !_debugDoingChildLayout && !_debugDoingPaint && !_debugDoingSemantics; + + /// Adds `child` to this [PipelineOwner]. + /// + /// During the phases of frame production (see [RendererBinding.drawFrame]), + /// the parent [PipelineOwner] will complete a phase for the nodes it owns + /// directly before invoking the flush method corresponding to the current + /// phase on its child [PipelineOwner]s. For example, during layout, the + /// parent [PipelineOwner] will first lay out its own nodes before calling + /// [flushLayout] on its children. During paint, it will first paint its own + /// nodes before calling [flushPaint] on its children. This order also applies + /// for all the other phases. + /// + /// No assumptions must be made about the order in which child + /// [PipelineOwner]s are flushed. + /// + /// No new children may be added after the [PipelineOwner] has started calling + /// [flushLayout] on any of its children until the end of the current frame. + /// + /// To remove a child, call [dropChild]. + void adoptChild(PipelineOwner child) { + assert(child._debugParent == null); + assert(!_children.contains(child)); + assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.'); + _children.add(child); + assert(_debugSetParent(child, this)); + if (_manifold != null) { + child.attach(_manifold!); + } + } + + /// Removes a child [PipelineOwner] previously added via [adoptChild]. + /// + /// This node will cease to call the flush methods on the `child` during frame + /// production. + /// + /// No children may be removed after the [PipelineOwner] has started calling + /// [flushLayout] on any of its children until the end of the current frame. + void dropChild(PipelineOwner child) { + assert(child._debugParent == this); + assert(_children.contains(child)); + assert(_debugAllowChildListModifications, 'Cannot modify child list after layout.'); + _children.remove(child); + assert(_debugSetParent(child, null)); + if (_manifold != null) { + child.detach(); + } + } + + /// Calls `visitor` for each immediate child of this [PipelineOwner]. + /// + /// See also: + /// + /// * [adoptChild] to add a child. + /// * [dropChild] to remove a child. + void visitChildren(PipelineOwnerVisitor visitor) { + _children.forEach(visitor); + } +} + +/// Signature for the callback to [PipelineOwner.visitChildren]. +/// +/// The argument is the child being visited. +typedef PipelineOwnerVisitor = void Function(PipelineOwner child); + +/// Manages a tree of [PipelineOwner]s. +/// +/// All [PipelineOwner]s within a tree are attached to the same +/// [PipelineManifold], which gives them access to shared functionality such +/// as requesting a visual update (by calling [requestVisualUpdate]). As such, +/// the [PipelineManifold] gives the [PipelineOwner]s access to functionality +/// usually provided by the bindings without tying the [PipelineOwner]s to a +/// particular binding implementation. +/// +/// The root of the [PipelineOwner] tree is attached to a [PipelineManifold] by +/// passing the manifold to [PipelineOwner.attach]. Children are attached to the +/// same [PipelineManifold] as their parent when they are adopted via +/// [PipelineOwner.adoptChild]. +/// +/// [PipelineOwner]s can register listeners with the [PipelineManifold] to be +/// informed when certain values provided by the [PipelineManifold] change. +abstract class PipelineManifold implements Listenable { + /// Whether [PipelineOwner]s connected to this [PipelineManifold] should + /// collect semantics information and produce a semantics tree. + /// + /// The [PipelineManifold] notifies its listeners (managed with [addListener] + /// and [removeListener]) when this property changes its value. + /// + /// See also: + /// + /// * [SemanticsBinding.semanticsEnabled], which [PipelineManifold] + /// implementations typically use to back this property. + bool get semanticsEnabled; + + /// Called by a [PipelineOwner] connected to this [PipelineManifold] when a + /// [RenderObject] associated with that pipeline owner wishes to update its + /// visual appearance. + /// + /// Typical implementations of this function will schedule a task to flush the + /// various stages of the pipeline. This function might be called multiple + /// times in quick succession. Implementations should take care to discard + /// duplicate calls quickly. + /// + /// A [PipelineOwner] connected to this [PipelineManifold] will call + /// [PipelineOwner.onNeedVisualUpdate] instead of this method if it has been + /// configured with a non-null [PipelineOwner.onNeedVisualUpdate] callback. + /// + /// See also: + /// + /// * [SchedulerBinding.ensureVisualUpdate], which [PipelineManifold] + /// implementations typically call to implement this method. + void requestVisualUpdate(); } const String _flutterRenderingLibrary = 'package:flutter/rendering.dart'; diff --git a/packages/flutter/test/rendering/binding_pipeline_manifold_init_test.dart b/packages/flutter/test/rendering/binding_pipeline_manifold_init_test.dart new file mode 100644 index 0000000000..5974e06639 --- /dev/null +++ b/packages/flutter/test/rendering/binding_pipeline_manifold_init_test.dart @@ -0,0 +1,25 @@ +// 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/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Initializing the RendererBinding does not crash when semantics is enabled', () { + try { + MyRenderingFlutterBinding(); + } catch (e) { + fail('Initializing the RenderingBinding threw an unexpected error:\n$e'); + } + expect(RendererBinding.instance, isA()); + expect(SemanticsBinding.instance.semanticsEnabled, isTrue); + }); +} + +// Binding that pretends the platform had semantics enabled before the binding +// is initialized. +class MyRenderingFlutterBinding extends RenderingFlutterBinding { + @override + bool get semanticsEnabled => true; +} diff --git a/packages/flutter/test/rendering/binding_pipeline_manifold_test.dart b/packages/flutter/test/rendering/binding_pipeline_manifold_test.dart new file mode 100644 index 0000000000..5374454db9 --- /dev/null +++ b/packages/flutter/test/rendering/binding_pipeline_manifold_test.dart @@ -0,0 +1,98 @@ +// 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/foundation.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'rendering_tester.dart'; + +void main() { + MyTestRenderingFlutterBinding.ensureInitialized(); + + tearDown(() { + final List children = []; + RendererBinding.instance.pipelineOwner.visitChildren((PipelineOwner child) { + children.add(child); + }); + children.forEach(RendererBinding.instance.pipelineOwner.dropChild); + }); + + test("BindingPipelineManifold notifies binding if render object managed by binding's PipelineOwner tree needs visual update", () { + final PipelineOwner child = PipelineOwner(); + RendererBinding.instance.pipelineOwner.adoptChild(child); + + final RenderObject renderObject = TestRenderObject(); + child.rootNode = renderObject; + renderObject.scheduleInitialLayout(); + RendererBinding.instance.pipelineOwner.flushLayout(); + + MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount = 0; + renderObject.markNeedsLayout(); + expect(MyTestRenderingFlutterBinding.instance.ensureVisualUpdateCount, 1); + }); + + test('Turning global semantics on/off creates semantics owners in PipelineOwner tree', () { + final PipelineOwner child = PipelineOwner( + onSemanticsUpdate: (_) { }, + ); + RendererBinding.instance.pipelineOwner.adoptChild(child); + + expect(child.semanticsOwner, isNull); + expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull); + + final SemanticsHandle handle = SemanticsBinding.instance.ensureSemantics(); + + expect(child.semanticsOwner, isNotNull); + expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNotNull); + + handle.dispose(); + + expect(child.semanticsOwner, isNull); + expect(RendererBinding.instance.pipelineOwner.semanticsOwner, isNull); + }); +} + +class MyTestRenderingFlutterBinding extends TestRenderingFlutterBinding { + static MyTestRenderingFlutterBinding get instance => BindingBase.checkInstance(_instance); + static MyTestRenderingFlutterBinding? _instance; + + static MyTestRenderingFlutterBinding ensureInitialized() { + if (_instance != null) { + return _instance!; + } + return MyTestRenderingFlutterBinding(); + } + + @override + void initInstances() { + super.initInstances(); + _instance = this; + } + + int ensureVisualUpdateCount = 0; + + @override + void ensureVisualUpdate() { + super.ensureVisualUpdate(); + ensureVisualUpdateCount++; + } +} + +class TestRenderObject extends RenderObject { + @override + void debugAssertDoesMeetConstraints() { } + + @override + Rect get paintBounds => Rect.zero; + + @override + void performLayout() { } + + @override + void performResize() { } + + @override + Rect get semanticBounds => Rect.zero; +} diff --git a/packages/flutter/test/rendering/pipeline_owner_tree_test.dart b/packages/flutter/test/rendering/pipeline_owner_tree_test.dart new file mode 100644 index 0000000000..5c2368dee5 --- /dev/null +++ b/packages/flutter/test/rendering/pipeline_owner_tree_test.dart @@ -0,0 +1,862 @@ +// 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/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + FlutterError.presentError = (FlutterErrorDetails details) { + // Make tests fail on FlutterErrors. + throw details.exception; + }; + + test('onNeedVisualUpdate takes precedence over manifold', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + int rootOnNeedVisualUpdateCallCount = 0; + final TestRenderObject rootRenderObject = TestRenderObject(); + final PipelineOwner root = PipelineOwner( + onNeedVisualUpdate: () { + rootOnNeedVisualUpdateCallCount += 1; + }, + ); + root.rootNode = rootRenderObject; + rootRenderObject.scheduleInitialLayout(); + + int child1OnNeedVisualUpdateCallCount = 0; + final TestRenderObject child1RenderObject = TestRenderObject(); + final PipelineOwner child1 = PipelineOwner( + onNeedVisualUpdate: () { + child1OnNeedVisualUpdateCallCount += 1; + }, + ); + child1.rootNode = child1RenderObject; + child1RenderObject.scheduleInitialLayout(); + + final TestRenderObject child2RenderObject = TestRenderObject(); + final PipelineOwner child2 = PipelineOwner(); + child2.rootNode = child2RenderObject; + child2RenderObject.scheduleInitialLayout(); + + root.adoptChild(child1); + root.adoptChild(child2); + root.attach(manifold); + root.flushLayout(); + manifold.requestVisualUpdateCount = 0; + + rootRenderObject.markNeedsLayout(); + expect(manifold.requestVisualUpdateCount, 0); + expect(rootOnNeedVisualUpdateCallCount, 1); + expect(child1OnNeedVisualUpdateCallCount, 0); + + child1RenderObject.markNeedsLayout(); + expect(manifold.requestVisualUpdateCount, 0); + expect(rootOnNeedVisualUpdateCallCount, 1); + expect(child1OnNeedVisualUpdateCallCount, 1); + + child2RenderObject.markNeedsLayout(); + expect(manifold.requestVisualUpdateCount, 1); + expect(rootOnNeedVisualUpdateCallCount, 1); + expect(child1OnNeedVisualUpdateCallCount, 1); + }); + + test("parent's render objects are laid out before child's render objects", () { + final TestPipelineManifold manifold = TestPipelineManifold(); + final List log = []; + + final TestRenderObject rootRenderObject = TestRenderObject( + onLayout: () { + log.add('layout parent'); + }, + ); + final PipelineOwner root = PipelineOwner(); + root.rootNode = rootRenderObject; + rootRenderObject.scheduleInitialLayout(); + + final TestRenderObject childRenderObject = TestRenderObject( + onLayout: () { + log.add('layout child'); + }, + ); + final PipelineOwner child = PipelineOwner(); + child.rootNode = childRenderObject; + childRenderObject.scheduleInitialLayout(); + + root.adoptChild(child); + root.attach(manifold); + expect(log, isEmpty); + + root.flushLayout(); + expect(log, ['layout parent', 'layout child']); + }); + + test("child cannot dirty parent's render object during flushLayout", () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + final TestRenderObject rootRenderObject = TestRenderObject(); + final PipelineOwner root = PipelineOwner(); + root.rootNode = rootRenderObject; + rootRenderObject.scheduleInitialLayout(); + + bool childLayoutExecuted = false; + final TestRenderObject childRenderObject = TestRenderObject( + onLayout: () { + childLayoutExecuted = true; + expect(() => rootRenderObject.markNeedsLayout(), throwsFlutterError); + }, + ); + final PipelineOwner child = PipelineOwner(); + child.rootNode = childRenderObject; + childRenderObject.scheduleInitialLayout(); + + root.adoptChild(child); + root.attach(manifold); + + + root.flushLayout(); + expect(childLayoutExecuted, isTrue); + }); + + test('updates compositing bits on children', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + final TestRenderObject rootRenderObject = TestRenderObject(); + final PipelineOwner root = PipelineOwner(); + root.rootNode = rootRenderObject; + rootRenderObject.markNeedsCompositingBitsUpdate(); + + final TestRenderObject childRenderObject = TestRenderObject(); + final PipelineOwner child = PipelineOwner(); + child.rootNode = childRenderObject; + childRenderObject.markNeedsCompositingBitsUpdate(); + + root.adoptChild(child); + root.attach(manifold); + expect(() => rootRenderObject.needsCompositing, throwsAssertionError); + expect(() => childRenderObject.needsCompositing, throwsAssertionError); + + root.flushCompositingBits(); + expect(rootRenderObject.needsCompositing, isTrue); + expect(childRenderObject.needsCompositing, isTrue); + }); + + test("parent's render objects are painted before child's render objects", () { + final TestPipelineManifold manifold = TestPipelineManifold(); + final List log = []; + + final TestRenderObject rootRenderObject = TestRenderObject( + onPaint: () { + log.add('paint parent'); + }, + ); + final PipelineOwner root = PipelineOwner(); + root.rootNode = rootRenderObject; + final OffsetLayer rootLayer = OffsetLayer(); + rootLayer.attach(rootRenderObject); + rootRenderObject.scheduleInitialLayout(); + rootRenderObject.scheduleInitialPaint(rootLayer); + + final TestRenderObject childRenderObject = TestRenderObject( + onPaint: () { + log.add('paint child'); + }, + ); + final PipelineOwner child = PipelineOwner(); + child.rootNode = childRenderObject; + final OffsetLayer childLayer = OffsetLayer(); + childLayer.attach(childRenderObject); + childRenderObject.scheduleInitialLayout(); + childRenderObject.scheduleInitialPaint(childLayer); + + root.adoptChild(child); + root.attach(manifold); + root.flushLayout(); // Can't paint with invalid layout. + expect(log, isEmpty); + + root.flushPaint(); + expect(log, ['paint parent', 'paint child']); + }); + + test("child paint cannot dirty parent's render object", () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + final TestRenderObject rootRenderObject = TestRenderObject(); + final PipelineOwner root = PipelineOwner(); + root.rootNode = rootRenderObject; + final OffsetLayer rootLayer = OffsetLayer(); + rootLayer.attach(rootRenderObject); + rootRenderObject.scheduleInitialLayout(); + rootRenderObject.scheduleInitialPaint(rootLayer); + + bool childPaintExecuted = false; + final TestRenderObject childRenderObject = TestRenderObject( + onPaint: () { + childPaintExecuted = true; + expect(() => rootRenderObject.markNeedsPaint(), throwsAssertionError); + }, + ); + final PipelineOwner child = PipelineOwner(); + child.rootNode = childRenderObject; + final OffsetLayer childLayer = OffsetLayer(); + childLayer.attach(childRenderObject); + childRenderObject.scheduleInitialLayout(); + childRenderObject.scheduleInitialPaint(childLayer); + + root.adoptChild(child); + root.attach(manifold); + root.flushLayout(); // Can't paint with invalid layout. + root.flushPaint(); + expect(childPaintExecuted, isTrue); + }); + + test("parent's render objects do semantics before child's render objects", () { + final TestPipelineManifold manifold = TestPipelineManifold() + ..semanticsEnabled = true; + final List log = []; + + final TestRenderObject rootRenderObject = TestRenderObject( + onSemantics: () { + log.add('semantics parent'); + }, + ); + final PipelineOwner root = PipelineOwner( + onSemanticsOwnerCreated: () { + rootRenderObject.scheduleInitialSemantics(); + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + ); + root.rootNode = rootRenderObject; + + final TestRenderObject childRenderObject = TestRenderObject( + onSemantics: () { + log.add('semantics child'); + }, + ); + final PipelineOwner child = PipelineOwner( + onSemanticsOwnerCreated: () { + childRenderObject.scheduleInitialSemantics(); + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + ); + child.rootNode = childRenderObject; + + root.adoptChild(child); + root.attach(manifold); + log.clear(); + + rootRenderObject.markNeedsSemanticsUpdate(); + childRenderObject.markNeedsSemanticsUpdate(); + root.flushSemantics(); + expect(log, ['semantics parent', 'semantics child']); + }); + + test("child cannot mark parent's render object dirty during flushSemantics", () { + final TestPipelineManifold manifold = TestPipelineManifold() + ..semanticsEnabled = true; + + final TestRenderObject rootRenderObject = TestRenderObject(); + final PipelineOwner root = PipelineOwner( + onSemanticsOwnerCreated: () { + rootRenderObject.scheduleInitialSemantics(); + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + ); + root.rootNode = rootRenderObject; + + bool childSemanticsCalled = false; + final TestRenderObject childRenderObject = TestRenderObject( + onSemantics: () { + childSemanticsCalled = true; + rootRenderObject.markNeedsSemanticsUpdate(); + }, + ); + final PipelineOwner child = PipelineOwner( + onSemanticsOwnerCreated: () { + childRenderObject.scheduleInitialSemantics(); + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + ); + child.rootNode = childRenderObject; + + root.adoptChild(child); + root.attach(manifold); + rootRenderObject.markNeedsSemanticsUpdate(); + childRenderObject.markNeedsSemanticsUpdate(); + root.flushSemantics(); + + expect(childSemanticsCalled, isTrue); + }); + + test('when manifold enables semantics all PipelineOwners in tree create SemanticsOwner', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + int rootOnSemanticsOwnerCreatedCount = 0; + int rootOnSemanticsOwnerDisposed = 0; + final PipelineOwner root = PipelineOwner( + onSemanticsOwnerCreated: () { + rootOnSemanticsOwnerCreatedCount++; + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + onSemanticsOwnerDisposed: () { + rootOnSemanticsOwnerDisposed++; + }, + ); + + int childOnSemanticsOwnerCreatedCount = 0; + int childOnSemanticsOwnerDisposed = 0; + final PipelineOwner child = PipelineOwner( + onSemanticsOwnerCreated: () { + childOnSemanticsOwnerCreatedCount++; + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + onSemanticsOwnerDisposed: () { + childOnSemanticsOwnerDisposed++; + }, + ); + + root.adoptChild(child); + root.attach(manifold); + expect(rootOnSemanticsOwnerCreatedCount, 0); + expect(childOnSemanticsOwnerCreatedCount, 0); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNull); + + manifold.semanticsEnabled = true; + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + + manifold.semanticsEnabled = false; + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 1); + expect(childOnSemanticsOwnerDisposed, 1); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNull); + }); + + test('when manifold enables semantics all PipelineOwners in tree that did not have a SemanticsOwner create one', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + int rootOnSemanticsOwnerCreatedCount = 0; + int rootOnSemanticsOwnerDisposed = 0; + final PipelineOwner root = PipelineOwner( + onSemanticsOwnerCreated: () { + rootOnSemanticsOwnerCreatedCount++; + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + onSemanticsOwnerDisposed: () { + rootOnSemanticsOwnerDisposed++; + }, + ); + + int childOnSemanticsOwnerCreatedCount = 0; + int childOnSemanticsOwnerDisposed = 0; + final PipelineOwner child = PipelineOwner( + onSemanticsOwnerCreated: () { + childOnSemanticsOwnerCreatedCount++; + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + onSemanticsOwnerDisposed: () { + childOnSemanticsOwnerDisposed++; + }, + ); + + root.adoptChild(child); + root.attach(manifold); + + final SemanticsHandle childSemantics = child.ensureSemantics(); + expect(rootOnSemanticsOwnerCreatedCount, 0); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNotNull); + + manifold.semanticsEnabled = true; + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + + manifold.semanticsEnabled = false; + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 1); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNotNull); + + childSemantics.dispose(); + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 1); + expect(childOnSemanticsOwnerDisposed, 1); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNull); + }); + + test('PipelineOwner can dispose local handle even when manifold forces semantics to on', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + int rootOnSemanticsOwnerCreatedCount = 0; + int rootOnSemanticsOwnerDisposed = 0; + final PipelineOwner root = PipelineOwner( + onSemanticsOwnerCreated: () { + rootOnSemanticsOwnerCreatedCount++; + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + onSemanticsOwnerDisposed: () { + rootOnSemanticsOwnerDisposed++; + }, + ); + + int childOnSemanticsOwnerCreatedCount = 0; + int childOnSemanticsOwnerDisposed = 0; + final PipelineOwner child = PipelineOwner( + onSemanticsOwnerCreated: () { + childOnSemanticsOwnerCreatedCount++; + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + onSemanticsOwnerDisposed: () { + childOnSemanticsOwnerDisposed++; + }, + ); + + root.adoptChild(child); + root.attach(manifold); + + final SemanticsHandle childSemantics = child.ensureSemantics(); + expect(rootOnSemanticsOwnerCreatedCount, 0); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNotNull); + + manifold.semanticsEnabled = true; + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + + childSemantics.dispose(); + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + + manifold.semanticsEnabled = false; + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 1); + expect(childOnSemanticsOwnerDisposed, 1); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNull); + }); + + test('can hold on to local handle when manifold turns off semantics', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + int rootOnSemanticsOwnerCreatedCount = 0; + int rootOnSemanticsOwnerDisposed = 0; + final PipelineOwner root = PipelineOwner( + onSemanticsOwnerCreated: () { + rootOnSemanticsOwnerCreatedCount++; + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + onSemanticsOwnerDisposed: () { + rootOnSemanticsOwnerDisposed++; + }, + ); + + int childOnSemanticsOwnerCreatedCount = 0; + int childOnSemanticsOwnerDisposed = 0; + final PipelineOwner child = PipelineOwner( + onSemanticsOwnerCreated: () { + childOnSemanticsOwnerCreatedCount++; + }, + onSemanticsUpdate: (SemanticsUpdate update) { }, + onSemanticsOwnerDisposed: () { + childOnSemanticsOwnerDisposed++; + }, + ); + + root.adoptChild(child); + root.attach(manifold); + + expect(rootOnSemanticsOwnerCreatedCount, 0); + expect(childOnSemanticsOwnerCreatedCount, 0); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNull); + + manifold.semanticsEnabled = true; + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + + final SemanticsHandle childSemantics = child.ensureSemantics(); + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 0); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + + manifold.semanticsEnabled = false; + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 1); + expect(childOnSemanticsOwnerDisposed, 0); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNotNull); + + childSemantics.dispose(); + + expect(rootOnSemanticsOwnerCreatedCount, 1); + expect(childOnSemanticsOwnerCreatedCount, 1); + expect(rootOnSemanticsOwnerDisposed, 1); + expect(childOnSemanticsOwnerDisposed, 1); + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNull); + }); + + test('cannot attach when already attached', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + final PipelineOwner owner = PipelineOwner(); + + owner.attach(manifold); + expect(() => owner.attach(manifold), throwsAssertionError); + }); + + test('attach update semanticsOwner', () { + final TestPipelineManifold manifold = TestPipelineManifold() + ..semanticsEnabled = true; + final PipelineOwner owner = PipelineOwner( + onSemanticsUpdate: (_) { }, + ); + + expect(owner.semanticsOwner, isNull); + owner.attach(manifold); + expect(owner.semanticsOwner, isNotNull); + }); + + test('attach does not request visual update if nothing is dirty', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + final TestRenderObject renderObject = TestRenderObject(); + final PipelineOwner owner = PipelineOwner(); + owner.rootNode = renderObject; + + expect(manifold.requestVisualUpdateCount, 0); + owner.attach(manifold); + expect(manifold.requestVisualUpdateCount, 0); + }); + + test('cannot detach when not attached', () { + final PipelineOwner owner = PipelineOwner(); + + expect(() => owner.detach(), throwsAssertionError); + }); + + test('cannot adopt twice', () { + final PipelineOwner root = PipelineOwner(); + final PipelineOwner child = PipelineOwner(); + root.adoptChild(child); + expect(() => root.adoptChild(child), throwsAssertionError); + }); + + test('cannot adopt child of other parent', () { + final PipelineOwner root = PipelineOwner(); + final PipelineOwner child = PipelineOwner(); + final PipelineOwner otherRoot = PipelineOwner(); + root.adoptChild(child); + expect(() => otherRoot.adoptChild(child), throwsAssertionError); + }); + + test('adopting creates semantics owner if necessary', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + final PipelineOwner root = PipelineOwner( + onSemanticsUpdate: (_) { }, + ); + final PipelineOwner child = PipelineOwner( + onSemanticsUpdate: (_) { }, + ); + final PipelineOwner childOfChild = PipelineOwner( + onSemanticsUpdate: (_) { }, + ); + root.attach(manifold); + + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNull); + expect(childOfChild.semanticsOwner, isNull); + + root.adoptChild(child); + + expect(root.semanticsOwner, isNull); + expect(child.semanticsOwner, isNull); + expect(childOfChild.semanticsOwner, isNull); + + manifold.semanticsEnabled = true; + + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + expect(childOfChild.semanticsOwner, isNull); + + child.adoptChild(childOfChild); + + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + expect(childOfChild.semanticsOwner, isNotNull); + }); + + test('cannot drop unattached child', () { + final PipelineOwner root = PipelineOwner(); + final PipelineOwner child = PipelineOwner(); + expect(() => root.dropChild(child), throwsAssertionError); + }); + + test('cannot drop child attached to other parent', () { + final PipelineOwner root = PipelineOwner(); + final PipelineOwner child = PipelineOwner(); + final PipelineOwner otherRoot = PipelineOwner(); + otherRoot.adoptChild(child); + expect(() => root.dropChild(child), throwsAssertionError); + }); + + test('dropping destroys semantics owner if necessary', () { + final TestPipelineManifold manifold = TestPipelineManifold() + ..semanticsEnabled = true; + final PipelineOwner root = PipelineOwner( + onSemanticsUpdate: (_) { }, + ); + final PipelineOwner child = PipelineOwner( + onSemanticsUpdate: (_) { }, + ); + final PipelineOwner childOfChild = PipelineOwner( + onSemanticsUpdate: (_) { }, + ); + root.attach(manifold); + root.adoptChild(child); + child.adoptChild(childOfChild); + + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + expect(childOfChild.semanticsOwner, isNotNull); + + child.dropChild(childOfChild); + + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + expect(childOfChild.semanticsOwner, isNull); + + final SemanticsHandle childSemantics = child.ensureSemantics(); + root.dropChild(child); + + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNotNull); + expect(childOfChild.semanticsOwner, isNull); + + childSemantics.dispose(); + + expect(root.semanticsOwner, isNotNull); + expect(child.semanticsOwner, isNull); + expect(childOfChild.semanticsOwner, isNull); + }); + + test('can adopt/drop children during own layout', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + final PipelineOwner root = PipelineOwner(); + final PipelineOwner child1 = PipelineOwner(); + final PipelineOwner child2 = PipelineOwner(); + + final TestRenderObject rootRenderObject = TestRenderObject( + onLayout: () { + child1.dropChild(child2); + root.dropChild(child1); + root.adoptChild(child2); + child2.adoptChild(child1); + }, + ); + + root.rootNode = rootRenderObject; + rootRenderObject.scheduleInitialLayout(); + + root.adoptChild(child1); + child1.adoptChild(child2); + root.attach(manifold); + expect(_treeWalk(root), [root, child1, child2]); + + root.flushLayout(); + + expect(_treeWalk(root), [root, child2, child1]); + }); + + test('cannot adopt/drop children during child layout', () { + final TestPipelineManifold manifold = TestPipelineManifold(); + + final PipelineOwner root = PipelineOwner(); + final PipelineOwner child1 = PipelineOwner(); + final PipelineOwner child2 = PipelineOwner(); + final PipelineOwner child3 = PipelineOwner(); + + Object? droppingError; + Object? adoptingError; + + final TestRenderObject childRenderObject = TestRenderObject( + onLayout: () { + child1.dropChild(child2); + child1.adoptChild(child3); + try { + root.dropChild(child1); + } catch (e) { + droppingError = e; + } + try { + root.adoptChild(child2); + } catch (e) { + adoptingError = e; + } + }, + ); + + child1.rootNode = childRenderObject; + childRenderObject.scheduleInitialLayout(); + + root.adoptChild(child1); + child1.adoptChild(child2); + root.attach(manifold); + expect(_treeWalk(root), [root, child1, child2]); + + root.flushLayout(); + + expect(adoptingError, isAssertionError.having((AssertionError e) => e.message, 'message', contains('Cannot modify child list after layout.'))); + expect(droppingError, isAssertionError.having((AssertionError e) => e.message, 'message', contains('Cannot modify child list after layout.'))); + }); + + test('visitChildren visits all children', () { + final PipelineOwner root = PipelineOwner(); + final PipelineOwner child1 = PipelineOwner(); + final PipelineOwner child2 = PipelineOwner(); + final PipelineOwner child3 = PipelineOwner(); + final PipelineOwner childOfChild3 = PipelineOwner(); + + root.adoptChild(child1); + root.adoptChild(child2); + root.adoptChild(child3); + child3.adoptChild(childOfChild3); + + final List children = []; + root.visitChildren((PipelineOwner child) { + children.add(child); + }); + expect(children, [child1, child2, child3]); + + children.clear(); + child3.visitChildren((PipelineOwner child) { + children.add(child); + }); + expect(children.single, childOfChild3); + }); +} + +class TestPipelineManifold extends ChangeNotifier implements PipelineManifold { + int requestVisualUpdateCount = 0; + + @override + void requestVisualUpdate() { + requestVisualUpdateCount++; + } + + @override + bool get semanticsEnabled => _semanticsEnabled; + bool _semanticsEnabled = false; + set semanticsEnabled(bool value) { + if (value == _semanticsEnabled) { + return; + } + _semanticsEnabled = value; + notifyListeners(); + } +} + +class TestRenderObject extends RenderObject { + TestRenderObject({this.onLayout, this.onPaint, this.onSemantics}); + + final VoidCallback? onLayout; + final VoidCallback? onPaint; + final VoidCallback? onSemantics; + + @override + bool get isRepaintBoundary => true; + + @override + void debugAssertDoesMeetConstraints() { } + + @override + Rect get paintBounds => Rect.zero; + + @override + void performLayout() { + onLayout?.call(); + } + + @override + void paint(PaintingContext context, Offset offset) { + onPaint?.call(); + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + onSemantics?.call(); + } + + @override + void performResize() { } + + @override + Rect get semanticBounds => Rect.zero; +} + +List _treeWalk(PipelineOwner root) { + final List results = [root]; + + void visitor(PipelineOwner child) { + results.add(child); + child.visitChildren(visitor); + } + + root.visitChildren(visitor); + return results; +} diff --git a/packages/flutter_test/lib/src/widget_tester.dart b/packages/flutter_test/lib/src/widget_tester.dart index af24daf5b6..c36c632240 100644 --- a/packages/flutter_test/lib/src/widget_tester.dart +++ b/packages/flutter_test/lib/src/widget_tester.dart @@ -1045,7 +1045,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher, Ticker void _verifySemanticsHandlesWereDisposed() { assert(_lastRecordedSemanticsHandles != null); // TODO(goderbauer): Fix known leak in web engine when running integration tests and remove this "correction", https://github.com/flutter/flutter/issues/121640. - final int knownWebEngineLeakForLiveTestsCorrection = kIsWeb && binding is LiveTestWidgetsFlutterBinding ? 2 : 0; + final int knownWebEngineLeakForLiveTestsCorrection = kIsWeb && binding is LiveTestWidgetsFlutterBinding ? 1 : 0; if (_currentSemanticsHandles - knownWebEngineLeakForLiveTestsCorrection > _lastRecordedSemanticsHandles!) { throw FlutterError.fromParts([