From 297f094c01a569631520ced479c9d9fc16d4b94b Mon Sep 17 00:00:00 2001 From: Michael Goderbauer Date: Wed, 7 Dec 2022 11:26:12 -0800 Subject: [PATCH] LookupBoundary (#116429) * LookupBoundary simplified * tests * doc and impl complete * doc fixes * add more tests * review * empty --- .../flutter/lib/src/widgets/framework.dart | 38 +- .../lib/src/widgets/lookup_boundary.dart | 255 +++++ packages/flutter/lib/widgets.dart | 1 + .../test/widgets/lookup_boundary_test.dart | 1020 +++++++++++++++++ 4 files changed, 1307 insertions(+), 7 deletions(-) create mode 100644 packages/flutter/lib/src/widgets/lookup_boundary.dart create mode 100644 packages/flutter/test/widgets/lookup_boundary_test.dart diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart index b37b90ea5c..a1ef69e3e8 100644 --- a/packages/flutter/lib/src/widgets/framework.dart +++ b/packages/flutter/lib/src/widgets/framework.dart @@ -2020,6 +2020,13 @@ class _InactiveElements { /// this callback. typedef ElementVisitor = void Function(Element element); +/// Signature for the callback to [BuildContext.visitAncestorElements]. +/// +/// The argument is the ancestor being visited. +/// +/// Return false to stop the walk. +typedef ConditionalElementVisitor = bool Function(Element element); + /// A handle to the location of a widget in the widget tree. /// /// This class presents a set of methods that can be used from @@ -2221,7 +2228,7 @@ abstract class BuildContext { /// /// All of the qualifications about when [dependOnInheritedWidgetOfExactType] can /// be called apply to this method as well. - InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object aspect }); + InheritedWidget dependOnInheritedElement(InheritedElement ancestor, { Object? aspect }); /// Obtains the nearest widget of the given type `T`, which must be the type of a /// concrete [InheritedWidget] subclass, and registers this build context with @@ -2229,6 +2236,7 @@ abstract class BuildContext { /// type is introduced, or the widget goes away), this build context is /// rebuilt so that it can obtain new values from that widget. /// + /// {@template flutter.widgets.BuildContext.dependOnInheritedWidgetOfExactType} /// This is typically called implicitly from `of()` static methods, e.g. /// [Theme.of]. /// @@ -2262,6 +2270,7 @@ abstract class BuildContext { /// [InheritedWidget] subclasses that supports partial updates, like /// [InheritedModel]. It specifies what "aspect" of the inherited /// widget this context depends on. + /// {@endtemplate} T? dependOnInheritedWidgetOfExactType({ Object? aspect }); /// Obtains the element corresponding to the nearest widget of the given type `T`, @@ -2269,6 +2278,7 @@ abstract class BuildContext { /// /// Returns null if no such element is found. /// + /// {@template flutter.widgets.BuildContext.getElementForInheritedWidgetOfExactType} /// Calling this method is O(1) with a small constant factor. /// /// This method does not establish a relationship with the target in the way @@ -2280,11 +2290,13 @@ abstract class BuildContext { /// [dependOnInheritedWidgetOfExactType] in [State.didChangeDependencies]. It is /// safe to use this method from [State.deactivate], which is called whenever /// the widget is removed from the tree. + /// {@endtemplate} InheritedElement? getElementForInheritedWidgetOfExactType(); /// Returns the nearest ancestor widget of the given type `T`, which must be the /// type of a concrete [Widget] subclass. /// + /// {@template flutter.widgets.BuildContext.findAncestorWidgetOfExactType} /// In general, [dependOnInheritedWidgetOfExactType] is more useful, since /// inherited widgets will trigger consumers to rebuild when they change. This /// method is appropriate when used in interaction event handlers (e.g. @@ -2306,11 +2318,13 @@ abstract class BuildContext { /// /// Returns null if a widget of the requested type does not appear in the /// ancestors of this context. + /// {@endtemplate} T? findAncestorWidgetOfExactType(); /// Returns the [State] object of the nearest ancestor [StatefulWidget] widget /// that is an instance of the given type `T`. /// + /// {@template flutter.widgets.BuildContext.findAncestorStateOfType} /// This should not be used from build methods, because the build context will /// not be rebuilt if the value that would be returned by this method changes. /// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such @@ -2332,6 +2346,7 @@ abstract class BuildContext { /// because the widget tree is no longer stable at that time. To refer to /// an ancestor from one of those methods, save a reference to the ancestor /// by calling [findAncestorStateOfType] in [State.didChangeDependencies]. + /// {@endtemplate} /// /// {@tool snippet} /// @@ -2344,17 +2359,20 @@ abstract class BuildContext { /// Returns the [State] object of the furthest ancestor [StatefulWidget] widget /// that is an instance of the given type `T`. /// + /// {@template flutter.widgets.BuildContext.findRootAncestorStateOfType} /// Functions the same way as [findAncestorStateOfType] but keeps visiting subsequent /// ancestors until there are none of the type instance of `T` remaining. /// Then returns the last one found. /// /// This operation is O(N) as well though N is the entire widget tree rather than /// a subtree. + /// {@endtemplate} T? findRootAncestorStateOfType(); /// Returns the [RenderObject] object of the nearest ancestor [RenderObjectWidget] widget /// that is an instance of the given type `T`. /// + /// {@template flutter.widgets.BuildContext.findAncestorRenderObjectOfType} /// This should not be used from build methods, because the build context will /// not be rebuilt if the value that would be returned by this method changes. /// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such @@ -2371,13 +2389,16 @@ abstract class BuildContext { /// because the widget tree is no longer stable at that time. To refer to /// an ancestor from one of those methods, save a reference to the ancestor /// by calling [findAncestorRenderObjectOfType] in [State.didChangeDependencies]. + /// {@endtemplate} T? findAncestorRenderObjectOfType(); /// Walks the ancestor chain, starting with the parent of this build context's - /// widget, invoking the argument for each ancestor. The callback is given a - /// reference to the ancestor widget's corresponding [Element] object. The - /// walk stops when it reaches the root widget or when the callback returns - /// false. The callback must not return null. + /// widget, invoking the argument for each ancestor. + /// + /// {@template flutter.widgets.BuildContext.visitAncestorElements} + /// The callback is given a reference to the ancestor widget's corresponding + /// [Element] object. The walk stops when it reaches the root widget or when + /// the callback returns false. The callback must not return null. /// /// This is useful for inspecting the widget tree. /// @@ -2387,10 +2408,12 @@ abstract class BuildContext { /// because the element tree is no longer stable at that time. To refer to /// an ancestor from one of those methods, save a reference to the ancestor /// by calling [visitAncestorElements] in [State.didChangeDependencies]. - void visitAncestorElements(bool Function(Element element) visitor); + /// {@endtemplate} + void visitAncestorElements(ConditionalElementVisitor visitor); /// Walks the children of this widget. /// + /// {@template flutter.widgets.BuildContext.visitChildElements} /// This is useful for applying changes to children after they are built /// without waiting for the next frame, especially if the children are known, /// and especially if there is exactly one child (as is always the case for @@ -2408,6 +2431,7 @@ abstract class BuildContext { /// significantly cheaper to use an [InheritedWidget] and have the descendants /// pull data down, than it is to use [visitChildElements] recursively to push /// data down to them. + /// {@endtemplate} void visitChildElements(ElementVisitor visitor); /// Start bubbling this notification at the given build context. @@ -4452,7 +4476,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { } @override - void visitAncestorElements(bool Function(Element element) visitor) { + void visitAncestorElements(ConditionalElementVisitor visitor) { assert(_debugCheckStateIsActiveForAncestorLookup()); Element? ancestor = _parent; while (ancestor != null && visitor(ancestor)) { diff --git a/packages/flutter/lib/src/widgets/lookup_boundary.dart b/packages/flutter/lib/src/widgets/lookup_boundary.dart new file mode 100644 index 0000000000..dc903dade4 --- /dev/null +++ b/packages/flutter/lib/src/widgets/lookup_boundary.dart @@ -0,0 +1,255 @@ +// 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 'framework.dart'; + +// Examples can assume: +// class MyWidget extends StatelessWidget { const MyWidget({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) => child; } + +/// A lookup boundary controls what entities are visible to descendants of the +/// boundary via the static lookup methods provided by the boundary. +/// +/// The static lookup methods of the boundary mirror the lookup methods by the +/// same name exposed on [BuildContext] and they can be used as direct +/// replacements. Unlike the methods on [BuildContext], these methods do not +/// find any ancestor entities of the closest [LookupBoundary] surrounding the +/// provided [BuildContext]. The root of the tree is an implicit lookup boundary. +/// +/// {@tool snippet} +/// In the example below, the [LookupBoundary.findAncestorWidgetOfExactType] +/// call returns null because the [LookupBoundary] "hides" `MyWidget` from the +/// [BuildContext] that was queried. +/// +/// ```dart +/// MyWidget( +/// child: LookupBoundary( +/// child: Builder( +/// builder: (BuildContext context) { +/// MyWidget? widget = LookupBoundary.findAncestorWidgetOfExactType(context); +/// return Text('$widget'); // "null" +/// }, +/// ), +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// A [LookupBoundary] only affects the behavior of the static lookup methods +/// defined on the boundary. It does not affect the behavior of the lookup +/// methods defined on [BuildContext]. +/// +/// A [LookupBoundary] is rarely instantiated directly. They are inserted at +/// locations of the widget tree where the render tree diverges from the element +/// tree, which is rather uncommon. Such anomalies are created by +/// [RenderObjectElement]s that don't attach their [RenderObject] to the closest +/// ancestor [RenderObjectElement], e.g. because they bootstrap a separate +/// stand-alone render tree. +// TODO(goderbauer): Reference the View widget here once available. +/// This behavior breaks the assumption some widgets have about the structure of +/// the render tree: These widgets may try to reach out to an ancestor widget, +/// assuming that their associated [RenderObject]s are also ancestors, which due +/// to the anomaly may not be the case. At the point where the divergence in the +/// two trees is introduced, a [LookupBoundary] can be used to hide that ancestor +/// from the querying widget. +/// +/// As an example, [Material.of] relies on lookup boundaries to hide the +/// [Material] widget from certain descendant button widget. Buttons reach out +/// to their [Material] ancestor to draw ink splashes on its associated render +/// object. This only produces the desired effect if the button render object +/// is a descendant of the [Material] render object. If the element tree and +/// the render tree are not in sync due to anomalies described above, this may +/// not be the case. To avoid incorrect visuals, the [Material] relies on +/// lookup boundaries to hide itself from descendants in subtrees with such +/// anomalies. Those subtrees are expected to introduce their own [Material] +/// widget that buttons there can utilize without crossing a lookup boundary. +class LookupBoundary extends InheritedWidget { + /// Creates a [LookupBoundary]. + /// + /// A none-null [child] widget must be provided. + const LookupBoundary({super.key, required super.child}); + + /// Obtains the nearest widget of the given type `T` within the current + /// [LookupBoundary] of `context`, which must be the type of a concrete + /// [InheritedWidget] subclass, and registers the provided build `context` + /// with that widget such that when that widget changes (or a new widget of + /// that type is introduced, or the widget goes away), the build context is + /// rebuilt so that it can obtain new values from that widget. + /// + /// This method behaves exactly like + /// [BuildContext.dependOnInheritedWidgetOfExactType], except it only + /// considers [InheritedWidget]s of the specified type `T` between the + /// provided [BuildContext] and its closest [LookupBoundary] ancestor. + /// [InheritedWidget]s past that [LookupBoundary] are invisible to this + /// method. The root of the tree is treated as an implicit lookup boundary. + /// + /// {@macro flutter.widgets.BuildContext.dependOnInheritedWidgetOfExactType} + static T? dependOnInheritedWidgetOfExactType(BuildContext context, { Object? aspect }) { + // The following call makes sure that context depends on something so + // Element.didChangeDependencies is called when context moves in the tree + // even when requested dependency remains unfulfilled (i.e. null is + // returned). + context.dependOnInheritedWidgetOfExactType(); + final InheritedElement? candidate = getElementForInheritedWidgetOfExactType(context); + if (candidate == null) { + return null; + } + context.dependOnInheritedElement(candidate, aspect: aspect); + return candidate.widget as T; + } + + /// Obtains the element corresponding to the nearest widget of the given type + /// `T` within the current [LookupBoundary] of `context`. + /// + /// `T` must be the type of a concrete [InheritedWidget] subclass. Returns + /// null if no such element is found. + /// + /// This method behaves exactly like + /// [BuildContext.getElementForInheritedWidgetOfExactType], except it only + /// considers [InheritedWidget]s of the specified type `T` between the + /// provided [BuildContext] and its closest [LookupBoundary] ancestor. + /// [InheritedWidget]s past that [LookupBoundary] are invisible to this + /// method. The root of the tree is treated as an implicit lookup boundary. + /// + /// {@macro flutter.widgets.BuildContext.getElementForInheritedWidgetOfExactType} + static InheritedElement? getElementForInheritedWidgetOfExactType(BuildContext context) { + final InheritedElement? candidate = context.getElementForInheritedWidgetOfExactType(); + if (candidate == null) { + return null; + } + final Element? boundary = context.getElementForInheritedWidgetOfExactType(); + if (boundary != null && boundary.depth > candidate.depth) { + return null; + } + return candidate; + } + + /// Returns the nearest ancestor widget of the given type `T` within the + /// current [LookupBoundary] of `context`. + /// + /// `T` must be the type of a concrete [Widget] subclass. + /// + /// This method behaves exactly like + /// [BuildContext.findAncestorWidgetOfExactType], except it only considers + /// [Widget]s of the specified type `T` between the provided [BuildContext] + /// and its closest [LookupBoundary] ancestor. [Widget]s past that + /// [LookupBoundary] are invisible to this method. The root of the tree is + /// treated as an implicit lookup boundary. + /// + /// {@macro flutter.widgets.BuildContext.findAncestorWidgetOfExactType} + static T? findAncestorWidgetOfExactType(BuildContext context) { + Element? target; + context.visitAncestorElements((Element ancestor) { + if (ancestor.widget.runtimeType == T) { + target = ancestor; + return false; + } + return ancestor.widget.runtimeType != LookupBoundary; + }); + return target?.widget as T?; + } + + /// Returns the [State] object of the nearest ancestor [StatefulWidget] widget + /// within the current [LookupBoundary] of `context` that is an instance of + /// the given type `T`. + /// + /// This method behaves exactly like + /// [BuildContext.findAncestorWidgetOfExactType], except it only considers + /// [State] objects of the specified type `T` between the provided + /// [BuildContext] and its closest [LookupBoundary] ancestor. [State] objects + /// past that [LookupBoundary] are invisible to this method. The root of the + /// tree is treated as an implicit lookup boundary. + /// + /// {@macro flutter.widgets.BuildContext.findAncestorStateOfType} + static T? findAncestorStateOfType(BuildContext context) { + StatefulElement? target; + context.visitAncestorElements((Element ancestor) { + if (ancestor is StatefulElement && ancestor.state is T) { + target = ancestor; + return false; + } + return ancestor.widget.runtimeType != LookupBoundary; + }); + return target?.state as T?; + } + + /// Returns the [State] object of the furthest ancestor [StatefulWidget] + /// widget within the current [LookupBoundary] of `context` that is an + /// instance of the given type `T`. + /// + /// This method behaves exactly like + /// [BuildContext.findRootAncestorStateOfType], except it considers the + /// closest [LookupBoundary] ancestor of `context` to be the root. [State] + /// objects past that [LookupBoundary] are invisible to this method. The root + /// of the tree is treated as an implicit lookup boundary. + /// + /// {@macro flutter.widgets.BuildContext.findRootAncestorStateOfType} + static T? findRootAncestorStateOfType(BuildContext context) { + StatefulElement? target; + context.visitAncestorElements((Element ancestor) { + if (ancestor is StatefulElement && ancestor.state is T) { + target = ancestor; + } + return ancestor.widget.runtimeType != LookupBoundary; + }); + return target?.state as T?; + } + + /// Returns the [RenderObject] object of the nearest ancestor + /// [RenderObjectWidget] widget within the current [LookupBoundary] of + /// `context` that is an instance of the given type `T`. + /// + /// This method behaves exactly like + /// [BuildContext.findAncestorRenderObjectOfType], except it only considers + /// [RenderObject]s of the specified type `T` between the provided + /// [BuildContext] and its closest [LookupBoundary] ancestor. [RenderObject]s + /// past that [LookupBoundary] are invisible to this method. The root of the + /// tree is treated as an implicit lookup boundary. + /// + /// {@macro flutter.widgets.BuildContext.findAncestorRenderObjectOfType} + static T? findAncestorRenderObjectOfType(BuildContext context) { + Element? target; + context.visitAncestorElements((Element ancestor) { + if (ancestor is RenderObjectElement && ancestor.renderObject is T) { + target = ancestor; + return false; + } + return ancestor.widget.runtimeType != LookupBoundary; + }); + return target?.renderObject as T?; + } + + /// Walks the ancestor chain, starting with the parent of the build context's + /// widget, invoking the argument for each ancestor until a [LookupBoundary] + /// or the root is reached. + /// + /// This method behaves exactly like [BuildContext.visitAncestorElements], + /// except it only walks the tree up to the closest [LookupBoundary] ancestor + /// of the provided context. The root of the tree is treated as an implicit + /// lookup boundary. + /// + /// {@macro flutter.widgets.BuildContext.visitAncestorElements} + static void visitAncestorElements(BuildContext context, ConditionalElementVisitor visitor) { + context.visitAncestorElements((Element ancestor) { + return visitor(ancestor) && ancestor.widget.runtimeType != LookupBoundary; + }); + } + + /// Walks the non-[LookupBoundary] child [Element]s of the provided + /// `context`. + /// + /// This method behaves exactly like [BuildContext.visitChildElements], + /// except it only visits children that are not a [LookupBoundary]. + /// + /// {@macro flutter.widgets.BuildContext.visitChildElements} + static void visitChildElements(BuildContext context, ElementVisitor visitor) { + context.visitChildElements((Element child) { + if (child.widget.runtimeType != LookupBoundary) { + visitor(child); + } + }); + } + + @override + bool updateShouldNotify(covariant InheritedWidget oldWidget) => false; +} diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart index 03fe927d73..e2c5e39b10 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -72,6 +72,7 @@ export 'src/widgets/keyboard_listener.dart'; export 'src/widgets/layout_builder.dart'; export 'src/widgets/list_wheel_scroll_view.dart'; export 'src/widgets/localizations.dart'; +export 'src/widgets/lookup_boundary.dart'; export 'src/widgets/magnifier.dart'; export 'src/widgets/media_query.dart'; export 'src/widgets/modal_barrier.dart'; diff --git a/packages/flutter/test/widgets/lookup_boundary_test.dart b/packages/flutter/test/widgets/lookup_boundary_test.dart new file mode 100644 index 0000000000..c5b8bfcb5f --- /dev/null +++ b/packages/flutter/test/widgets/lookup_boundary_test.dart @@ -0,0 +1,1020 @@ +// 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/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('LookupBoundary.dependOnInheritedWidgetOfExactType', () { + testWidgets('respects boundary', (WidgetTester tester) async { + InheritedWidget? containerThroughBoundary; + InheritedWidget? containerStoppedAtBoundary; + + final Key inheritedKey = UniqueKey(); + + await tester.pumpWidget(MyInheritedWidget( + value: 2, + key: inheritedKey, + child: LookupBoundary( + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.dependOnInheritedWidgetOfExactType(); + containerStoppedAtBoundary = LookupBoundary.dependOnInheritedWidgetOfExactType(context); + return const SizedBox.expand(); + }, + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.widget(find.byKey(inheritedKey)))); + expect(containerStoppedAtBoundary, isNull); + }); + + testWidgets('ignores ancestor boundary', (WidgetTester tester) async { + InheritedWidget? inheritedWidget; + + final Key inheritedKey = UniqueKey(); + + await tester.pumpWidget(LookupBoundary( + child: MyInheritedWidget( + value: 2, + key: inheritedKey, + child: Builder( + builder: (BuildContext context) { + inheritedWidget = LookupBoundary.dependOnInheritedWidgetOfExactType(context); + return const SizedBox.expand(); + }, + ), + ), + )); + + expect(inheritedWidget, equals(tester.widget(find.byKey(inheritedKey)))); + }); + + testWidgets('finds widget before boundary', (WidgetTester tester) async { + InheritedWidget? containerThroughBoundary; + InheritedWidget? containerStoppedAtBoundary; + + final Key inheritedKey = UniqueKey(); + + await tester.pumpWidget(MyInheritedWidget( + value: 2, + child: LookupBoundary( + child: MyInheritedWidget( + key: inheritedKey, + value: 1, + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.dependOnInheritedWidgetOfExactType(); + containerStoppedAtBoundary = LookupBoundary.dependOnInheritedWidgetOfExactType(context); + return const SizedBox.expand(); + }, + ), + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.widget(find.byKey(inheritedKey)))); + expect(containerStoppedAtBoundary, equals(tester.widget(find.byKey(inheritedKey)))); + }); + + testWidgets('creates dependency', (WidgetTester tester) async { + MyInheritedWidget? inheritedWidget; + + final Widget widgetTree = DidChangeDependencySpy( + onDidChangeDependencies: (BuildContext context) { + inheritedWidget = LookupBoundary.dependOnInheritedWidgetOfExactType(context); + }, + ); + + await tester.pumpWidget( + MyInheritedWidget( + value: 1, + child: widgetTree, + ), + ); + expect(inheritedWidget!.value, 1); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + await tester.pumpWidget( + MyInheritedWidget( + value: 2, + child: widgetTree, + ), + ); + expect(inheritedWidget!.value, 2); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 2); + }); + + testWidgets('causes didChangeDependencies to be called on move even if dependency was not fulfilled due to boundary', (WidgetTester tester) async { + MyInheritedWidget? inheritedWidget; + final Key globalKey = GlobalKey(); + + final Widget widgetTree = DidChangeDependencySpy( + key: globalKey, + onDidChangeDependencies: (BuildContext context) { + inheritedWidget = LookupBoundary.dependOnInheritedWidgetOfExactType(context); + }, + ); + + await tester.pumpWidget( + MyInheritedWidget( + value: 1, + child: LookupBoundary( + child: widgetTree, + ), + ), + ); + expect(inheritedWidget, isNull); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + // Value of inherited widget changes, but there should be no dependency due to boundary. + await tester.pumpWidget( + MyInheritedWidget( + value: 2, + child: LookupBoundary( + child: widgetTree, + ), + ), + ); + expect(inheritedWidget, isNull); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + // Widget is moved, didChangeDependencies is called, but dependency is still not found due to boundary. + await tester.pumpWidget( + SizedBox( + child: MyInheritedWidget( + value: 2, + child: LookupBoundary( + child: widgetTree, + ), + ), + ), + ); + expect(inheritedWidget, isNull); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 2); + + await tester.pumpWidget( + SizedBox( + child: MyInheritedWidget( + value: 2, + child: LookupBoundary( + child: MyInheritedWidget( + value: 4, + child: widgetTree, + ), + ), + ), + ), + ); + expect(inheritedWidget!.value, 4); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 3); + }); + + testWidgets('causes didChangeDependencies to be called on move even if dependency was non-existant', (WidgetTester tester) async { + MyInheritedWidget? inheritedWidget; + final Key globalKey = GlobalKey(); + + final Widget widgetTree = DidChangeDependencySpy( + key: globalKey, + onDidChangeDependencies: (BuildContext context) { + inheritedWidget = LookupBoundary.dependOnInheritedWidgetOfExactType(context); + }, + ); + + await tester.pumpWidget(widgetTree); + expect(inheritedWidget, isNull); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + // Widget moved, didChangeDependencies must be called. + await tester.pumpWidget( + SizedBox( + child: widgetTree, + ), + ); + expect(inheritedWidget, isNull); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 2); + + // Widget moved, didChangeDependencies must be called. + await tester.pumpWidget( + MyInheritedWidget( + value: 6, + child: SizedBox( + child: widgetTree, + ), + ), + ); + expect(inheritedWidget!.value, 6); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 3); + }); + }); + + group('LookupBoundary.getElementForInheritedWidgetOfExactType', () { + testWidgets('respects boundary', (WidgetTester tester) async { + InheritedElement? containerThroughBoundary; + InheritedElement? containerStoppedAtBoundary; + + final Key inheritedKey = UniqueKey(); + + await tester.pumpWidget(MyInheritedWidget( + value: 2, + key: inheritedKey, + child: LookupBoundary( + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.getElementForInheritedWidgetOfExactType(); + containerStoppedAtBoundary = LookupBoundary.getElementForInheritedWidgetOfExactType(context); + return const SizedBox.expand(); + }, + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.element(find.byKey(inheritedKey)))); + expect(containerStoppedAtBoundary, isNull); + }); + + testWidgets('ignores ancestor boundary', (WidgetTester tester) async { + InheritedElement? inheritedWidget; + + final Key inheritedKey = UniqueKey(); + + await tester.pumpWidget(LookupBoundary( + child: MyInheritedWidget( + value: 2, + key: inheritedKey, + child: Builder( + builder: (BuildContext context) { + inheritedWidget = LookupBoundary.getElementForInheritedWidgetOfExactType(context); + return const SizedBox.expand(); + }, + ), + ), + )); + + expect(inheritedWidget, equals(tester.element(find.byKey(inheritedKey)))); + }); + + testWidgets('finds widget before boundary', (WidgetTester tester) async { + InheritedElement? containerThroughBoundary; + InheritedElement? containerStoppedAtBoundary; + + final Key inheritedKey = UniqueKey(); + + await tester.pumpWidget(MyInheritedWidget( + value: 2, + child: LookupBoundary( + child: MyInheritedWidget( + key: inheritedKey, + value: 1, + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.getElementForInheritedWidgetOfExactType(); + containerStoppedAtBoundary = LookupBoundary.getElementForInheritedWidgetOfExactType(context); + return const SizedBox.expand(); + }, + ), + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.element(find.byKey(inheritedKey)))); + expect(containerStoppedAtBoundary, equals(tester.element(find.byKey(inheritedKey)))); + }); + + testWidgets('does not creates dependency', (WidgetTester tester) async { + + final Widget widgetTree = DidChangeDependencySpy( + onDidChangeDependencies: (BuildContext context) { + LookupBoundary.getElementForInheritedWidgetOfExactType(context); + }, + ); + + await tester.pumpWidget( + MyInheritedWidget( + value: 1, + child: widgetTree, + ), + ); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + await tester.pumpWidget( + MyInheritedWidget( + value: 2, + child: widgetTree, + ), + ); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + }); + + testWidgets('does not cause didChangeDependencies to be called on move when found', (WidgetTester tester) async { + final Key globalKey = GlobalKey(); + + final Widget widgetTree = DidChangeDependencySpy( + key: globalKey, + onDidChangeDependencies: (BuildContext context) { + LookupBoundary.getElementForInheritedWidgetOfExactType(context); + }, + ); + + await tester.pumpWidget( + MyInheritedWidget( + value: 1, + child: LookupBoundary( + child: widgetTree, + ), + ), + ); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + // Value of inherited widget changes, but there should be no dependency due to boundary. + await tester.pumpWidget( + MyInheritedWidget( + value: 2, + child: LookupBoundary( + child: widgetTree, + ), + ), + ); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + // Widget is moved, didChangeDependencies is called, but dependency is still not found due to boundary. + await tester.pumpWidget( + SizedBox( + child: MyInheritedWidget( + value: 2, + child: LookupBoundary( + child: widgetTree, + ), + ), + ), + ); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + await tester.pumpWidget( + SizedBox( + child: MyInheritedWidget( + value: 2, + child: LookupBoundary( + child: MyInheritedWidget( + value: 4, + child: widgetTree, + ), + ), + ), + ), + ); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + }); + + testWidgets('does not cause didChangeDependencies to be called on move when nothing was found', (WidgetTester tester) async { + final Key globalKey = GlobalKey(); + + final Widget widgetTree = DidChangeDependencySpy( + key: globalKey, + onDidChangeDependencies: (BuildContext context) { + LookupBoundary.getElementForInheritedWidgetOfExactType(context); + }, + ); + + await tester.pumpWidget(widgetTree); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + // Widget moved, didChangeDependencies must be called. + await tester.pumpWidget( + SizedBox( + child: widgetTree, + ), + ); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + // Widget moved, didChangeDependencies must be called. + await tester.pumpWidget( + MyInheritedWidget( + value: 6, + child: SizedBox( + child: widgetTree, + ), + ), + ); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + }); + }); + + group('LookupBoundary.findAncestorWidgetOfExactType', () { + testWidgets('respects boundary', (WidgetTester tester) async { + Widget? containerThroughBoundary; + Widget? containerStoppedAtBoundary; + Widget? boundaryThroughBoundary; + Widget? boundaryStoppedAtBoundary; + + final Key containerKey = UniqueKey(); + final Key boundaryKey = UniqueKey(); + + await tester.pumpWidget(Container( + key: containerKey, + child: LookupBoundary( + key: boundaryKey, + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.findAncestorWidgetOfExactType(); + containerStoppedAtBoundary = LookupBoundary.findAncestorWidgetOfExactType(context); + boundaryThroughBoundary = context.findAncestorWidgetOfExactType(); + boundaryStoppedAtBoundary = LookupBoundary.findAncestorWidgetOfExactType(context); + return const SizedBox.expand(); + }, + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.widget(find.byKey(containerKey)))); + expect(containerStoppedAtBoundary, isNull); + expect(boundaryThroughBoundary, equals(tester.widget(find.byKey(boundaryKey)))); + expect(boundaryStoppedAtBoundary, equals(tester.widget(find.byKey(boundaryKey)))); + }); + + testWidgets('finds right widget before boundary', (WidgetTester tester) async { + Widget? containerThroughBoundary; + Widget? containerStoppedAtBoundary; + + final Key outerContainerKey = UniqueKey(); + final Key innerContainerKey = UniqueKey(); + + await tester.pumpWidget(Container( + key: outerContainerKey, + child: LookupBoundary( + child: Container( + color: Colors.blue, + child: Container( + key: innerContainerKey, + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.findAncestorWidgetOfExactType(); + containerStoppedAtBoundary = LookupBoundary.findAncestorWidgetOfExactType(context); + return const SizedBox.expand(); + }, + ), + ), + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.widget(find.byKey(innerContainerKey)))); + expect(containerStoppedAtBoundary, equals(tester.widget(find.byKey(innerContainerKey)))); + }); + + testWidgets('works if nothing is found', (WidgetTester tester) async { + Widget? containerStoppedAtBoundary; + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + containerStoppedAtBoundary = LookupBoundary.findAncestorWidgetOfExactType(context); + return const SizedBox.expand(); + }, + )); + + expect(containerStoppedAtBoundary, isNull); + }); + + testWidgets('does not establish a dependency', (WidgetTester tester) async { + Widget? containerThroughBoundary; + Widget? containerStoppedAtBoundary; + Widget? containerStoppedAtBoundaryUnfulfilled; + + final Key innerContainerKey = UniqueKey(); + final Key globalKey = GlobalKey(); + + final Widget widgetTree = LookupBoundary( + child: Container( + key: innerContainerKey, + child: DidChangeDependencySpy( + key: globalKey, + onDidChangeDependencies: (BuildContext context) { + containerThroughBoundary = context.findAncestorWidgetOfExactType(); + containerStoppedAtBoundary = LookupBoundary.findAncestorWidgetOfExactType(context); + containerStoppedAtBoundaryUnfulfilled = LookupBoundary.findAncestorWidgetOfExactType(context); + }, + ), + ), + ); + + await tester.pumpWidget(widgetTree); + + expect(containerThroughBoundary, equals(tester.widget(find.byKey(innerContainerKey)))); + expect(containerStoppedAtBoundary, equals(tester.widget(find.byKey(innerContainerKey)))); + expect(containerStoppedAtBoundaryUnfulfilled, isNull); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + await tester.pumpWidget( + SizedBox( // Changes tree structure, triggers global key move of DidChangeDependencySpy. + child: widgetTree, + ), + ); + + // Tree restructuring above would have called didChangeDependencies if dependency had been established. + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + }); + }); + + group('LookupBoundary.findAncestorStateOfType', () { + testWidgets('respects boundary', (WidgetTester tester) async { + State? containerThroughBoundary; + State? containerStoppedAtBoundary; + + final Key containerKey = UniqueKey(); + + await tester.pumpWidget(MyStatefulContainer( + key: containerKey, + child: LookupBoundary( + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.findAncestorStateOfType(); + containerStoppedAtBoundary = LookupBoundary.findAncestorStateOfType(context); + return const SizedBox.expand(); + }, + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.state(find.byKey(containerKey)))); + expect(containerStoppedAtBoundary, isNull); + }); + + testWidgets('finds right widget before boundary', (WidgetTester tester) async { + State? containerThroughBoundary; + State? containerStoppedAtBoundary; + + final Key outerContainerKey = UniqueKey(); + final Key innerContainerKey = UniqueKey(); + + await tester.pumpWidget(MyStatefulContainer( + key: outerContainerKey, + child: LookupBoundary( + child: MyStatefulContainer( + child: MyStatefulContainer( + key: innerContainerKey, + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.findAncestorStateOfType(); + containerStoppedAtBoundary = LookupBoundary.findAncestorStateOfType(context); + return const SizedBox.expand(); + }, + ), + ), + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.state(find.byKey(innerContainerKey)))); + expect(containerStoppedAtBoundary, equals(tester.state(find.byKey(innerContainerKey)))); + }); + + testWidgets('works if nothing is found', (WidgetTester tester) async { + State? containerStoppedAtBoundary; + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + containerStoppedAtBoundary = LookupBoundary.findAncestorStateOfType(context); + return const SizedBox.expand(); + }, + )); + + expect(containerStoppedAtBoundary, isNull); + }); + + testWidgets('does not establish a dependency', (WidgetTester tester) async { + State? containerThroughBoundary; + State? containerStoppedAtBoundary; + State? containerStoppedAtBoundaryUnfulfilled; + + final Key innerContainerKey = UniqueKey(); + final Key globalKey = GlobalKey(); + + final Widget widgetTree = LookupBoundary( + child: MyStatefulContainer( + key: innerContainerKey, + child: DidChangeDependencySpy( + key: globalKey, + onDidChangeDependencies: (BuildContext context) { + containerThroughBoundary = context.findAncestorStateOfType(); + containerStoppedAtBoundary = LookupBoundary.findAncestorStateOfType(context); + containerStoppedAtBoundaryUnfulfilled = LookupBoundary.findAncestorStateOfType(context); + }, + ), + ), + ); + + await tester.pumpWidget(widgetTree); + + expect(containerThroughBoundary, equals(tester.state(find.byKey(innerContainerKey)))); + expect(containerStoppedAtBoundary, equals(tester.state(find.byKey(innerContainerKey)))); + expect(containerStoppedAtBoundaryUnfulfilled, isNull); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + await tester.pumpWidget( + SizedBox( // Changes tree structure, triggers global key move of DidChangeDependencySpy. + child: widgetTree, + ), + ); + + // Tree restructuring above would have called didChangeDependencies if dependency had been established. + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + }); + }); + + group('LookupBoundary.findRootAncestorStateOfType', () { + testWidgets('respects boundary', (WidgetTester tester) async { + State? containerThroughBoundary; + State? containerStoppedAtBoundary; + + final Key containerKey = UniqueKey(); + + await tester.pumpWidget(MyStatefulContainer( + key: containerKey, + child: LookupBoundary( + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.findRootAncestorStateOfType(); + containerStoppedAtBoundary = LookupBoundary.findRootAncestorStateOfType(context); + return const SizedBox.expand(); + }, + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.state(find.byKey(containerKey)))); + expect(containerStoppedAtBoundary, isNull); + }); + + testWidgets('finds right widget before boundary', (WidgetTester tester) async { + State? containerThroughBoundary; + State? containerStoppedAtBoundary; + + final Key outerContainerKey = UniqueKey(); + final Key innerContainerKey = UniqueKey(); + + await tester.pumpWidget(MyStatefulContainer( + key: outerContainerKey, + child: LookupBoundary( + child: MyStatefulContainer( + key: innerContainerKey, + child: MyStatefulContainer( + child: Builder( + builder: (BuildContext context) { + containerThroughBoundary = context.findRootAncestorStateOfType(); + containerStoppedAtBoundary = LookupBoundary.findRootAncestorStateOfType(context); + return const SizedBox.expand(); + }, + ), + ), + ), + ), + )); + + expect(containerThroughBoundary, equals(tester.state(find.byKey(outerContainerKey)))); + expect(containerStoppedAtBoundary, equals(tester.state(find.byKey(innerContainerKey)))); + }); + + testWidgets('works if nothing is found', (WidgetTester tester) async { + State? containerStoppedAtBoundary; + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + containerStoppedAtBoundary = LookupBoundary.findRootAncestorStateOfType(context); + return const SizedBox.expand(); + }, + )); + + expect(containerStoppedAtBoundary, isNull); + }); + + testWidgets('does not establish a dependency', (WidgetTester tester) async { + State? containerThroughBoundary; + State? containerStoppedAtBoundary; + State? containerStoppedAtBoundaryUnfulfilled; + + final Key innerContainerKey = UniqueKey(); + final Key globalKey = GlobalKey(); + + final Widget widgetTree = LookupBoundary( + child: MyStatefulContainer( + key: innerContainerKey, + child: DidChangeDependencySpy( + key: globalKey, + onDidChangeDependencies: (BuildContext context) { + containerThroughBoundary = context.findRootAncestorStateOfType(); + containerStoppedAtBoundary = LookupBoundary.findRootAncestorStateOfType(context); + containerStoppedAtBoundaryUnfulfilled = LookupBoundary.findRootAncestorStateOfType(context); + }, + ), + ), + ); + + await tester.pumpWidget(widgetTree); + + expect(containerThroughBoundary, equals(tester.state(find.byKey(innerContainerKey)))); + expect(containerStoppedAtBoundary, equals(tester.state(find.byKey(innerContainerKey)))); + expect(containerStoppedAtBoundaryUnfulfilled, isNull); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + await tester.pumpWidget( + SizedBox( // Changes tree structure, triggers global key move of DidChangeDependencySpy. + child: widgetTree, + ), + ); + + // Tree restructuring above would have called didChangeDependencies if dependency had been established. + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + }); + }); + + group('LookupBoundary.findAncestorRenderObjectOfType', () { + testWidgets('respects boundary', (WidgetTester tester) async { + RenderPadding? paddingThroughBoundary; + RenderPadding? passingStoppedAtBoundary; + + final Key paddingKey = UniqueKey(); + + await tester.pumpWidget(Padding( + padding: EdgeInsets.zero, + key: paddingKey, + child: LookupBoundary( + child: Builder( + builder: (BuildContext context) { + paddingThroughBoundary = context.findAncestorRenderObjectOfType(); + passingStoppedAtBoundary = LookupBoundary.findAncestorRenderObjectOfType(context); + return const SizedBox.expand(); + }, + ), + ), + )); + + expect(paddingThroughBoundary, equals(tester.renderObject(find.byKey(paddingKey)))); + expect(passingStoppedAtBoundary, isNull); + }); + + testWidgets('finds right widget before boundary', (WidgetTester tester) async { + RenderPadding? paddingThroughBoundary; + RenderPadding? paddingStoppedAtBoundary; + + final Key outerPaddingKey = UniqueKey(); + final Key innerPaddingKey = UniqueKey(); + + await tester.pumpWidget(Padding( + padding: EdgeInsets.zero, + key: outerPaddingKey, + child: LookupBoundary( + child: Padding( + padding: EdgeInsets.zero, + child: Padding( + padding: EdgeInsets.zero, + key: innerPaddingKey, + child: Builder( + builder: (BuildContext context) { + paddingThroughBoundary = context.findAncestorRenderObjectOfType(); + paddingStoppedAtBoundary = LookupBoundary.findAncestorRenderObjectOfType(context); + return const SizedBox.expand(); + }, + ), + ), + ), + ), + )); + + expect(paddingThroughBoundary, equals(tester.renderObject(find.byKey(innerPaddingKey)))); + expect(paddingStoppedAtBoundary, equals(tester.renderObject(find.byKey(innerPaddingKey)))); + }); + + testWidgets('works if nothing is found', (WidgetTester tester) async { + RenderPadding? paddingStoppedAtBoundary; + + await tester.pumpWidget(Builder( + builder: (BuildContext context) { + paddingStoppedAtBoundary = LookupBoundary.findAncestorRenderObjectOfType(context); + return const SizedBox.expand(); + }, + )); + + expect(paddingStoppedAtBoundary, isNull); + }); + + testWidgets('does not establish a dependency', (WidgetTester tester) async { + RenderPadding? paddingThroughBoundary; + RenderPadding? paddingStoppedAtBoundary; + RenderWrap? wrapStoppedAtBoundaryUnfulfilled; + + final Key innerPaddingKey = UniqueKey(); + final Key globalKey = GlobalKey(); + + final Widget widgetTree = LookupBoundary( + child: Padding( + padding: EdgeInsets.zero, + key: innerPaddingKey, + child: DidChangeDependencySpy( + key: globalKey, + onDidChangeDependencies: (BuildContext context) { + paddingThroughBoundary = context.findAncestorRenderObjectOfType(); + paddingStoppedAtBoundary = LookupBoundary.findAncestorRenderObjectOfType(context); + wrapStoppedAtBoundaryUnfulfilled = LookupBoundary.findAncestorRenderObjectOfType(context); + }, + ), + ), + ); + + await tester.pumpWidget(widgetTree); + + expect(paddingThroughBoundary, equals(tester.renderObject(find.byKey(innerPaddingKey)))); + expect(paddingStoppedAtBoundary, equals(tester.renderObject(find.byKey(innerPaddingKey)))); + expect(wrapStoppedAtBoundaryUnfulfilled, isNull); + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + + await tester.pumpWidget( + SizedBox( // Changes tree structure, triggers global key move of DidChangeDependencySpy. + child: widgetTree, + ), + ); + + // Tree restructuring above would have called didChangeDependencies if dependency had been established. + expect(tester.state<_DidChangeDependencySpyState>(find.byType(DidChangeDependencySpy)).didChangeDependenciesCount, 1); + }); + }); + + group('LookupBoundary.visitAncestorElements', () { + testWidgets('respects boundary', (WidgetTester tester) async { + final List throughBoundary = []; + final List stoppedAtBoundary = []; + final List stoppedAtBoundaryTerminatedEarly = []; + + final Key level0 = UniqueKey(); + final Key level1 = UniqueKey(); + final Key level2 = UniqueKey(); + final Key level3 = UniqueKey(); + final Key level4 = UniqueKey(); + + await tester.pumpWidget(Container( + key: level0, + child: Container( + key: level1, + child: LookupBoundary( + key: level2, + child: Container( + key: level3, + child: Container( + key: level4, + child: Builder( + builder: (BuildContext context) { + context.visitAncestorElements((Element element) { + throughBoundary.add(element); + return element.widget.key != level0; + }); + LookupBoundary.visitAncestorElements(context, (Element element) { + stoppedAtBoundary.add(element); + return element.widget.key != level0; + }); + LookupBoundary.visitAncestorElements(context, (Element element) { + stoppedAtBoundaryTerminatedEarly.add(element); + return element.widget.key != level3; + }); + return const SizedBox(); + } + ) + ) + ) + ) + ), + )); + + expect(throughBoundary, [ + tester.element(find.byKey(level4)), + tester.element(find.byKey(level3)), + tester.element(find.byKey(level2)), + tester.element(find.byKey(level1)), + tester.element(find.byKey(level0)), + ]); + + expect(stoppedAtBoundary, [ + tester.element(find.byKey(level4)), + tester.element(find.byKey(level3)), + tester.element(find.byKey(level2)), + ]); + + expect(stoppedAtBoundaryTerminatedEarly, [ + tester.element(find.byKey(level4)), + tester.element(find.byKey(level3)), + ]); + }); + }); + + group('LookupBoundary.visitChildElements', () { + testWidgets('respects boundary', (WidgetTester tester) async { + final Key root = UniqueKey(); + final Key child1 = UniqueKey(); + final Key child2 = UniqueKey(); + final Key child3 = UniqueKey(); + + await tester.pumpWidget(Column( + key: root, + children: [ + LookupBoundary( + key: child1, + child: Container(), + ), + Container( + key: child2, + child: LookupBoundary( + child: Container(), + ), + ), + Container( + key: child3, + ), + ], + )); + + final List throughBoundary = []; + final List stoppedAtBoundary = []; + + final BuildContext context = tester.element(find.byKey(root)); + + context.visitChildElements((Element element) { + throughBoundary.add(element); + }); + LookupBoundary.visitChildElements(context, (Element element) { + stoppedAtBoundary.add(element); + }); + + expect(throughBoundary, [ + tester.element(find.byKey(child1)), + tester.element(find.byKey(child2)), + tester.element(find.byKey(child3)), + ]); + + expect(stoppedAtBoundary, [ + tester.element(find.byKey(child2)), + tester.element(find.byKey(child3)), + ]); + + }); + }); +} + +class MyStatefulContainer extends StatefulWidget { + const MyStatefulContainer({super.key, required this.child}); + + final Widget child; + + @override + State createState() => MyStatefulContainerState(); +} + +class MyStatefulContainerState extends State { + @override + Widget build(BuildContext context) { + return widget.child; + } +} + +class MyOtherStatefulContainerState extends State { + @override + Widget build(BuildContext context) { + return widget.child; + } +} + +class MyInheritedWidget extends InheritedWidget { + const MyInheritedWidget({super.key, required this.value, required super.child}); + + final int value; + + @override + bool updateShouldNotify(MyInheritedWidget oldWidget) => oldWidget.value != value; +} + +class DidChangeDependencySpy extends StatefulWidget { + const DidChangeDependencySpy({super.key, required this.onDidChangeDependencies}); + + final OnDidChangeDependencies onDidChangeDependencies; + + @override + State createState() => _DidChangeDependencySpyState(); +} + +class _DidChangeDependencySpyState extends State { + int didChangeDependenciesCount = 0; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + didChangeDependenciesCount += 1; + widget.onDidChangeDependencies(context); + } + + @override + Widget build(BuildContext context) { + return Container(); + } +} + +typedef OnDidChangeDependencies = void Function(BuildContext context);