blob: 85b1a71a677f5064af3065e9c7aaa01714087b8b [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
class _ManyRelayoutBoundaries extends StatelessWidget {
const _ManyRelayoutBoundaries({
required this.levels,
required this.child,
});
final Widget child;
final int levels;
@override
Widget build(BuildContext context) {
final Widget result = levels <= 1
? child
: _ManyRelayoutBoundaries(levels: levels - 1, child: child);
return SizedBox.square(dimension: 50, child: result);
}
}
void rebuildLayoutBuilderSubtree(RenderBox descendant) {
assert(descendant is! RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox>);
AbstractNode? node = descendant.parent;
while (node != null) {
if (node is! RenderConstrainedLayoutBuilder<BoxConstraints, RenderBox>) {
node = node.parent;
} else {
node.markNeedsBuild();
return;
}
}
assert(false);
}
void verifyTreeIsClean() {
final RenderObject renderObject = RendererBinding.instance.renderView;
bool hasDirtyNode = renderObject.debugNeedsLayout;
void visitor(RenderObject renderObject) {
expect(renderObject.debugNeedsLayout, false, reason: '$renderObject is dirty');
hasDirtyNode = hasDirtyNode || renderObject.debugNeedsLayout;
if (!hasDirtyNode) {
renderObject.visitChildren(visitor);
}
}
visitor(renderObject);
}
void verifyOverlayChildReadyForLayout(GlobalKey overlayWidgetKey) {
final RenderBox layoutSurrogate = overlayWidgetKey.currentContext!.findRenderObject()! as RenderBox;
assert(
layoutSurrogate.runtimeType.toString() == '_RenderLayoutSurrogateProxyBox',
layoutSurrogate.runtimeType,
);
if (layoutSurrogate.debugNeedsLayout) {
assert(layoutSurrogate.debugDoingThisLayout);
}
expect(!layoutSurrogate.debugNeedsLayout || layoutSurrogate.debugDoingThisLayout, true);
}
List<RenderObject> _ancestorRenderTheaters(RenderObject child) {
final List<RenderObject> results = <RenderObject>[];
RenderObject? node = child;
while (node != null) {
if (node.runtimeType.toString() == '_RenderTheater') {
results.add(node);
}
final AbstractNode? parent = node.parent;
node = parent is RenderObject? parent : null;
}
return results;
}
void main() {
final OverlayPortalController controller1 = OverlayPortalController(debugLabel: 'controller1');
final OverlayPortalController controller2 = OverlayPortalController(debugLabel: 'controller2');
final OverlayPortalController controller3 = OverlayPortalController(debugLabel: 'controller3');
final OverlayPortalController controller4 = OverlayPortalController(debugLabel: 'controller4');
setUp(() {
controller1.show();
controller2.show();
controller3.show();
controller4.show();
_PaintOrder.paintOrder.clear();
});
testWidgets('The overlay child sees the right inherited widgets', (WidgetTester tester) async {
int buildCount = 0;
TextDirection? directionSeenByOverlayChild;
TextDirection textDirection = TextDirection.rtl;
late StateSetter setState;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return Directionality(
textDirection: textDirection,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) {
buildCount += 1;
directionSeenByOverlayChild = Directionality.maybeOf(context);
return const SizedBox();
},
child: const SizedBox(),
),
);
}
);
},
),
],
),
),
);
expect(buildCount, 1);
expect(directionSeenByOverlayChild, textDirection);
setState(() {
textDirection = TextDirection.ltr;
});
await tester.pump();
expect(buildCount, 2);
expect(directionSeenByOverlayChild, textDirection);
});
testWidgets('Safe to deactivate and re-activate OverlayPortal', (WidgetTester tester) async {
final Widget widget = Directionality(
key: GlobalKey(debugLabel: 'key'),
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
);
},
),
],
),
);
await tester.pumpWidget(widget);
await tester.pumpWidget(SizedBox(child: widget));
});
testWidgets('Throws when the same controller is attached to multiple OverlayPortal', (WidgetTester tester) async {
final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller');
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return Column(
children: <Widget>[
OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
),
OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
),
],
);
},
),
],
),
);
await tester.pumpWidget(widget);
expect(
tester.takeException().toString(),
stringContainsInOrder(<String>['Failed to attach' ,'It is already attached to']),
);
});
testWidgets('show/hide works', (WidgetTester tester) async {
final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller');
const Widget target = SizedBox();
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) => target,
);
},
),
],
),
);
await tester.pumpWidget(widget);
expect(find.byWidget(target), findsNothing);
await tester.pump();
expect(find.byWidget(target), findsNothing);
controller.show();
await tester.pump();
expect(find.byWidget(target), findsOneWidget);
controller.hide();
await tester.pump();
expect(find.byWidget(target), findsNothing);
controller.show();
await tester.pump();
expect(find.byWidget(target), findsOneWidget);
});
testWidgets('overlayChildBuilder is not evaluated until show is called', (WidgetTester tester) async {
final OverlayPortalController controller = OverlayPortalController(debugLabel: 'local controller');
final Widget widget = Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return OverlayPortal(
controller: controller,
overlayChildBuilder: (BuildContext context) => throw StateError('Unreachable'),
child: const SizedBox(),
);
},
),
],
),
);
await tester.pumpWidget(widget);
expect(tester.takeException(), isNull);
});
testWidgets('overlay child can use Positioned', (WidgetTester tester) async {
double dimensions = 30;
late StateSetter setState;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return Positioned(
width: dimensions,
height: dimensions,
child: const Placeholder(),
);
},
child: const SizedBox(),
);
}
);
},
),
],
),
),
);
expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero) ;
expect(tester.getSize(find.byType(Placeholder)), const Size(30, 30)) ;
setState(() {
dimensions = 50;
});
await tester.pump();
expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero) ;
expect(tester.getSize(find.byType(Placeholder)), const Size(50, 50)) ;
});
testWidgets('overlay child can be hit tested', (WidgetTester tester) async {
double offset = 0;
late StateSetter setState;
bool isHit = false;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
setState = setter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return Positioned(
left: offset,
top: offset,
width: 1.0,
height: 1.0,
child: GestureDetector(onTap: () { isHit = true; }),
);
},
child: const SizedBox(),
);
}
);
},
),
],
),
),
);
assert(!isHit);
await tester.tapAt(const Offset(0.5, 0.5));
expect(isHit, true);
isHit = false;
setState(() {
offset = 50;
});
await tester.pump();
assert(!isHit);
await tester.tapAt(const Offset(0.5, 0.5));
expect(isHit, false);
isHit = false;
await tester.tapAt(const Offset(50.5, 50.5));
expect(isHit, true);
});
testWidgets('works in a LayoutBuilder', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
);
}
);
},
),
],
),
),
);
expect(tester.takeException(), isNull);
});
testWidgets('works in a LayoutBuilder 2', (WidgetTester tester) async {
late StateSetter setState;
bool shouldShowChild = false;
Widget layoutBuilder(BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
);
}
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter setter) {
setState = setter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: shouldShowChild ? LayoutBuilder(builder: layoutBuilder) : null,
);
}),
],
),
),
);
expect(tester.takeException(), isNull);
setState(() { shouldShowChild = true; });
await tester.pump();
expect(tester.takeException(), isNull);
});
testWidgets('throws when no Overlay', (WidgetTester tester) async {
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: SizedBox.square(
dimension: 50,
child: OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => const SizedBox(),
child: const SizedBox(),
),
),
),
);
expect(
tester.takeException().toString(),
startsWith(
'No Overlay widget found.\n'
'OverlayPortal widgets require an Overlay widget ancestor.\n'
'An overlay lets widgets float on top of other widget children.\n'
'To introduce an Overlay widget, you can either directly include one, or use a widget '
'that contains an Overlay itself, such as a Navigator, WidgetApp, MaterialApp, or CupertinoApp.\n'
'The specific widget that could not find a Overlay ancestor was:\n'
),
);
});
testWidgets('widget is laid out before overlay child', (WidgetTester tester) async {
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
int layoutCount = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(
builder: (BuildContext context) {
return _ManyRelayoutBoundaries(levels: 50, child: Builder(builder: (BuildContext context) {
return OverlayPortal(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
verifyOverlayChildReadyForLayout(widgetKey);
layoutCount += 1;
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}));
}
),
],
),
),
);
expect(layoutCount, 1);
// Make the widget's render object dirty and verifies in the LayoutBuilder's
// callback that the widget's render object is already laid out.
final RenderObject renderChild1 = widgetKey.currentContext!.findRenderObject()!;
renderChild1.markNeedsLayout();
// Dirty both render subtree branches.
childBox.markNeedsLayout();
rebuildLayoutBuilderSubtree(overlayChildBox);
// Make sure childBox's depth is greater than that of the overlay
// child, and childBox's parent isn't dirty (childBox is a dirty relayout
// boundary).
expect(widgetKey.currentContext!.findRenderObject()!.depth, lessThan(overlayChildBox.depth));
await tester.pump();
expect(layoutCount, 2);
verifyTreeIsClean();
});
testWidgets('adding/removing overlay child does not redirty overlay more than once', (WidgetTester tester) async {
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget');
final GlobalKey overlayKey = GlobalKey(debugLabel: 'overlay');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final _RenderLayoutCounter overlayLayoutCounter = _RenderLayoutCounter();
int layoutCount = 0;
controller1.hide();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
// Overlay.performLayout will call layoutCounter.layout.
OverlayEntry(builder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: overlayLayoutCounter)),
OverlayEntry(
builder: (BuildContext context) {
return _ManyRelayoutBoundaries(levels: 50, child: Builder(builder: (BuildContext context) {
return OverlayPortal(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount += 1;
expect(tester.renderObject(find.byType(Overlay)).debugNeedsLayout, false);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}));
}
),
],
),
),
);
expect(layoutCount, 0);
expect(overlayLayoutCounter.layoutCount, 1);
verifyTreeIsClean();
// Add overlay child.
controller1.show();
await tester.pump();
expect(layoutCount, 1);
expect(overlayLayoutCounter.layoutCount, 1);
verifyTreeIsClean();
// Remove the overlay child.
controller1.hide();
await tester.pump();
expect(layoutCount, 1);
expect(overlayLayoutCounter.layoutCount, 1);
verifyTreeIsClean();
});
testWidgets('Change overlay constraints', (WidgetTester tester) async {
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget outer');
final GlobalKey overlayKey = GlobalKey(debugLabel: 'overlay');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final _RenderLayoutCounter overlayLayoutCounter = _RenderLayoutCounter();
int layoutCount = 0;
late StateSetter setState;
double dimension = 100;
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: SizedBox.square(
dimension: dimension,
child: Overlay(
key: overlayKey,
initialEntries: <OverlayEntry>[
// Overlay.performLayout calls layoutCounter.layout.
OverlayEntry(builder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: overlayLayoutCounter)),
OverlayEntry(
builder: (BuildContext outerEntryContext) {
return Center(
child: _ManyRelayoutBoundaries(
levels: 50,
child: Builder(builder: (BuildContext context) {
return OverlayPortal(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount += 1;
// Both overlays need to be clean at this point.
expect(
tester.renderObjectList(find.byType(Overlay)),
everyElement(wrapMatcher((RenderObject object) => !object.debugNeedsLayout || object.debugDoingThisLayout)),
);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
),
);
}
),
],
),
),
),
);
}
),
);
expect(layoutCount, 1);
expect(overlayLayoutCounter.layoutCount, 1);
expect(childBox.size, const Size.square(50));
expect(overlayChildBox.size, const Size.square(100));
verifyTreeIsClean();
// The incoming constraints changed.
setState(() {
dimension = 150;
});
await tester.pump();
expect(childBox.size, const Size.square(50));
expect(overlayChildBox.size, const Size.square(150));
expect(layoutCount, 2);
expect(overlayLayoutCounter.layoutCount, 2);
verifyTreeIsClean();
});
testWidgets('Can target the root overlay', (WidgetTester tester) async {
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget outer');
final GlobalKey rootOverlayKey = GlobalKey(debugLabel: 'root overlay');
final GlobalKey localOverlayKey = GlobalKey(debugLabel: 'local overlay');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final _RenderLayoutCounter overlayLayoutCounter = _RenderLayoutCounter();
int layoutCount = 0;
OverlayPortal Function({ Widget? child, required OverlayPortalController controller, Key? key, required WidgetBuilder overlayChildBuilder, }) constructorToUse = OverlayPortal.new;
late StateSetter setState;
// This tree has 3 nested Overlays.
await tester.pumpWidget(
StatefulBuilder(
builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return Center(
child: Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
key: rootOverlayKey,
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return Overlay(
key: localOverlayKey,
initialEntries: <OverlayEntry>[
// Overlay.performLayout calls layoutCounter.layout.
OverlayEntry(builder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: overlayLayoutCounter)),
OverlayEntry(builder: (BuildContext outerEntryContext) {
return Center(
child: Builder(builder: (BuildContext context) {
return constructorToUse(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount += 1;
// Both overlays need to be clean at this point.
expect(
tester.renderObjectList(find.byType(Overlay)),
everyElement(wrapMatcher((RenderObject object) => !object.debugNeedsLayout || object.debugDoingThisLayout)),
);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
);
}),
],
);
}),
],
);
}),
],
),
),
);
}
),
);
expect(layoutCount, 1);
expect(overlayLayoutCounter.layoutCount, 1);
expect(_ancestorRenderTheaters(overlayChildBox).length, 3);
verifyTreeIsClean();
// Now targets the root overlay.
setState(() { constructorToUse = OverlayPortal.targetsRootOverlay; });
await tester.pump();
expect(layoutCount, 2);
expect(overlayLayoutCounter.layoutCount, 1);
expect(_ancestorRenderTheaters(overlayChildBox).single, tester.renderObject(find.byKey(rootOverlayKey)));
verifyTreeIsClean();
});
group('GlobalKey Reparenting', () {
testWidgets('child is laid out before overlay child after OverlayEntry shuffle', (WidgetTester tester) async {
int layoutCount = 0;
final GlobalKey widgetKey = GlobalKey(debugLabel: 'widget');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final OverlayEntry overlayEntry1 = OverlayEntry(builder: (BuildContext context) {
return _ManyRelayoutBoundaries(
levels: 50,
child: Builder(builder: (BuildContext context) {
return OverlayPortal(
key: widgetKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount += 1;
verifyOverlayChildReadyForLayout(widgetKey);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
);
});
final OverlayEntry overlayEntry2 = OverlayEntry(builder: (BuildContext context) => const Placeholder());
final OverlayEntry overlayEntry3 = OverlayEntry(builder: (BuildContext context) => const Placeholder());
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[overlayEntry1, overlayEntry2, overlayEntry3],
),
),
);
expect(layoutCount, 1);
verifyTreeIsClean();
widgetKey.currentContext!.findRenderObject()!.markNeedsLayout();
childBox.markNeedsLayout();
rebuildLayoutBuilderSubtree(overlayChildBox);
// Make sure childBox's depth is greater than that of the overlay child.
expect(
widgetKey.currentContext!.findRenderObject()!.depth,
lessThan(overlayChildBox.depth),
);
tester.state<OverlayState>(find.byType(Overlay)).rearrange(<OverlayEntry>[overlayEntry3, overlayEntry2, overlayEntry1]);
await tester.pump();
expect(layoutCount, 2);
expect(widgetKey.currentContext!.findRenderObject()!.depth, lessThan(overlayChildBox.depth));
verifyTreeIsClean();
});
testWidgets('widget is laid out before overlay child after reparenting', (WidgetTester tester) async {
final GlobalKey targetGlobalKey = GlobalKey(debugLabel: 'target widget');
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
late StateSetter setState1, setState2;
bool targetMovedToOverlayEntry3 = false;
int layoutCount1 = 0;
int layoutCount2 = 0;
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return _ManyRelayoutBoundaries(
levels: 50,
child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return targetMovedToOverlayEntry3 ? const SizedBox() : OverlayPortal(
key: targetGlobalKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount1 += 1;
verifyOverlayChildReadyForLayout(targetGlobalKey);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
);
}),
OverlayEntry(builder: (BuildContext context) => const Placeholder()),
OverlayEntry(builder: (BuildContext context) {
return SizedBox(
child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return !targetMovedToOverlayEntry3 ? const SizedBox() : OverlayPortal(
key: targetGlobalKey,
controller: controller1,
overlayChildBuilder: (BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
layoutCount2 += 1;
verifyOverlayChildReadyForLayout(targetGlobalKey);
return WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
});
},
child: WidgetToRenderBoxAdapter(renderBox: childBox),
);
}),
);
}),
],
),
),
);
expect(layoutCount1, 1);
expect(layoutCount2, 0);
targetGlobalKey.currentContext!.findRenderObject()!.markNeedsLayout();
childBox.markNeedsLayout();
rebuildLayoutBuilderSubtree(overlayChildBox);
setState1(() {});
setState2(() {});
targetMovedToOverlayEntry3 = true;
await tester.pump();
expect(
targetGlobalKey.currentContext!.findRenderObject()!.depth,
lessThan(overlayChildBox.depth),
);
verifyTreeIsClean();
expect(layoutCount1, 1);
expect(layoutCount2, 1);
});
testWidgets('Swap child and overlayChild', (WidgetTester tester) async {
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
late StateSetter setState;
bool swapChildAndRemoteChild = false;
// WidgetToRenderBoxAdapter has its own builtin GlobalKey.
final Widget child1 = WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
final Widget child2 = WidgetToRenderBoxAdapter(renderBox: childBox);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return _ManyRelayoutBoundaries(
levels: 50,
child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => swapChildAndRemoteChild ? child1 : child2,
child: swapChildAndRemoteChild ? child2 : child1,
);
}),
);
}),
],
),
),
);
setState(() { swapChildAndRemoteChild = true; });
await tester.pump();
verifyTreeIsClean();
});
testWidgets('forgetChild', (WidgetTester tester) async {
final RenderBox childBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
late StateSetter setState1;
late StateSetter setState2;
bool takeChildren = false;
// WidgetToRenderBoxAdapter has its own builtin GlobalKey.
final Widget child1 = WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
final Widget child2 = WidgetToRenderBoxAdapter(renderBox: childBox);
controller1.hide();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => child2,
child: takeChildren ? child1 : null,
);
});
}),
OverlayEntry(builder: (BuildContext context) {
return _ManyRelayoutBoundaries(
levels: 50,
child: StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => child1,
child: takeChildren ? null : child2,
);
}),
);
}),
],
),
),
);
controller1.show();
controller2.hide();
setState2(() { takeChildren = true; });
setState1(() { });
await tester.pump();
verifyTreeIsClean();
});
testWidgets('Nested overlay children: swap inner and outer', (WidgetTester tester) async {
final GlobalKey outerKey = GlobalKey(debugLabel: 'Original Outer Widget');
final GlobalKey innerKey = GlobalKey(debugLabel: 'Original Inner Widget');
final RenderBox child1Box = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox child2Box = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox overlayChildBox = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
late StateSetter setState;
bool swapped = false;
// WidgetToRenderBoxAdapter has its own builtin GlobalKey.
final Widget child1 = WidgetToRenderBoxAdapter(renderBox: child1Box);
final Widget child2 = WidgetToRenderBoxAdapter(renderBox: child2Box);
final Widget child3 = WidgetToRenderBoxAdapter(renderBox: overlayChildBox);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return OverlayPortal(
key: swapped ? outerKey : innerKey,
controller: swapped ? controller2 : controller1,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
key: swapped ? innerKey : outerKey,
controller: swapped ? controller1 : controller2,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
controller: OverlayPortalController(),
overlayChildBuilder: (BuildContext context) => child3,
);
},
child: child2,
);
},
child: child1,
);
});
}),
],
),
),
);
setState(() { swapped = true; });
await tester.pump();
verifyTreeIsClean();
});
testWidgets('Paint order', (WidgetTester tester) async {
final GlobalKey outerKey = GlobalKey(debugLabel: 'Original Outer Widget');
final GlobalKey innerKey = GlobalKey(debugLabel: 'Original Inner Widget');
late StateSetter setState;
const Widget child1 = _PaintOrder();
const Widget child2 = _PaintOrder();
const Widget child3 = _PaintOrder();
const Widget child4 = _PaintOrder();
controller1.show();
controller2.show();
controller3.show();
controller4.show();
// Expected Order child1 -> innerKey -> child4.
Widget widget = Column(
children: <Widget>[
OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => child1,
),
OverlayPortal(
key: outerKey,
controller: controller2,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
key: innerKey,
controller: controller3,
overlayChildBuilder: (BuildContext context) => child3,
child: child2,
);
},
),
OverlayPortal(
controller: controller4,
overlayChildBuilder: (BuildContext context) => child4,
),
],
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return widget;
});
}),
],
),
),
);
expect(_PaintOrder.paintOrder,
<Widget>[
child1,
child2,
child3,
child4,
],
);
_PaintOrder.paintOrder.clear();
// Swap the nested OverlayPortal.
widget = Column(
children: <Widget>[
OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => child1,
),
OverlayPortal(
key: innerKey,
controller: controller3,
overlayChildBuilder: (BuildContext context) {
return OverlayPortal(
key: outerKey,
controller: controller2,
overlayChildBuilder: (BuildContext context) => child3,
child: child2,
);
},
),
OverlayPortal(
controller: controller4,
overlayChildBuilder: (BuildContext context) => child4,
),
],
);
setState(() {});
await tester.pump();
expect(_PaintOrder.paintOrder,
<Widget>[
child1,
child3,
child2,
child4,
],
);
});
group('Swapping', () {
StateSetter? setState1, setState2;
bool swapped = false;
void setState({ required bool newValue }) {
swapped = newValue;
setState1?.call(() {});
setState2?.call(() {});
}
tearDown(() {
swapped = false;
setState1 = null;
setState2 = null;
});
testWidgets('between OverlayEntry & overlayChild', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
// WidgetToRenderBoxAdapter is keyed by the render box.
return WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1);
}),
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
child: const SizedBox(),
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between OverlayEntry & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1);
}),
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
child: const SizedBox(),
);
}
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between overlayChild & overlayChild', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return OverlayPortal(
// WidgetToRenderBoxAdapter is keyed by the render box.
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1),
child: const SizedBox(),
);
}),
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
child: const SizedBox(),
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between overlayChild & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1),
child: const SizedBox(),
);
}
);
}),
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState2 = stateSetter;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
child: const SizedBox(),
);
}
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between child & overlayChild', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return OverlayPortal(
// WidgetToRenderBoxAdapter is keyed by the render box.
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1),
child: WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
testWidgets('between child & overlayChild, featuring LayoutBuilder', (WidgetTester tester) async {
final _RenderLayoutCounter counter1 = _RenderLayoutCounter();
final _RenderLayoutCounter counter2 = _RenderLayoutCounter();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayStatefulEntry(builder: (BuildContext context, StateSetter stateSetter) {
setState1 = stateSetter;
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return OverlayPortal(
// WidgetToRenderBoxAdapter is keyed by the render box.
controller: controller1,
overlayChildBuilder: (BuildContext context) => WidgetToRenderBoxAdapter(renderBox: swapped ? counter2 : counter1),
child: WidgetToRenderBoxAdapter(renderBox: swapped ? counter1 : counter2),
);
}
);
}),
],
),
),
);
expect(counter1.layoutCount, 1);
expect(counter2.layoutCount, 1);
setState(newValue: true);
await tester.pump();
expect(counter1.layoutCount, 2);
expect(counter2.layoutCount, 2);
setState(newValue: false);
await tester.pump();
expect(counter1.layoutCount, 3);
expect(counter2.layoutCount, 3);
});
});
});
group('Paint order', () {
testWidgets('show bringsToTop', (WidgetTester tester) async {
controller1.hide();
const _PaintOrder child1 = _PaintOrder();
const _PaintOrder child2 = _PaintOrder();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return Column(
children: <Widget>[
OverlayPortal(controller: controller1, overlayChildBuilder: (BuildContext context) => child1),
OverlayPortal(controller: controller2, overlayChildBuilder: (BuildContext context) => child2),
],
);
}),
],
),
),
);
// Only child2 is visible.
expect(
_PaintOrder.paintOrder,
<_PaintOrder>[
child2,
],
);
_PaintOrder.paintOrder.clear();
controller1.show();
await tester.pump();
expect(
_PaintOrder.paintOrder,
<_PaintOrder>[
child2,
child1,
],
);
_PaintOrder.paintOrder.clear();
controller2.show();
await tester.pump();
expect(
_PaintOrder.paintOrder,
<_PaintOrder>[
child1,
child2,
],
);
_PaintOrder.paintOrder.clear();
controller2.hide();
controller1.hide();
await tester.pump();
expect(
_PaintOrder.paintOrder,
isEmpty,
);
});
testWidgets('Paint order does not change after global key reparenting', (WidgetTester tester) async {
final GlobalKey key = GlobalKey();
late StateSetter setState;
bool reparented = false;
// WidgetToRenderBoxAdapter has its own builtin GlobalKey.
final RenderBox child1Box = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final RenderBox child2Box = RenderConstrainedBox(additionalConstraints: const BoxConstraints());
final Widget child1 = WidgetToRenderBoxAdapter(renderBox: child1Box);
final Widget child2 = WidgetToRenderBoxAdapter(renderBox: child2Box);
final Widget overlayPortal1 = OverlayPortal(
key: key,
controller: controller1,
overlayChildBuilder: (BuildContext context) => child1,
child: const SizedBox(),
);
final Widget overlayPortal2 = OverlayPortal(
controller: controller2,
overlayChildBuilder: (BuildContext context) => child2,
child: const SizedBox(),
);
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Overlay(
initialEntries: <OverlayEntry>[
OverlayEntry(builder: (BuildContext context) {
return Column(
children: <Widget>[
StatefulBuilder(builder: (BuildContext context, StateSetter stateSetter) {
setState = stateSetter;
return reparented ? SizedBox(child: overlayPortal1) : overlayPortal1;
}),
overlayPortal2,
],
);
}),
],
),
),
);
final RenderObject theater = tester.renderObject<RenderObject>(find.byType(Overlay));
final List<RenderObject> childrenVisited = <RenderObject>[];
theater.visitChildren(childrenVisited.add);
expect(childrenVisited.length, 3);
expect(childrenVisited, containsAllInOrder(<AbstractNode>[child1Box.parent!, child2Box.parent!]));
childrenVisited.clear();
setState(() { reparented = true; });
await tester.pump();
theater.visitChildren(childrenVisited.add);
// The child list stays the same.
expect(childrenVisited, containsAllInOrder(<AbstractNode>[child1Box.parent!, child2Box.parent!]));
});
});
}
class OverlayStatefulEntry extends OverlayEntry {
OverlayStatefulEntry({
required StatefulWidgetBuilder builder,
}) : super(builder: (BuildContext context) => StatefulBuilder(builder: builder));
}
class _RenderLayoutCounter extends RenderProxyBox {
int layoutCount = 0;
bool _parentDoingLayout = false;
@override
void layout(Constraints constraints, {bool parentUsesSize = false}) {
assert(!_parentDoingLayout);
_parentDoingLayout = true;
layoutCount += 1;
super.layout(constraints, parentUsesSize: parentUsesSize);
_parentDoingLayout = false;
}
@override
void performLayout() {
super.performLayout();
if (!_parentDoingLayout) {
layoutCount += 1;
}
}
}
class _PaintOrder extends SingleChildRenderObjectWidget {
const _PaintOrder();
static List<_PaintOrder> paintOrder = <_PaintOrder>[];
void onPaint() => paintOrder.add(this);
@override
_RenderPaintRecorder createRenderObject(BuildContext context) => _RenderPaintRecorder()..onPaint = onPaint;
@override
void updateRenderObject(BuildContext context, _RenderPaintRecorder renderObject) => renderObject.onPaint = onPaint;
}
class _RenderPaintRecorder extends RenderProxyBox {
VoidCallback? onPaint;
@override
void paint(PaintingContext context, Offset offset) {
onPaint?.call();
super.paint(context, offset);
}
}