LookupBoundary (#116429)

* LookupBoundary simplified

* tests

* doc and impl complete

* doc fixes

* add more tests

* review

* empty
diff --git a/packages/flutter/lib/src/widgets/framework.dart b/packages/flutter/lib/src/widgets/framework.dart
index b37b90e..a1ef69e 100644
--- a/packages/flutter/lib/src/widgets/framework.dart
+++ b/packages/flutter/lib/src/widgets/framework.dart
@@ -2020,6 +2020,13 @@
 /// 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 @@
   ///
   /// 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 @@
   /// 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 @@
   /// [InheritedWidget] subclasses that supports partial updates, like
   /// [InheritedModel]. It specifies what "aspect" of the inherited
   /// widget this context depends on.
+  /// {@endtemplate}
   T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
 
   /// Obtains the element corresponding to the nearest widget of the given type `T`,
@@ -2269,6 +2278,7 @@
   ///
   /// 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 @@
   /// [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<T extends InheritedWidget>();
 
   /// 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 @@
   ///
   /// Returns null if a widget of the requested type does not appear in the
   /// ancestors of this context.
+  /// {@endtemplate}
   T? findAncestorWidgetOfExactType<T extends Widget>();
 
   /// 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 @@
   /// 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 @@
   /// 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<T extends State>();
 
   /// 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 @@
   /// 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<T extends RenderObject>();
 
   /// 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 @@
   /// 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 @@
   /// 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 @@
   }
 
   @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 0000000..dc903da
--- /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<MyWidget>(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<T extends InheritedWidget>(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<LookupBoundary>();
+    final InheritedElement? candidate = getElementForInheritedWidgetOfExactType<T>(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<T extends InheritedWidget>(BuildContext context) {
+    final InheritedElement? candidate = context.getElementForInheritedWidgetOfExactType<T>();
+    if (candidate == null) {
+      return null;
+    }
+    final Element? boundary = context.getElementForInheritedWidgetOfExactType<LookupBoundary>();
+    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<T extends Widget>(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<T extends State>(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<T extends State>(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<T extends RenderObject>(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 03fe927..e2c5e39 100644
--- a/packages/flutter/lib/widgets.dart
+++ b/packages/flutter/lib/widgets.dart
@@ -72,6 +72,7 @@
 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 0000000..c5b8bfc
--- /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<MyInheritedWidget>();
+              containerStoppedAtBoundary = LookupBoundary.dependOnInheritedWidgetOfExactType<MyInheritedWidget>(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<MyInheritedWidget>(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<MyInheritedWidget>();
+                containerStoppedAtBoundary = LookupBoundary.dependOnInheritedWidgetOfExactType<MyInheritedWidget>(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<MyInheritedWidget>(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<MyInheritedWidget>(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<MyInheritedWidget>(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<MyInheritedWidget>();
+              containerStoppedAtBoundary = LookupBoundary.getElementForInheritedWidgetOfExactType<MyInheritedWidget>(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<MyInheritedWidget>(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<MyInheritedWidget>();
+                containerStoppedAtBoundary = LookupBoundary.getElementForInheritedWidgetOfExactType<MyInheritedWidget>(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<MyInheritedWidget>(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<MyInheritedWidget>(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<MyInheritedWidget>(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<Container>();
+              containerStoppedAtBoundary = LookupBoundary.findAncestorWidgetOfExactType<Container>(context);
+              boundaryThroughBoundary = context.findAncestorWidgetOfExactType<LookupBoundary>();
+              boundaryStoppedAtBoundary = LookupBoundary.findAncestorWidgetOfExactType<LookupBoundary>(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<Container>();
+                  containerStoppedAtBoundary = LookupBoundary.findAncestorWidgetOfExactType<Container>(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<Container>(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<Container>();
+              containerStoppedAtBoundary = LookupBoundary.findAncestorWidgetOfExactType<Container>(context);
+              containerStoppedAtBoundaryUnfulfilled = LookupBoundary.findAncestorWidgetOfExactType<Material>(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<MyStatefulContainerState>();
+              containerStoppedAtBoundary = LookupBoundary.findAncestorStateOfType<MyStatefulContainerState>(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<MyStatefulContainerState>();
+                  containerStoppedAtBoundary = LookupBoundary.findAncestorStateOfType<MyStatefulContainerState>(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<MyStatefulContainerState>(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<MyStatefulContainerState>();
+              containerStoppedAtBoundary = LookupBoundary.findAncestorStateOfType<MyStatefulContainerState>(context);
+              containerStoppedAtBoundaryUnfulfilled = LookupBoundary.findAncestorStateOfType<MyOtherStatefulContainerState>(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<MyStatefulContainerState>();
+              containerStoppedAtBoundary = LookupBoundary.findRootAncestorStateOfType<MyStatefulContainerState>(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<MyStatefulContainerState>();
+                  containerStoppedAtBoundary = LookupBoundary.findRootAncestorStateOfType<MyStatefulContainerState>(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<MyStatefulContainerState>(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<MyStatefulContainerState>();
+              containerStoppedAtBoundary = LookupBoundary.findRootAncestorStateOfType<MyStatefulContainerState>(context);
+              containerStoppedAtBoundaryUnfulfilled = LookupBoundary.findRootAncestorStateOfType<MyOtherStatefulContainerState>(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<RenderPadding>();
+              passingStoppedAtBoundary = LookupBoundary.findAncestorRenderObjectOfType<RenderPadding>(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<RenderPadding>();
+                  paddingStoppedAtBoundary = LookupBoundary.findAncestorRenderObjectOfType<RenderPadding>(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<RenderPadding>(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<RenderPadding>();
+              paddingStoppedAtBoundary = LookupBoundary.findAncestorRenderObjectOfType<RenderPadding>(context);
+              wrapStoppedAtBoundaryUnfulfilled = LookupBoundary.findAncestorRenderObjectOfType<RenderWrap>(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<Element> throughBoundary = <Element>[];
+      final List<Element> stoppedAtBoundary = <Element>[];
+      final List<Element> stoppedAtBoundaryTerminatedEarly = <Element>[];
+
+      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, <Element>[
+        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, <Element>[
+        tester.element(find.byKey(level4)),
+        tester.element(find.byKey(level3)),
+        tester.element(find.byKey(level2)),
+      ]);
+
+      expect(stoppedAtBoundaryTerminatedEarly, <Element>[
+        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: <Widget>[
+          LookupBoundary(
+            key: child1,
+            child: Container(),
+          ),
+          Container(
+            key: child2,
+            child: LookupBoundary(
+              child: Container(),
+            ),
+          ),
+          Container(
+            key: child3,
+          ),
+        ],
+      ));
+
+      final List<Element> throughBoundary = <Element>[];
+      final List<Element> stoppedAtBoundary = <Element>[];
+
+      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, <Element>[
+        tester.element(find.byKey(child1)),
+        tester.element(find.byKey(child2)),
+        tester.element(find.byKey(child3)),
+      ]);
+
+      expect(stoppedAtBoundary, <Element>[
+        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<MyStatefulContainer> createState() => MyStatefulContainerState();
+}
+
+class MyStatefulContainerState extends State<MyStatefulContainer> {
+  @override
+  Widget build(BuildContext context) {
+    return widget.child;
+  }
+}
+
+class MyOtherStatefulContainerState extends State<MyStatefulContainer> {
+  @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<DidChangeDependencySpy> createState() => _DidChangeDependencySpyState();
+}
+
+class _DidChangeDependencySpyState extends State<DidChangeDependencySpy> {
+  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);