Unverified Commit 297f094c authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

LookupBoundary (#116429)

* LookupBoundary simplified

* tests

* doc and impl complete

* doc fixes

* add more tests

* review

* empty
parent cc256c3e
...@@ -2020,6 +2020,13 @@ class _InactiveElements { ...@@ -2020,6 +2020,13 @@ class _InactiveElements {
/// this callback. /// this callback.
typedef ElementVisitor = void Function(Element element); 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. /// A handle to the location of a widget in the widget tree.
/// ///
/// This class presents a set of methods that can be used from /// This class presents a set of methods that can be used from
...@@ -2221,7 +2228,7 @@ abstract class BuildContext { ...@@ -2221,7 +2228,7 @@ abstract class BuildContext {
/// ///
/// All of the qualifications about when [dependOnInheritedWidgetOfExactType] can /// All of the qualifications about when [dependOnInheritedWidgetOfExactType] can
/// be called apply to this method as well. /// 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 /// 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 /// concrete [InheritedWidget] subclass, and registers this build context with
...@@ -2229,6 +2236,7 @@ abstract class BuildContext { ...@@ -2229,6 +2236,7 @@ abstract class BuildContext {
/// type is introduced, or the widget goes away), this build context is /// type is introduced, or the widget goes away), this build context is
/// rebuilt so that it can obtain new values from that widget. /// 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. /// This is typically called implicitly from `of()` static methods, e.g.
/// [Theme.of]. /// [Theme.of].
/// ///
...@@ -2262,6 +2270,7 @@ abstract class BuildContext { ...@@ -2262,6 +2270,7 @@ abstract class BuildContext {
/// [InheritedWidget] subclasses that supports partial updates, like /// [InheritedWidget] subclasses that supports partial updates, like
/// [InheritedModel]. It specifies what "aspect" of the inherited /// [InheritedModel]. It specifies what "aspect" of the inherited
/// widget this context depends on. /// widget this context depends on.
/// {@endtemplate}
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect }); T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({ Object? aspect });
/// Obtains the element corresponding to the nearest widget of the given type `T`, /// Obtains the element corresponding to the nearest widget of the given type `T`,
...@@ -2269,6 +2278,7 @@ abstract class BuildContext { ...@@ -2269,6 +2278,7 @@ abstract class BuildContext {
/// ///
/// Returns null if no such element is found. /// Returns null if no such element is found.
/// ///
/// {@template flutter.widgets.BuildContext.getElementForInheritedWidgetOfExactType}
/// Calling this method is O(1) with a small constant factor. /// Calling this method is O(1) with a small constant factor.
/// ///
/// This method does not establish a relationship with the target in the way /// This method does not establish a relationship with the target in the way
...@@ -2280,11 +2290,13 @@ abstract class BuildContext { ...@@ -2280,11 +2290,13 @@ abstract class BuildContext {
/// [dependOnInheritedWidgetOfExactType] in [State.didChangeDependencies]. It is /// [dependOnInheritedWidgetOfExactType] in [State.didChangeDependencies]. It is
/// safe to use this method from [State.deactivate], which is called whenever /// safe to use this method from [State.deactivate], which is called whenever
/// the widget is removed from the tree. /// the widget is removed from the tree.
/// {@endtemplate}
InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>(); InheritedElement? getElementForInheritedWidgetOfExactType<T extends InheritedWidget>();
/// Returns the nearest ancestor widget of the given type `T`, which must be the /// Returns the nearest ancestor widget of the given type `T`, which must be the
/// type of a concrete [Widget] subclass. /// type of a concrete [Widget] subclass.
/// ///
/// {@template flutter.widgets.BuildContext.findAncestorWidgetOfExactType}
/// In general, [dependOnInheritedWidgetOfExactType] is more useful, since /// In general, [dependOnInheritedWidgetOfExactType] is more useful, since
/// inherited widgets will trigger consumers to rebuild when they change. This /// inherited widgets will trigger consumers to rebuild when they change. This
/// method is appropriate when used in interaction event handlers (e.g. /// method is appropriate when used in interaction event handlers (e.g.
...@@ -2306,11 +2318,13 @@ abstract class BuildContext { ...@@ -2306,11 +2318,13 @@ abstract class BuildContext {
/// ///
/// Returns null if a widget of the requested type does not appear in the /// Returns null if a widget of the requested type does not appear in the
/// ancestors of this context. /// ancestors of this context.
/// {@endtemplate}
T? findAncestorWidgetOfExactType<T extends Widget>(); T? findAncestorWidgetOfExactType<T extends Widget>();
/// Returns the [State] object of the nearest ancestor [StatefulWidget] widget /// Returns the [State] object of the nearest ancestor [StatefulWidget] widget
/// that is an instance of the given type `T`. /// 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 /// 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. /// not be rebuilt if the value that would be returned by this method changes.
/// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such /// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such
...@@ -2332,6 +2346,7 @@ abstract class BuildContext { ...@@ -2332,6 +2346,7 @@ abstract class BuildContext {
/// because the widget tree is no longer stable at that time. To refer to /// 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 /// an ancestor from one of those methods, save a reference to the ancestor
/// by calling [findAncestorStateOfType] in [State.didChangeDependencies]. /// by calling [findAncestorStateOfType] in [State.didChangeDependencies].
/// {@endtemplate}
/// ///
/// {@tool snippet} /// {@tool snippet}
/// ///
...@@ -2344,17 +2359,20 @@ abstract class BuildContext { ...@@ -2344,17 +2359,20 @@ abstract class BuildContext {
/// Returns the [State] object of the furthest ancestor [StatefulWidget] widget /// Returns the [State] object of the furthest ancestor [StatefulWidget] widget
/// that is an instance of the given type `T`. /// that is an instance of the given type `T`.
/// ///
/// {@template flutter.widgets.BuildContext.findRootAncestorStateOfType}
/// Functions the same way as [findAncestorStateOfType] but keeps visiting subsequent /// Functions the same way as [findAncestorStateOfType] but keeps visiting subsequent
/// ancestors until there are none of the type instance of `T` remaining. /// ancestors until there are none of the type instance of `T` remaining.
/// Then returns the last one found. /// Then returns the last one found.
/// ///
/// This operation is O(N) as well though N is the entire widget tree rather than /// This operation is O(N) as well though N is the entire widget tree rather than
/// a subtree. /// a subtree.
/// {@endtemplate}
T? findRootAncestorStateOfType<T extends State>(); T? findRootAncestorStateOfType<T extends State>();
/// Returns the [RenderObject] object of the nearest ancestor [RenderObjectWidget] widget /// Returns the [RenderObject] object of the nearest ancestor [RenderObjectWidget] widget
/// that is an instance of the given type `T`. /// 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 /// 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. /// not be rebuilt if the value that would be returned by this method changes.
/// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such /// In general, [dependOnInheritedWidgetOfExactType] is more appropriate for such
...@@ -2371,13 +2389,16 @@ abstract class BuildContext { ...@@ -2371,13 +2389,16 @@ abstract class BuildContext {
/// because the widget tree is no longer stable at that time. To refer to /// 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 /// an ancestor from one of those methods, save a reference to the ancestor
/// by calling [findAncestorRenderObjectOfType] in [State.didChangeDependencies]. /// by calling [findAncestorRenderObjectOfType] in [State.didChangeDependencies].
/// {@endtemplate}
T? findAncestorRenderObjectOfType<T extends RenderObject>(); T? findAncestorRenderObjectOfType<T extends RenderObject>();
/// Walks the ancestor chain, starting with the parent of this build context's /// 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 /// widget, invoking the argument for each ancestor.
/// reference to the ancestor widget's corresponding [Element] object. The ///
/// walk stops when it reaches the root widget or when the callback returns /// {@template flutter.widgets.BuildContext.visitAncestorElements}
/// false. The callback must not return null. /// 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. /// This is useful for inspecting the widget tree.
/// ///
...@@ -2387,10 +2408,12 @@ abstract class BuildContext { ...@@ -2387,10 +2408,12 @@ abstract class BuildContext {
/// because the element tree is no longer stable at that time. To refer to /// 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 /// an ancestor from one of those methods, save a reference to the ancestor
/// by calling [visitAncestorElements] in [State.didChangeDependencies]. /// by calling [visitAncestorElements] in [State.didChangeDependencies].
void visitAncestorElements(bool Function(Element element) visitor); /// {@endtemplate}
void visitAncestorElements(ConditionalElementVisitor visitor);
/// Walks the children of this widget. /// Walks the children of this widget.
/// ///
/// {@template flutter.widgets.BuildContext.visitChildElements}
/// This is useful for applying changes to children after they are built /// This is useful for applying changes to children after they are built
/// without waiting for the next frame, especially if the children are known, /// 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 /// and especially if there is exactly one child (as is always the case for
...@@ -2408,6 +2431,7 @@ abstract class BuildContext { ...@@ -2408,6 +2431,7 @@ abstract class BuildContext {
/// significantly cheaper to use an [InheritedWidget] and have the descendants /// significantly cheaper to use an [InheritedWidget] and have the descendants
/// pull data down, than it is to use [visitChildElements] recursively to push /// pull data down, than it is to use [visitChildElements] recursively to push
/// data down to them. /// data down to them.
/// {@endtemplate}
void visitChildElements(ElementVisitor visitor); void visitChildElements(ElementVisitor visitor);
/// Start bubbling this notification at the given build context. /// Start bubbling this notification at the given build context.
...@@ -4452,7 +4476,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext { ...@@ -4452,7 +4476,7 @@ abstract class Element extends DiagnosticableTree implements BuildContext {
} }
@override @override
void visitAncestorElements(bool Function(Element element) visitor) { void visitAncestorElements(ConditionalElementVisitor visitor) {
assert(_debugCheckStateIsActiveForAncestorLookup()); assert(_debugCheckStateIsActiveForAncestorLookup());
Element? ancestor = _parent; Element? ancestor = _parent;
while (ancestor != null && visitor(ancestor)) { while (ancestor != null && visitor(ancestor)) {
......
// 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;
}
...@@ -72,6 +72,7 @@ export 'src/widgets/keyboard_listener.dart'; ...@@ -72,6 +72,7 @@ export 'src/widgets/keyboard_listener.dart';
export 'src/widgets/layout_builder.dart'; export 'src/widgets/layout_builder.dart';
export 'src/widgets/list_wheel_scroll_view.dart'; export 'src/widgets/list_wheel_scroll_view.dart';
export 'src/widgets/localizations.dart'; export 'src/widgets/localizations.dart';
export 'src/widgets/lookup_boundary.dart';
export 'src/widgets/magnifier.dart'; export 'src/widgets/magnifier.dart';
export 'src/widgets/media_query.dart'; export 'src/widgets/media_query.dart';
export 'src/widgets/modal_barrier.dart'; export 'src/widgets/modal_barrier.dart';
......
// 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);
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment