// 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/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; import 'semantics_tester.dart'; void main() { test('OverlayEntry dispatches memory events', () async { await expectLater( await memoryEvents( () => OverlayEntry( builder: (BuildContext context) => Container(), ).dispose(), OverlayEntry, ), areCreateAndDispose, ); }); testWidgets('OverflowEntries context contains Overlay', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); bool didBuild = false; late final OverlayEntry overlayEntry1; addTearDown(() => overlayEntry1..remove()..dispose()); late final OverlayEntry overlayEntry2; addTearDown(() => overlayEntry2..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ overlayEntry1 = OverlayEntry( builder: (BuildContext context) { didBuild = true; final Overlay overlay = context.findAncestorWidgetOfExactType<Overlay>()!; expect(overlay.key, equals(overlayKey)); return Container(); }, ), overlayEntry2 = OverlayEntry( builder: (BuildContext context) => Container(), ), ], ), ), ); expect(didBuild, isTrue); final RenderObject theater = overlayKey.currentContext!.findRenderObject()!; expect(theater, hasAGoodToStringDeep); expect( theater.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( '_RenderTheater#744c9\n' ' │ parentData: <none>\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' ' │ skipCount: 0\n' ' │ textDirection: ltr\n' ' │\n' ' ├─onstage 1: RenderLimitedBox#bb803\n' ' │ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n' ' │ │ size)\n' ' │ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ │ size: Size(800.0, 600.0)\n' ' │ │ maxWidth: 0.0\n' ' │ │ maxHeight: 0.0\n' ' │ │\n' ' │ └─child: RenderConstrainedBox#62707\n' ' │ parentData: <none> (can use size)\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' ' │ additionalConstraints: BoxConstraints(biggest)\n' ' │\n' ' ├─onstage 2: RenderLimitedBox#af5f1\n' ' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n' ' ╎ │ size)\n' ' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ │ size: Size(800.0, 600.0)\n' ' ╎ │ maxWidth: 0.0\n' ' ╎ │ maxHeight: 0.0\n' ' ╎ │\n' ' ╎ └─child: RenderConstrainedBox#69c48\n' ' ╎ parentData: <none> (can use size)\n' ' ╎ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ size: Size(800.0, 600.0)\n' ' ╎ additionalConstraints: BoxConstraints(biggest)\n' ' ╎\n' ' └╌no offstage children\n', ), ); }); testWidgets('Offstage overlay', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); late final OverlayEntry overlayEntry1; addTearDown(() => overlayEntry1..remove()..dispose()); late final OverlayEntry overlayEntry2; addTearDown(() => overlayEntry2..remove()..dispose()); late final OverlayEntry overlayEntry3; addTearDown(() => overlayEntry3..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ overlayEntry1 = OverlayEntry( opaque: true, maintainState: true, builder: (BuildContext context) => Container(), ), overlayEntry2 = OverlayEntry( opaque: true, maintainState: true, builder: (BuildContext context) => Container(), ), overlayEntry3 = OverlayEntry( opaque: true, maintainState: true, builder: (BuildContext context) => Container(), ), ], ), ), ); final RenderObject theater = overlayKey.currentContext!.findRenderObject()!; expect(theater, hasAGoodToStringDeep); expect( theater.toStringDeep(minLevel: DiagnosticLevel.info), equalsIgnoringHashCodes( '_RenderTheater#385b3\n' ' │ parentData: <none>\n' ' │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' │ size: Size(800.0, 600.0)\n' ' │ skipCount: 2\n' ' │ textDirection: ltr\n' ' │\n' ' ├─onstage 1: RenderLimitedBox#0a77a\n' ' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0) (can use\n' ' ╎ │ size)\n' ' ╎ │ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ │ size: Size(800.0, 600.0)\n' ' ╎ │ maxWidth: 0.0\n' ' ╎ │ maxHeight: 0.0\n' ' ╎ │\n' ' ╎ └─child: RenderConstrainedBox#21f3a\n' ' ╎ parentData: <none> (can use size)\n' ' ╎ constraints: BoxConstraints(w=800.0, h=600.0)\n' ' ╎ size: Size(800.0, 600.0)\n' ' ╎ additionalConstraints: BoxConstraints(biggest)\n' ' ╎\n' ' ╎╌offstage 1: RenderLimitedBox#62c8c NEEDS-LAYOUT NEEDS-PAINT\n' ' ╎ │ parentData: not positioned; offset=Offset(0.0, 0.0)\n' ' ╎ │ constraints: MISSING\n' ' ╎ │ size: MISSING\n' ' ╎ │ maxWidth: 0.0\n' ' ╎ │ maxHeight: 0.0\n' ' ╎ │\n' ' ╎ └─child: RenderConstrainedBox#425fa NEEDS-LAYOUT NEEDS-PAINT\n' ' ╎ parentData: <none>\n' ' ╎ constraints: MISSING\n' ' ╎ size: MISSING\n' ' ╎ additionalConstraints: BoxConstraints(biggest)\n' ' ╎\n' ' └╌offstage 2: RenderLimitedBox#03ae2 NEEDS-LAYOUT NEEDS-PAINT\n' ' │ parentData: not positioned; offset=Offset(0.0, 0.0)\n' ' │ constraints: MISSING\n' ' │ size: MISSING\n' ' │ maxWidth: 0.0\n' ' │ maxHeight: 0.0\n' ' │\n' ' └─child: RenderConstrainedBox#b4d48 NEEDS-LAYOUT NEEDS-PAINT\n' ' parentData: <none>\n' ' constraints: MISSING\n' ' size: MISSING\n' ' additionalConstraints: BoxConstraints(biggest)\n', ), ); }); testWidgets('insert top', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); final List<String> buildOrder = <String>[]; late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('Base'); return Container(); }, ), ], ), ), ); expect(buildOrder, <String>['Base']); buildOrder.clear(); final OverlayState overlay = overlayKey.currentState! as OverlayState; late final OverlayEntry newEntry; addTearDown(() => newEntry..remove()..dispose()); overlay.insert( newEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('New'); return Container(); }, ), ); await tester.pump(); expect(buildOrder, <String>['Base', 'New']); }); testWidgets('insert below', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); final List<String> buildOrder = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('Base'); return Container(); }, ), ], ), ), ); expect(buildOrder, <String>['Base']); buildOrder.clear(); final OverlayState overlay = overlayKey.currentState! as OverlayState; late final OverlayEntry newEntry; addTearDown(() => newEntry..remove()..dispose()); overlay.insert( newEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('New'); return Container(); }, ), below: baseEntry, ); await tester.pump(); expect(buildOrder, <String>['New', 'Base']); }); testWidgets('insert above', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); late final OverlayEntry topEntry; addTearDown(() => topEntry..remove()..dispose()); final List<String> buildOrder = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('Base'); return Container(); }, ), topEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('Top'); return Container(); }, ), ], ), ), ); expect(buildOrder, <String>['Base', 'Top']); buildOrder.clear(); final OverlayState overlay = overlayKey.currentState! as OverlayState; late final OverlayEntry newEntry; addTearDown(() => newEntry..remove()..dispose()); overlay.insert( newEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('New'); return Container(); }, ), above: baseEntry, ); await tester.pump(); expect(buildOrder, <String>['Base', 'New', 'Top']); }); testWidgets('insertAll top', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); final List<String> buildOrder = <String>[]; late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('Base'); return Container(); }, ), ], ), ), ); expect(buildOrder, <String>['Base']); final List<OverlayEntry> entries = <OverlayEntry>[ OverlayEntry( builder: (BuildContext context) { buildOrder.add('New1'); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add('New2'); return Container(); }, ), ]; addTearDown(() { for (final OverlayEntry entry in entries) { entry..remove()..dispose(); } }); buildOrder.clear(); final OverlayState overlay = overlayKey.currentState! as OverlayState; overlay.insertAll(entries); await tester.pump(); expect(buildOrder, <String>['Base', 'New1', 'New2']); }); testWidgets('insertAll below', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); final List<String> buildOrder = <String>[]; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('Base'); return Container(); }, ), ], ), ), ); expect(buildOrder, <String>['Base']); final List<OverlayEntry> entries = <OverlayEntry>[ OverlayEntry( builder: (BuildContext context) { buildOrder.add('New1'); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add('New2'); return Container(); }, ), ]; addTearDown(() { for (final OverlayEntry entry in entries) { entry..remove()..dispose(); } }); buildOrder.clear(); final OverlayState overlay = overlayKey.currentState! as OverlayState; overlay.insertAll(entries, below: baseEntry); await tester.pump(); expect(buildOrder, <String>['New1', 'New2','Base']); }); testWidgets('insertAll above', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); final List<String> buildOrder = <String>[]; late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); late final OverlayEntry topEntry; addTearDown(() => topEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('Base'); return Container(); }, ), topEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add('Top'); return Container(); }, ), ], ), ), ); expect(buildOrder, <String>['Base', 'Top']); final List<OverlayEntry> entries = <OverlayEntry>[ OverlayEntry( builder: (BuildContext context) { buildOrder.add('New1'); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add('New2'); return Container(); }, ), ]; addTearDown(() { for (final OverlayEntry entry in entries) { entry..remove()..dispose(); } }); buildOrder.clear(); final OverlayState overlay = overlayKey.currentState! as OverlayState; overlay.insertAll(entries, above: baseEntry); await tester.pump(); expect(buildOrder, <String>['Base', 'New1', 'New2', 'Top']); }); testWidgets('rearrange', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); final List<int> buildOrder = <int>[]; final List<OverlayEntry> initialEntries = <OverlayEntry>[ OverlayEntry( builder: (BuildContext context) { buildOrder.add(0); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add(1); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add(2); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add(3); return Container(); }, ), ]; addTearDown(() { for (final OverlayEntry entry in initialEntries) { entry..remove()..dispose(); } }); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: initialEntries, ), ), ); expect(buildOrder, <int>[0, 1, 2, 3]); late final OverlayEntry newEntry; addTearDown(() => newEntry..remove()..dispose()); final List<OverlayEntry> rearranged = <OverlayEntry>[ initialEntries[3], newEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add(4); return Container(); }, ), initialEntries[2], // 1 intentionally missing, will end up on top initialEntries[0], ]; buildOrder.clear(); final OverlayState overlay = overlayKey.currentState! as OverlayState; overlay.rearrange(rearranged); await tester.pump(); expect(buildOrder, <int>[3, 4, 2, 0, 1]); }); testWidgets('rearrange above', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); final List<int> buildOrder = <int>[]; final List<OverlayEntry> initialEntries = <OverlayEntry>[ OverlayEntry( builder: (BuildContext context) { buildOrder.add(0); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add(1); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add(2); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add(3); return Container(); }, ), ]; addTearDown(() { for (final OverlayEntry entry in initialEntries) { entry..remove()..dispose(); } }); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: initialEntries, ), ), ); expect(buildOrder, <int>[0, 1, 2, 3]); late final OverlayEntry newEntry; addTearDown(() => newEntry..remove()..dispose()); final List<OverlayEntry> rearranged = <OverlayEntry>[ initialEntries[3], newEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add(4); return Container(); }, ), initialEntries[2], // 1 intentionally missing initialEntries[0], ]; buildOrder.clear(); final OverlayState overlay = overlayKey.currentState! as OverlayState; overlay.rearrange(rearranged, above: initialEntries[2]); await tester.pump(); expect(buildOrder, <int>[3, 4, 2, 1, 0]); }); testWidgets('rearrange below', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); final List<int> buildOrder = <int>[]; final List<OverlayEntry> initialEntries = <OverlayEntry>[ OverlayEntry( builder: (BuildContext context) { buildOrder.add(0); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add(1); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add(2); return Container(); }, ), OverlayEntry( builder: (BuildContext context) { buildOrder.add(3); return Container(); }, ), ]; addTearDown(() { for (final OverlayEntry entry in initialEntries) { entry..remove()..dispose(); } }); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: initialEntries, ), ), ); expect(buildOrder, <int>[0, 1, 2, 3]); late final OverlayEntry newEntry; addTearDown(() => newEntry..remove()..dispose()); final List<OverlayEntry> rearranged = <OverlayEntry>[ initialEntries[3], newEntry = OverlayEntry( builder: (BuildContext context) { buildOrder.add(4); return Container(); }, ), initialEntries[2], // 1 intentionally missing initialEntries[0], ]; buildOrder.clear(); final OverlayState overlay = overlayKey.currentState! as OverlayState; overlay.rearrange(rearranged, below: initialEntries[2]); await tester.pump(); expect(buildOrder, <int>[3, 4, 1, 2, 0]); }); testWidgets('debugVerifyInsertPosition', (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); late OverlayEntry base; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ base = _buildOverlayEntry((BuildContext context) { return Container(); }, ), ], ), ), ); final OverlayState overlay = overlayKey.currentState! as OverlayState; try { overlay.insert( _buildOverlayEntry((BuildContext context) { return Container(); }), above: _buildOverlayEntry((BuildContext context) { return Container(); }, ), below: _buildOverlayEntry((BuildContext context) { return Container(); }, ), ); } on AssertionError catch (e) { expect(e.message, 'Only one of `above` and `below` may be specified.'); } expect(() => overlay.insert( _buildOverlayEntry((BuildContext context) { return Container(); }), above: base, ), isNot(throwsAssertionError)); try { overlay.insert( _buildOverlayEntry((BuildContext context) { return Container(); }), above: _buildOverlayEntry((BuildContext context) { return Container(); }, ), ); } on AssertionError catch (e) { expect(e.message, 'The provided entry used for `above` must be present in the Overlay.'); } try { overlay.rearrange(<OverlayEntry>[base], above: _buildOverlayEntry((BuildContext context) { return Container(); }, )); } on AssertionError catch (e) { expect(e.message, 'The provided entry used for `above` must be present in the Overlay and in the `newEntriesList`.'); } await tester.pump(); }); testWidgets('OverlayState.of() throws when called if an Overlay does not exist', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Builder( builder: (BuildContext context) { late FlutterError error; final Widget debugRequiredFor = Container(); try { Overlay.of(context, debugRequiredFor: debugRequiredFor); } on FlutterError catch (e) { error = e; } finally { expect(error, isNotNull); expect(error.diagnostics.length, 5); expect(error.diagnostics[2].level, DiagnosticLevel.hint); expect(error.diagnostics[2].toStringDeep(), equalsIgnoringHashCodes( 'The most common way to add an Overlay to an application is to\n' 'include a MaterialApp, CupertinoApp or Navigator widget in the\n' 'runApp() call.\n' )); expect(error.diagnostics[3], isA<DiagnosticsProperty<Widget>>()); expect(error.diagnostics[3].value, debugRequiredFor); expect(error.diagnostics[4], isA<DiagnosticsProperty<Element>>()); expect(error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' No Overlay widget found.\n' ' Container widgets require an Overlay widget ancestor for correct\n' ' operation.\n' ' The most common way to add an Overlay to an application is to\n' ' include a MaterialApp, CupertinoApp or Navigator widget in the\n' ' runApp() call.\n' ' The specific widget that failed to find an overlay was:\n' ' Container\n' ' The context from which that widget was searching for an overlay\n' ' was:\n' ' Builder\n' )); } return Container(); }, ), ), ); }); testWidgets("OverlayState.maybeOf() works when an Overlay does and doesn't exist", (WidgetTester tester) async { final GlobalKey overlayKey = GlobalKey(); OverlayState? foundState; late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { foundState = Overlay.maybeOf(context); return Container(); }, ), ], ), ), ); expect(tester.takeException(), isNull); expect(foundState, isNotNull); foundState = null; await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Builder( builder: (BuildContext context) { foundState = Overlay.maybeOf(context); return const SizedBox(); }, ), ), ); expect(tester.takeException(), isNull); expect(foundState, isNull); }); testWidgets('OverlayEntry.opaque can be changed when OverlayEntry is not part of an Overlay (yet)', (WidgetTester tester) async { final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>(); final Key root = UniqueKey(); final Key top = UniqueKey(); final OverlayEntry rootEntry = OverlayEntry( builder: (BuildContext context) { return Container(key: root); }, ); addTearDown(() => rootEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ rootEntry, ], ), ), ); expect(find.byKey(root), findsOneWidget); final OverlayEntry newEntry = OverlayEntry( builder: (BuildContext context) { return Container(key: top); }, ); addTearDown(() => newEntry..remove()..dispose()); expect(newEntry.opaque, isFalse); newEntry.opaque = true; // Does neither trigger an assert nor throw. expect(newEntry.opaque, isTrue); // The new opaqueness is honored when inserted into an overlay. overlayKey.currentState!.insert(newEntry); await tester.pumpAndSettle(); expect(find.byKey(root), findsNothing); expect(find.byKey(top), findsOneWidget); }); testWidgets('OverlayEntries do not rebuild when opaqueness changes', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/45797. final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>(); final Key bottom = UniqueKey(); final Key middle = UniqueKey(); final Key top = UniqueKey(); final Widget bottomWidget = StatefulTestWidget(key: bottom); final Widget middleWidget = StatefulTestWidget(key: middle); final Widget topWidget = StatefulTestWidget(key: top); final OverlayEntry bottomEntry = OverlayEntry( maintainState: true, builder: (BuildContext context) { return bottomWidget; }, ); addTearDown(() => bottomEntry..remove()..dispose()); final OverlayEntry middleEntry = OverlayEntry( maintainState: true, builder: (BuildContext context) { return middleWidget; }, ); addTearDown(() => middleEntry..remove()..dispose()); final OverlayEntry topEntry = OverlayEntry( maintainState: true, builder: (BuildContext context) { return topWidget; }, ); addTearDown(() => topEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ bottomEntry, middleEntry, topEntry, ], ), ), ); // All widgets are onstage. expect(tester.state<StatefulTestState>(find.byKey(bottom)).rebuildCount, 1); expect(tester.state<StatefulTestState>(find.byKey(middle)).rebuildCount, 1); expect(tester.state<StatefulTestState>(find.byKey(top)).rebuildCount, 1); middleEntry.opaque = true; await tester.pump(); // Bottom widget is offstage and did not rebuild. expect(find.byKey(bottom), findsNothing); expect(tester.state<StatefulTestState>(find.byKey(bottom, skipOffstage: false)).rebuildCount, 1); expect(tester.state<StatefulTestState>(find.byKey(middle)).rebuildCount, 1); expect(tester.state<StatefulTestState>(find.byKey(top)).rebuildCount, 1); }); testWidgets('OverlayEntries do not rebuild when opaque entry is added', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/45797. final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>(); final Key bottom = UniqueKey(); final Key middle = UniqueKey(); final Key top = UniqueKey(); final Widget bottomWidget = StatefulTestWidget(key: bottom); final Widget middleWidget = StatefulTestWidget(key: middle); final Widget topWidget = StatefulTestWidget(key: top); final OverlayEntry bottomEntry = OverlayEntry( maintainState: true, builder: (BuildContext context) { return bottomWidget; }, ); addTearDown(() => bottomEntry..remove()..dispose()); final OverlayEntry middleEntry = OverlayEntry( opaque: true, maintainState: true, builder: (BuildContext context) { return middleWidget; }, ); addTearDown(() => middleEntry..remove()..dispose()); final OverlayEntry topEntry = OverlayEntry( maintainState: true, builder: (BuildContext context) { return topWidget; }, ); addTearDown(() => topEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ bottomEntry, topEntry, ], ), ), ); // Both widgets are onstage. expect(tester.state<StatefulTestState>(find.byKey(bottom)).rebuildCount, 1); expect(tester.state<StatefulTestState>(find.byKey(top)).rebuildCount, 1); overlayKey.currentState!.rearrange(<OverlayEntry>[ bottomEntry, middleEntry, topEntry, ]); await tester.pump(); // Bottom widget is offstage and did not rebuild. expect(find.byKey(bottom), findsNothing); expect(tester.state<StatefulTestState>(find.byKey(bottom, skipOffstage: false)).rebuildCount, 1); expect(tester.state<StatefulTestState>(find.byKey(middle)).rebuildCount, 1); expect(tester.state<StatefulTestState>(find.byKey(top)).rebuildCount, 1); }); testWidgets('entries below opaque entries are ignored for hit testing', (WidgetTester tester) async { final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>(); int bottomTapCount = 0; late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( maintainState: true, builder: (BuildContext context) { return GestureDetector( onTap: () { bottomTapCount++; }, ); }, ), ], ), ), ); expect(bottomTapCount, 0); await tester.tap(find.byKey(overlayKey), warnIfMissed: false); // gesture detector is translucent; no hit is registered between it and the render view expect(bottomTapCount, 1); late final OverlayEntry newEntry1; addTearDown(() => newEntry1..remove()..dispose()); overlayKey.currentState!.insert( newEntry1 = OverlayEntry( maintainState: true, opaque: true, builder: (BuildContext context) { return Container(); }, ), ); await tester.pump(); // Bottom is offstage and does not receive tap events. expect(find.byType(GestureDetector), findsNothing); expect(find.byType(GestureDetector, skipOffstage: false), findsOneWidget); await tester.tap(find.byKey(overlayKey), warnIfMissed: false); // gesture detector is translucent; no hit is registered between it and the render view expect(bottomTapCount, 1); int topTapCount = 0; late final OverlayEntry newEntry2; addTearDown(() => newEntry2..remove()..dispose()); overlayKey.currentState!.insert( newEntry2 = OverlayEntry( maintainState: true, opaque: true, builder: (BuildContext context) { return GestureDetector( onTap: () { topTapCount++; }, ); }, ), ); await tester.pump(); expect(topTapCount, 0); await tester.tap(find.byKey(overlayKey), warnIfMissed: false); // gesture detector is translucent; no hit is registered between it and the render view expect(topTapCount, 1); expect(bottomTapCount, 1); }); testWidgets('Semantics of entries below opaque entries are ignored', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final GlobalKey<OverlayState> overlayKey = GlobalKey<OverlayState>(); late final OverlayEntry bottomEntry; addTearDown(() => bottomEntry..remove()..dispose()); late final OverlayEntry topEntry; addTearDown(() => topEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( key: overlayKey, initialEntries: <OverlayEntry>[ bottomEntry = OverlayEntry( maintainState: true, builder: (BuildContext context) { return const Text('bottom'); }, ), topEntry = OverlayEntry( maintainState: true, opaque: true, builder: (BuildContext context) { return const Text('top'); }, ), ], ), ), ); expect(find.text('bottom'), findsNothing); expect(find.text('bottom', skipOffstage: false), findsOneWidget); expect(find.text('top'), findsOneWidget); expect(semantics, includesNodeWith(label: 'top')); expect(semantics, isNot(includesNodeWith(label: 'bottom'))); semantics.dispose(); }); testWidgets('Can use Positioned within OverlayEntry', (WidgetTester tester) async { late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { return const Positioned( left: 145, top: 123, child: Text('positioned child'), ); }, ), ], ), ), ); expect(tester.getTopLeft(find.text('positioned child')), const Offset(145, 123)); }); testWidgets('Overlay can set and update clipBehavior', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ _buildOverlayEntry((BuildContext context) => Positioned(left: 2000, right: 2500, child: Container())), ], ), ), ); // By default, clipBehavior should be Clip.hardEdge final RenderObject renderObject = tester.renderObject(find.byType(Overlay)); expect((renderObject as dynamic).clipBehavior, equals(Clip.hardEdge)); for (final Clip clip in Clip.values) { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ _buildOverlayEntry((BuildContext context) => Container()), ], clipBehavior: clip, ), ), ); expect((renderObject as dynamic).clipBehavior, clip); bool visited = false; renderObject.visitChildren((RenderObject child) { visited = true; switch (clip) { case Clip.none: expect(renderObject.describeApproximatePaintClip(child), null); case Clip.hardEdge: case Clip.antiAlias: case Clip.antiAliasWithSaveLayer: expect( renderObject.describeApproximatePaintClip(child), const Rect.fromLTRB(0, 0, 800, 600), ); } }); expect(visited, true); } }); testWidgets('Overlay always applies clip', (WidgetTester tester) async { late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) => Positioned(left: 10, right: 10, child: Container()), ), ], ), ), ); final RenderObject renderObject = tester.renderObject(find.byType(Overlay)); expect((renderObject as dynamic).paint, paints ..save() ..clipRect(rect: const Rect.fromLTWH(0.0, 0.0, 800.0, 600.0)) ..restore(), ); }); testWidgets('OverlayEntry throws if inserted to an invalid Overlay', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: Overlay(), ), ); final OverlayState overlay = tester.state(find.byType(Overlay)); final OverlayEntry entry = OverlayEntry(builder: (BuildContext context) => const SizedBox()); addTearDown(() => entry..remove()..dispose()); expect( () => overlay.insert(entry), returnsNormally, ); // Throws when inserted to the same Overlay. expect( () => overlay.insert(entry), throwsA(isA<FlutterError>().having( (FlutterError error) => error.toString(), 'toString()', allOf( contains('The specified entry is already present in the target Overlay.'), contains('The OverlayEntry was'), contains('The Overlay the OverlayEntry was trying to insert to was'), ), )), ); await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: SizedBox(child: Overlay()), ), ); // Throws if inserted to an already disposed Overlay. expect( () => overlay.insert(entry), throwsA(isA<FlutterError>().having( (FlutterError error) => error.toString(), 'toString()', allOf( contains('Attempted to insert an OverlayEntry to an already disposed Overlay.'), contains('The OverlayEntry was'), contains('The Overlay the OverlayEntry was trying to insert to was'), ), )), ); final OverlayState newOverlay = tester.state(find.byType(Overlay)); // Throws when inserted to a different Overlay without calling remove. expect( () => newOverlay.insert(entry), throwsA(isA<FlutterError>().having( (FlutterError error) => error.toString(), 'toString()', allOf( contains('The specified entry is already present in a different Overlay.'), contains('The OverlayEntry was'), contains('The Overlay the OverlayEntry was trying to insert to was'), contains("The OverlayEntry's current Overlay was"), ), )), ); }); group('OverlayEntry listenable', () { final GlobalKey overlayKey = GlobalKey(); final Widget emptyOverlay = Directionality( textDirection: TextDirection.ltr, child: Overlay(key: overlayKey), ); testWidgets('mounted state can be listened', (WidgetTester tester) async { await tester.pumpWidget(emptyOverlay); final OverlayState overlay = overlayKey.currentState! as OverlayState; final List<bool> mountedLog = <bool>[]; final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) => Container(), ); addTearDown(entry.dispose); entry.addListener(() { mountedLog.add(entry.mounted); }); overlay.insert(entry); expect(mountedLog, isEmpty); // Pump a frame. The Overlay entry will be mounted. await tester.pump(); expect(mountedLog, <bool>[true]); entry.remove(); expect(mountedLog, <bool>[true]); await tester.pump(); expect(mountedLog, <bool>[true, false]); // Insert & remove again. overlay.insert(entry); await tester.pump(); entry.remove(); await tester.pump(); expect(mountedLog, <bool>[true, false, true, false]); }); testWidgets('throw if disposed before removal', (WidgetTester tester) async { await tester.pumpWidget(emptyOverlay); final OverlayState overlay = overlayKey.currentState! as OverlayState; final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) => Container(), ); addTearDown(() => entry..remove()..dispose()); overlay.insert(entry); Object? error; try { entry.dispose(); } catch (e) { error = e; } expect(error, isAssertionError); }); test('dispose works', () { final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) => Container(), ); entry.dispose(); Object? error; try { entry.addListener(() { }); } catch (e) { error = e; } expect(error, isAssertionError); }); testWidgets('delayed dispose', (WidgetTester tester) async { await tester.pumpWidget(emptyOverlay); final OverlayState overlay = overlayKey.currentState! as OverlayState; final List<bool> mountedLog = <bool>[]; final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) => Container(), ); entry.addListener(() { mountedLog.add(entry.mounted); }); overlay.insert(entry); await tester.pump(); expect(mountedLog, <bool>[true]); entry.remove(); // Call dispose on the entry. The listeners should be notified for one // last time after this. entry.dispose(); expect(mountedLog, <bool>[true]); await tester.pump(); expect(mountedLog, <bool>[true, false]); expect(tester.takeException(), isNull); // The entry is no longer usable. Object? error; try { entry.addListener(() { }); } catch (e) { error = e; } expect(error, isAssertionError); }); }); group('LookupBoundary', () { testWidgets('hides Overlay from Overlay.maybeOf', (WidgetTester tester) async { OverlayState? overlay; late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { return LookupBoundary( child: Builder( builder: (BuildContext context) { overlay = Overlay.maybeOf(context); return Container(); }, ), ); }, ), ], ), ), ); expect(overlay, isNull); }); testWidgets('hides Overlay from Overlay.of', (WidgetTester tester) async { late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { return LookupBoundary( child: Builder( builder: (BuildContext context) { Overlay.of(context); return Container(); }, ), ); }, ), ], ), ), ); final Object? exception = tester.takeException(); expect(exception, isFlutterError); final FlutterError error = exception! as FlutterError; expect( error.toStringDeep(), 'FlutterError\n' ' No Overlay widget found within the closest LookupBoundary.\n' ' There is an ancestor Overlay widget, but it is hidden by a\n' ' LookupBoundary.\n' ' Some widgets require an Overlay widget ancestor for correct\n' ' operation.\n' ' The most common way to add an Overlay to an application is to\n' ' include a MaterialApp, CupertinoApp or Navigator widget in the\n' ' runApp() call.\n' ' The context from which that widget was searching for an overlay\n' ' was:\n' ' Builder\n' ); }); testWidgets('hides Overlay from debugCheckHasOverlay', (WidgetTester tester) async { late final OverlayEntry baseEntry; addTearDown(() => baseEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay( initialEntries: <OverlayEntry>[ baseEntry = OverlayEntry( builder: (BuildContext context) { return LookupBoundary( child: Builder( builder: (BuildContext context) { debugCheckHasOverlay(context); return Container(); }, ), ); }, ), ], ), ), ); final Object? exception = tester.takeException(); expect(exception, isFlutterError); final FlutterError error = exception! as FlutterError; expect( error.toStringDeep(), startsWith( 'FlutterError\n' ' No Overlay widget found within the closest LookupBoundary.\n' ' There is an ancestor Overlay widget, but it is hidden by a\n' ' LookupBoundary.\n' ' Builder widgets require an Overlay widget ancestor within the\n' ' closest LookupBoundary.\n' ' An overlay lets widgets float on top of other widget children.\n' ' To introduce an Overlay widget, you can either directly include\n' ' one, or use a widget that contains an Overlay itself, such as a\n' ' Navigator, WidgetApp, MaterialApp, or CupertinoApp.\n' ' The specific widget that could not find a Overlay ancestor was:\n' ' Builder\n' ' The ancestors of this widget were:\n' ' LookupBoundary\n' ), ); }); }); testWidgets('Overlay.wrap', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay.wrap( child: const Center( child: Text('Hello World'), ), ), ), ); final State overlayState = tester.state(find.byType(Overlay)); expect(find.text('Hello World'), findsOneWidget); expect(find.text('Bye, bye'), findsNothing); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Overlay.wrap( child: const Center( child: Text('Bye, bye'), ), ), ), ); expect(find.text('Hello World'), findsNothing); expect(find.text('Bye, bye'), findsOneWidget); expect(tester.state(find.byType(Overlay)), same(overlayState)); }); testWidgets('Overlay.wrap is sized by child in an unconstrained environment', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: UnconstrainedBox( child: Overlay.wrap( child: const Center( child: SizedBox( width: 123, height: 456, ) ), ), ), ), ); expect(tester.getSize(find.byType(Overlay)), const Size(123, 456)); }); testWidgets('Overlay is sized by child in an unconstrained environment', (WidgetTester tester) async { final OverlayEntry initialEntry = OverlayEntry( opaque: true, canSizeOverlay: true, builder: (BuildContext context) { return const SizedBox(width: 123, height: 456); } ); addTearDown(() => initialEntry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: UnconstrainedBox( child: Overlay( initialEntries: <OverlayEntry>[initialEntry] ), ), ), ); expect(tester.getSize(find.byType(Overlay)), const Size(123, 456)); final OverlayState overlay = tester.state<OverlayState>(find.byType(Overlay)); final OverlayEntry nonSizingEntry = OverlayEntry( builder: (BuildContext context) { return const SizedBox( width: 600, height: 600, child: Center(child: Text('Hello')), ); }, ); addTearDown(nonSizingEntry.dispose); overlay.insert(nonSizingEntry); await tester.pump(); expect(tester.getSize(find.byType(Overlay)), const Size(123, 456)); expect(find.text('Hello'), findsOneWidget); final OverlayEntry sizingEntry = OverlayEntry( canSizeOverlay: true, builder: (BuildContext context) { return const SizedBox( width: 222, height: 111, child: Center(child: Text('World')), ); }, ); addTearDown(sizingEntry.dispose); overlay.insert(sizingEntry); await tester.pump(); expect(tester.getSize(find.byType(Overlay)), const Size(222, 111)); expect(find.text('Hello'), findsOneWidget); expect(find.text('World'), findsOneWidget); nonSizingEntry.remove(); await tester.pump(); expect(tester.getSize(find.byType(Overlay)), const Size(222, 111)); expect(find.text('Hello'), findsNothing); expect(find.text('World'), findsOneWidget); sizingEntry.remove(); await tester.pump(); expect(tester.getSize(find.byType(Overlay)), const Size(123, 456)); expect(find.text('Hello'), findsNothing); expect(find.text('World'), findsNothing); }); testWidgets('Overlay throws if unconstrained and has no child', (WidgetTester tester) async { final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[]; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = errors.add; await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: UnconstrainedBox( child: Overlay(), ), ), ); FlutterError.onError = oldHandler; expect( errors.first.toString().replaceAll('\n', ' '), contains('Overlay was given infinite constraints and cannot be sized by a suitable child.'), ); }); testWidgets('Overlay throws if unconstrained and only positioned child', (WidgetTester tester) async { final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[]; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = errors.add; final OverlayEntry entry = OverlayEntry( canSizeOverlay: true, builder: (BuildContext context) { return const Positioned( top: 100, child: SizedBox(width: 600, height: 600), ); }, ); addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: UnconstrainedBox( child: Overlay( initialEntries: <OverlayEntry>[entry], ), ), ), ); FlutterError.onError = oldHandler; expect( errors.first.toString().replaceAll('\n', ' '), contains('Overlay was given infinite constraints and cannot be sized by a suitable child.'), ); }); testWidgets('Overlay throws if unconstrained and no canSizeOverlay child', (WidgetTester tester) async { final List<FlutterErrorDetails> errors = <FlutterErrorDetails>[]; final FlutterExceptionHandler? oldHandler = FlutterError.onError; FlutterError.onError = errors.add; final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) { return const SizedBox(width: 600, height: 600); }, ); addTearDown(() => entry..remove()..dispose()); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: UnconstrainedBox( child: Overlay( initialEntries: <OverlayEntry>[entry], ), ), ), ); FlutterError.onError = oldHandler; expect( errors.first.toString().replaceAll('\n', ' '), contains('Overlay was given infinite constraints and cannot be sized by a suitable child.'), ); }); } class StatefulTestWidget extends StatefulWidget { const StatefulTestWidget({super.key}); @override State<StatefulTestWidget> createState() => StatefulTestState(); } class StatefulTestState extends State<StatefulTestWidget> { int rebuildCount = 0; @override Widget build(BuildContext context) { rebuildCount += 1; return Container(); } } /// This helper makes leak tracker forgiving the entry is not disposed. OverlayEntry _buildOverlayEntry(WidgetBuilder builder) => OverlayEntry(builder: builder);