1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:ui';
6
7import 'package:flutter/cupertino.dart';
8import 'package:flutter/material.dart';
9import 'package:flutter/rendering.dart';
10import 'package:flutter/services.dart';
11import 'package:flutter_test/flutter_test.dart';
12import '../widgets/semantics_tester.dart';
13
14MaterialApp _buildAppWithDialog(
15 Widget dialog, {
16 ThemeData? theme,
17 double textScaleFactor = 1.0,
18 TraversalEdgeBehavior? traversalEdgeBehavior,
19}) {
20 return MaterialApp(
21 theme: theme,
22 home: Material(
23 child: Builder(
24 builder: (BuildContext context) {
25 return Center(
26 child: ElevatedButton(
27 child: const Text('X'),
28 onPressed: () {
29 showDialog<void>(
30 context: context,
31 traversalEdgeBehavior: traversalEdgeBehavior,
32 builder: (BuildContext context) {
33 return MediaQuery.withClampedTextScaling(
34 minScaleFactor: textScaleFactor,
35 maxScaleFactor: textScaleFactor,
36 child: dialog,
37 );
38 },
39 );
40 },
41 ),
42 );
43 },
44 ),
45 ),
46 );
47}
48
49Material _getMaterialFromDialog(WidgetTester tester) {
50 return tester.widget<Material>(
51 find.descendant(of: find.byType(Dialog), matching: find.byType(Material)),
52 );
53}
54
55RenderParagraph _getTextRenderObjectFromDialog(WidgetTester tester, String text) {
56 return tester
57 .element<StatelessElement>(
58 find.descendant(of: find.byType(Dialog), matching: find.text(text)),
59 )
60 .renderObject!
61 as RenderParagraph;
62}
63
64// What was the AlertDialog's ButtonBar when many of these tests were written,
65// is now a Padding widget with an OverflowBar child. The Padding widget's size
66// and location match the original ButtonBar's size and location.
67Finder _findOverflowBar() {
68 return find.ancestor(of: find.byType(OverflowBar), matching: find.byType(Padding)).first;
69}
70
71const ShapeBorder _defaultM2DialogShape = RoundedRectangleBorder(
72 borderRadius: BorderRadius.all(Radius.circular(4.0)),
73);
74final ShapeBorder _defaultM3DialogShape = RoundedRectangleBorder(
75 borderRadius: BorderRadius.circular(28.0),
76);
77
78void main() {
79 final ThemeData material3Theme = ThemeData(brightness: Brightness.dark);
80 final ThemeData material2Theme = ThemeData(useMaterial3: false, brightness: Brightness.dark);
81
82 testWidgets('Dialog is scrollable', (WidgetTester tester) async {
83 bool didPressOk = false;
84 final AlertDialog dialog = AlertDialog(
85 content: Container(height: 5000.0, width: 300.0, color: Colors.green[500]),
86 actions: <Widget>[
87 TextButton(
88 onPressed: () {
89 didPressOk = true;
90 },
91 child: const Text('OK'),
92 ),
93 ],
94 );
95 await tester.pumpWidget(_buildAppWithDialog(dialog));
96
97 await tester.tap(find.text('X'));
98 await tester.pumpAndSettle();
99
100 expect(didPressOk, false);
101 await tester.tap(find.text('OK'));
102 expect(didPressOk, true);
103 });
104
105 testWidgets('Dialog background color from AlertDialog', (WidgetTester tester) async {
106 const Color customColor = Colors.pink;
107 const AlertDialog dialog = AlertDialog(backgroundColor: customColor, actions: <Widget>[]);
108 await tester.pumpWidget(
109 _buildAppWithDialog(dialog, theme: ThemeData(brightness: Brightness.dark)),
110 );
111
112 await tester.tap(find.text('X'));
113 await tester.pumpAndSettle();
114
115 final Material materialWidget = _getMaterialFromDialog(tester);
116 expect(materialWidget.color, customColor);
117 });
118
119 testWidgets('Dialog background defaults to ColorScheme.surfaceContainerHigh', (
120 WidgetTester tester,
121 ) async {
122 final ThemeData theme = ThemeData(
123 colorScheme: ThemeData().colorScheme.copyWith(
124 surface: Colors.orange,
125 background: Colors.green,
126 surfaceContainerHigh: Colors.red,
127 ),
128 );
129 const Dialog dialog = Dialog(child: SizedBox(width: 200, height: 200));
130 await tester.pumpWidget(_buildAppWithDialog(dialog, theme: theme));
131
132 await tester.tap(find.text('X'));
133 await tester.pumpAndSettle();
134
135 final Material materialWidget = _getMaterialFromDialog(tester);
136 expect(materialWidget.color, theme.colorScheme.surfaceContainerHigh);
137 });
138
139 testWidgets('Material2 - Dialog Defaults', (WidgetTester tester) async {
140 const AlertDialog dialog = AlertDialog(
141 title: Text('Title'),
142 content: Text('Y'),
143 actions: <Widget>[],
144 );
145 await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material2Theme));
146
147 await tester.tap(find.text('X'));
148 await tester.pumpAndSettle();
149
150 final Material materialWidget = _getMaterialFromDialog(tester);
151 expect(materialWidget.color, Colors.grey[800]);
152 expect(materialWidget.shape, _defaultM2DialogShape);
153 expect(materialWidget.elevation, 24.0);
154
155 final Offset bottomLeft = tester.getBottomLeft(
156 find.descendant(of: find.byType(Dialog), matching: find.byType(Material)),
157 );
158 expect(bottomLeft.dy, 360.0);
159 });
160
161 testWidgets('Material3 - Dialog Defaults', (WidgetTester tester) async {
162 const AlertDialog dialog = AlertDialog(
163 title: Text('Title'),
164 content: Text('Y'),
165 actions: <Widget>[],
166 );
167 await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material3Theme));
168
169 await tester.tap(find.text('X'));
170 await tester.pumpAndSettle();
171
172 final Material material3Widget = _getMaterialFromDialog(tester);
173 expect(material3Widget.color, material3Theme.colorScheme.surfaceContainerHigh);
174 expect(material3Widget.shape, _defaultM3DialogShape);
175 expect(material3Widget.elevation, 6.0);
176 });
177
178 testWidgets('Material2 - Dialog.fullscreen Defaults', (WidgetTester tester) async {
179 const String dialogTextM2 = 'Fullscreen Dialog - M2';
180
181 await tester.pumpWidget(
182 _buildAppWithDialog(
183 theme: material2Theme,
184 const Dialog.fullscreen(child: Text(dialogTextM2)),
185 ),
186 );
187
188 await tester.tap(find.text('X'));
189 await tester.pumpAndSettle();
190
191 expect(find.text(dialogTextM2), findsOneWidget);
192
193 final Material materialWidget = _getMaterialFromDialog(tester);
194 expect(materialWidget.color, Colors.grey[800]);
195
196 // Try to dismiss the fullscreen dialog with the escape key.
197 await tester.sendKeyEvent(LogicalKeyboardKey.escape);
198 await tester.pumpAndSettle();
199
200 expect(find.text(dialogTextM2), findsNothing);
201 });
202
203 testWidgets('Material3 - Dialog.fullscreen Defaults', (WidgetTester tester) async {
204 const String dialogTextM3 = 'Fullscreen Dialog - M3';
205
206 await tester.pumpWidget(
207 _buildAppWithDialog(
208 theme: material3Theme,
209 const Dialog.fullscreen(child: Text(dialogTextM3)),
210 ),
211 );
212
213 await tester.tap(find.text('X'));
214 await tester.pumpAndSettle();
215
216 expect(find.text(dialogTextM3), findsOneWidget);
217
218 final Material materialWidget = _getMaterialFromDialog(tester);
219 expect(materialWidget.color, material3Theme.colorScheme.surface);
220
221 // Try to dismiss the fullscreen dialog with the escape key.
222 await tester.sendKeyEvent(LogicalKeyboardKey.escape);
223 await tester.pumpAndSettle();
224
225 expect(find.text(dialogTextM3), findsNothing);
226 });
227
228 testWidgets('Custom dialog elevation', (WidgetTester tester) async {
229 const double customElevation = 12.0;
230 const Color shadowColor = Color(0xFF000001);
231 const Color surfaceTintColor = Color(0xFF000002);
232 const AlertDialog dialog = AlertDialog(
233 actions: <Widget>[],
234 elevation: customElevation,
235 shadowColor: shadowColor,
236 surfaceTintColor: surfaceTintColor,
237 );
238 await tester.pumpWidget(_buildAppWithDialog(dialog));
239
240 await tester.tap(find.text('X'));
241 await tester.pumpAndSettle();
242
243 final Material materialWidget = _getMaterialFromDialog(tester);
244 expect(materialWidget.elevation, customElevation);
245 expect(materialWidget.shadowColor, shadowColor);
246 expect(materialWidget.surfaceTintColor, surfaceTintColor);
247 });
248
249 testWidgets('Custom Title Text Style', (WidgetTester tester) async {
250 const String titleText = 'Title';
251 const TextStyle titleTextStyle = TextStyle(color: Colors.pink);
252 const AlertDialog dialog = AlertDialog(
253 title: Text(titleText),
254 titleTextStyle: titleTextStyle,
255 actions: <Widget>[],
256 );
257 await tester.pumpWidget(_buildAppWithDialog(dialog));
258
259 await tester.tap(find.text('X'));
260 await tester.pumpAndSettle();
261
262 final RenderParagraph title = _getTextRenderObjectFromDialog(tester, titleText);
263 expect(title.text.style, titleTextStyle);
264 });
265
266 testWidgets('Custom Content Text Style', (WidgetTester tester) async {
267 const String contentText = 'Content';
268 const TextStyle contentTextStyle = TextStyle(color: Colors.pink);
269 const AlertDialog dialog = AlertDialog(
270 content: Text(contentText),
271 contentTextStyle: contentTextStyle,
272 actions: <Widget>[],
273 );
274 await tester.pumpWidget(_buildAppWithDialog(dialog));
275
276 await tester.tap(find.text('X'));
277 await tester.pumpAndSettle();
278
279 final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText);
280 expect(content.text.style, contentTextStyle);
281 });
282
283 testWidgets('AlertDialog custom clipBehavior', (WidgetTester tester) async {
284 const AlertDialog dialog = AlertDialog(actions: <Widget>[], clipBehavior: Clip.antiAlias);
285 await tester.pumpWidget(_buildAppWithDialog(dialog));
286
287 await tester.tap(find.text('X'));
288 await tester.pumpAndSettle();
289
290 final Material materialWidget = _getMaterialFromDialog(tester);
291 expect(materialWidget.clipBehavior, Clip.antiAlias);
292 });
293
294 testWidgets('SimpleDialog custom clipBehavior', (WidgetTester tester) async {
295 const SimpleDialog dialog = SimpleDialog(clipBehavior: Clip.antiAlias, children: <Widget>[]);
296 await tester.pumpWidget(_buildAppWithDialog(dialog));
297
298 await tester.tap(find.text('X'));
299 await tester.pumpAndSettle();
300
301 final Material materialWidget = _getMaterialFromDialog(tester);
302 expect(materialWidget.clipBehavior, Clip.antiAlias);
303 });
304
305 testWidgets('Custom dialog shape', (WidgetTester tester) async {
306 const RoundedRectangleBorder customBorder = RoundedRectangleBorder(
307 borderRadius: BorderRadius.all(Radius.circular(16.0)),
308 );
309 const AlertDialog dialog = AlertDialog(actions: <Widget>[], shape: customBorder);
310 await tester.pumpWidget(_buildAppWithDialog(dialog));
311
312 await tester.tap(find.text('X'));
313 await tester.pumpAndSettle();
314
315 final Material materialWidget = _getMaterialFromDialog(tester);
316 expect(materialWidget.shape, customBorder);
317 });
318
319 testWidgets('Null dialog shape', (WidgetTester tester) async {
320 final ThemeData theme = ThemeData();
321 const AlertDialog dialog = AlertDialog(actions: <Widget>[]);
322 await tester.pumpWidget(_buildAppWithDialog(dialog, theme: theme));
323
324 await tester.tap(find.text('X'));
325 await tester.pumpAndSettle();
326
327 final Material materialWidget = _getMaterialFromDialog(tester);
328 expect(
329 materialWidget.shape,
330 theme.useMaterial3 ? _defaultM3DialogShape : _defaultM2DialogShape,
331 );
332 });
333
334 testWidgets('Rectangular dialog shape', (WidgetTester tester) async {
335 const ShapeBorder customBorder = Border();
336 const AlertDialog dialog = AlertDialog(actions: <Widget>[], shape: customBorder);
337 await tester.pumpWidget(_buildAppWithDialog(dialog));
338
339 await tester.tap(find.text('X'));
340 await tester.pumpAndSettle();
341
342 final Material materialWidget = _getMaterialFromDialog(tester);
343 expect(materialWidget.shape, customBorder);
344 });
345
346 testWidgets('Custom dialog alignment', (WidgetTester tester) async {
347 const AlertDialog dialog = AlertDialog(actions: <Widget>[], alignment: Alignment.bottomLeft);
348 await tester.pumpWidget(_buildAppWithDialog(dialog));
349
350 await tester.tap(find.text('X'));
351 await tester.pumpAndSettle();
352
353 final Offset bottomLeft = tester.getBottomLeft(
354 find.descendant(of: find.byType(Dialog), matching: find.byType(Material)),
355 );
356 expect(bottomLeft.dx, 40.0);
357 expect(bottomLeft.dy, 576.0);
358 });
359
360 testWidgets('Simple dialog control test', (WidgetTester tester) async {
361 await tester.pumpWidget(
362 const MaterialApp(
363 home: Material(
364 child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))),
365 ),
366 ),
367 );
368
369 final BuildContext context = tester.element(find.text('Go'));
370
371 final Future<int?> result = showDialog<int>(
372 context: context,
373 builder: (BuildContext context) {
374 return SimpleDialog(
375 title: const Text('Title'),
376 children: <Widget>[
377 SimpleDialogOption(
378 onPressed: () {
379 Navigator.pop(context, 42);
380 },
381 child: const Text('First option'),
382 ),
383 const SimpleDialogOption(child: Text('Second option')),
384 ],
385 );
386 },
387 );
388
389 await tester.pumpAndSettle(const Duration(seconds: 1));
390 expect(find.text('Title'), findsOneWidget);
391 await tester.tap(find.text('First option'));
392
393 expect(await result, equals(42));
394 });
395
396 testWidgets('Can show dialog using navigator global key', (WidgetTester tester) async {
397 final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
398 await tester.pumpWidget(
399 MaterialApp(
400 navigatorKey: navigator,
401 home: const Material(child: Center(child: Text('Go'))),
402 ),
403 );
404
405 final Future<int?> result = showDialog<int>(
406 context: navigator.currentContext!,
407 builder: (BuildContext context) {
408 return SimpleDialog(
409 title: const Text('Title'),
410 children: <Widget>[
411 SimpleDialogOption(
412 onPressed: () {
413 Navigator.pop(context, 42);
414 },
415 child: const Text('First option'),
416 ),
417 const SimpleDialogOption(child: Text('Second option')),
418 ],
419 );
420 },
421 );
422
423 await tester.pumpAndSettle(const Duration(seconds: 1));
424 expect(find.text('Title'), findsOneWidget);
425 await tester.tap(find.text('First option'));
426
427 expect(await result, equals(42));
428 });
429
430 testWidgets('Custom padding on SimpleDialogOption', (WidgetTester tester) async {
431 const EdgeInsets customPadding = EdgeInsets.fromLTRB(4, 10, 8, 6);
432 final SimpleDialog dialog = SimpleDialog(
433 title: const Text('Title'),
434 children: <Widget>[
435 SimpleDialogOption(
436 onPressed: () {},
437 padding: customPadding,
438 child: const Text('First option'),
439 ),
440 ],
441 );
442
443 await tester.pumpWidget(_buildAppWithDialog(dialog));
444 await tester.tap(find.text('X'));
445 await tester.pumpAndSettle();
446
447 final Rect dialogRect = tester.getRect(find.byType(SimpleDialogOption));
448 final Rect textRect = tester.getRect(find.text('First option'));
449
450 expect(textRect.left, dialogRect.left + customPadding.left);
451 expect(textRect.top, dialogRect.top + customPadding.top);
452 expect(textRect.right, dialogRect.right - customPadding.right);
453 expect(textRect.bottom, dialogRect.bottom - customPadding.bottom);
454 });
455
456 testWidgets('Barrier dismissible', (WidgetTester tester) async {
457 await tester.pumpWidget(
458 const MaterialApp(
459 home: Material(
460 child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))),
461 ),
462 ),
463 );
464
465 final BuildContext context = tester.element(find.text('Go'));
466
467 showDialog<void>(
468 context: context,
469 builder: (BuildContext context) {
470 return Container(
471 width: 100.0,
472 height: 100.0,
473 alignment: Alignment.center,
474 child: const Text('Dialog1'),
475 );
476 },
477 );
478
479 await tester.pumpAndSettle(const Duration(seconds: 1));
480 expect(find.text('Dialog1'), findsOneWidget);
481
482 // Tap on the barrier.
483 await tester.tapAt(const Offset(10.0, 10.0));
484
485 await tester.pumpAndSettle(const Duration(seconds: 1));
486 expect(find.text('Dialog1'), findsNothing);
487
488 showDialog<void>(
489 context: context,
490 barrierDismissible: false,
491 builder: (BuildContext context) {
492 return Container(
493 width: 100.0,
494 height: 100.0,
495 alignment: Alignment.center,
496 child: const Text('Dialog2'),
497 );
498 },
499 );
500
501 await tester.pumpAndSettle(const Duration(seconds: 1));
502 expect(find.text('Dialog2'), findsOneWidget);
503
504 // Tap on the barrier, which shouldn't do anything this time.
505 await tester.tapAt(const Offset(10.0, 10.0));
506
507 await tester.pumpAndSettle(const Duration(seconds: 1));
508 expect(find.text('Dialog2'), findsOneWidget);
509 });
510
511 testWidgets('Barrier color', (WidgetTester tester) async {
512 await tester.pumpWidget(const MaterialApp(home: Center(child: Text('Test'))));
513 final BuildContext context = tester.element(find.text('Test'));
514
515 // Test default barrier color
516 showDialog<void>(
517 context: context,
518 builder: (BuildContext context) {
519 return const Text('Dialog');
520 },
521 );
522 await tester.pumpAndSettle();
523 expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.black54);
524
525 // Dismiss it and test a custom barrier color
526 await tester.tapAt(const Offset(10.0, 10.0));
527 showDialog<void>(
528 context: context,
529 builder: (BuildContext context) {
530 return const Text('Dialog');
531 },
532 barrierColor: Colors.pink,
533 );
534 await tester.pumpAndSettle();
535 expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.pink);
536 });
537
538 testWidgets('Dialog hides underlying semantics tree', (WidgetTester tester) async {
539 final SemanticsTester semantics = SemanticsTester(tester);
540 const String buttonText = 'A button covered by dialog overlay';
541 await tester.pumpWidget(
542 const MaterialApp(
543 home: Material(
544 child: Center(child: ElevatedButton(onPressed: null, child: Text(buttonText))),
545 ),
546 ),
547 );
548
549 expect(semantics, includesNodeWith(label: buttonText));
550
551 final BuildContext context = tester.element(find.text(buttonText));
552
553 const String alertText = 'A button in an overlay alert';
554 showDialog<void>(
555 context: context,
556 builder: (BuildContext context) {
557 return const AlertDialog(title: Text(alertText));
558 },
559 );
560
561 await tester.pumpAndSettle(const Duration(seconds: 1));
562
563 expect(semantics, includesNodeWith(label: alertText));
564 expect(semantics, isNot(includesNodeWith(label: buttonText)));
565
566 semantics.dispose();
567 });
568
569 testWidgets('AlertDialog.actionsPadding defaults', (WidgetTester tester) async {
570 final AlertDialog dialog = AlertDialog(
571 title: const Text('title'),
572 content: const Text('content'),
573 actions: <Widget>[ElevatedButton(onPressed: () {}, child: const Text('button'))],
574 );
575
576 await tester.pumpWidget(_buildAppWithDialog(dialog));
577
578 await tester.tap(find.text('X'));
579 await tester.pumpAndSettle();
580
581 // The [AlertDialog] is the entire screen, since it also contains the scrim.
582 // The first [Material] child of [AlertDialog] is the actual dialog
583 // itself.
584 final Size dialogSize = tester.getSize(
585 find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)).first,
586 );
587 final Size actionsSize = tester.getSize(_findOverflowBar());
588
589 expect(actionsSize.width, dialogSize.width);
590 });
591
592 testWidgets('AlertDialog.actionsPadding surrounds actions with padding', (
593 WidgetTester tester,
594 ) async {
595 final AlertDialog dialog = AlertDialog(
596 title: const Text('title'),
597 content: const Text('content'),
598 actions: <Widget>[ElevatedButton(onPressed: () {}, child: const Text('button'))],
599 // The OverflowBar is inset by the buttonPadding/2 + actionsPadding
600 buttonPadding: EdgeInsets.zero,
601 actionsPadding: const EdgeInsets.all(30.0), // custom padding value
602 );
603
604 await tester.pumpWidget(_buildAppWithDialog(dialog));
605
606 await tester.tap(find.text('X'));
607 await tester.pumpAndSettle();
608
609 // The [AlertDialog] is the entire screen, since it also contains the scrim.
610 // The first [Material] child of [AlertDialog] is the actual dialog
611 // itself.
612 final Size dialogSize = tester.getSize(
613 find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)).first,
614 );
615 final Size actionsSize = tester.getSize(find.byType(OverflowBar));
616
617 expect(actionsSize.width, dialogSize.width - (30.0 * 2));
618 });
619
620 testWidgets('Material2 - AlertDialog.buttonPadding defaults', (WidgetTester tester) async {
621 final GlobalKey key1 = GlobalKey();
622 final GlobalKey key2 = GlobalKey();
623
624 final AlertDialog dialog = AlertDialog(
625 title: const Text('title'),
626 content: const Text('content'),
627 actions: <Widget>[
628 ElevatedButton(key: key1, onPressed: () {}, child: const Text('button 1')),
629 ElevatedButton(key: key2, onPressed: () {}, child: const Text('button 2')),
630 ],
631 );
632
633 await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material2Theme));
634
635 await tester.tap(find.text('X'));
636 await tester.pumpAndSettle();
637
638 // Padding between both buttons
639 expect(
640 tester.getBottomLeft(find.byKey(key2)).dx,
641 tester.getBottomRight(find.byKey(key1)).dx + 8.0,
642 );
643
644 // Padding between button and edges of the button bar
645 // First button
646 expect(
647 tester.getTopRight(find.byKey(key1)).dy,
648 tester.getTopRight(_findOverflowBar()).dy + 8.0,
649 ); // top
650 expect(
651 tester.getBottomRight(find.byKey(key1)).dy,
652 tester.getBottomRight(_findOverflowBar()).dy - 8.0,
653 ); // bottom
654
655 // Second button
656 expect(
657 tester.getTopRight(find.byKey(key2)).dy,
658 tester.getTopRight(_findOverflowBar()).dy + 8.0,
659 ); // top
660 expect(
661 tester.getBottomRight(find.byKey(key2)).dy,
662 tester.getBottomRight(_findOverflowBar()).dy - 8.0,
663 ); // bottom
664 expect(
665 tester.getBottomRight(find.byKey(key2)).dx,
666 tester.getBottomRight(_findOverflowBar()).dx - 8.0,
667 ); // right
668 });
669
670 testWidgets('Material3 - AlertDialog.buttonPadding defaults', (WidgetTester tester) async {
671 final GlobalKey key1 = GlobalKey();
672 final GlobalKey key2 = GlobalKey();
673
674 final AlertDialog dialog = AlertDialog(
675 title: const Text('title'),
676 content: const Text('content'),
677 actions: <Widget>[
678 ElevatedButton(key: key1, onPressed: () {}, child: const Text('button 1')),
679 ElevatedButton(key: key2, onPressed: () {}, child: const Text('button 2')),
680 ],
681 );
682
683 await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material3Theme));
684
685 await tester.tap(find.text('X'));
686 await tester.pumpAndSettle();
687
688 // Padding between both buttons
689 expect(
690 tester.getBottomLeft(find.byKey(key2)).dx,
691 tester.getBottomRight(find.byKey(key1)).dx + 8.0,
692 );
693
694 // Padding between button and edges of the button bar
695 // First button
696 expect(
697 tester.getTopRight(find.byKey(key1)).dy,
698 tester.getTopRight(_findOverflowBar()).dy,
699 ); // top
700 expect(
701 tester.getBottomRight(find.byKey(key1)).dy,
702 tester.getBottomRight(_findOverflowBar()).dy - 24.0,
703 ); // bottom
704
705 // // Second button
706 expect(
707 tester.getTopRight(find.byKey(key2)).dy,
708 tester.getTopRight(_findOverflowBar()).dy,
709 ); // top
710 expect(
711 tester.getBottomRight(find.byKey(key2)).dy,
712 tester.getBottomRight(_findOverflowBar()).dy - 24.0,
713 ); // bottom
714 expect(
715 tester.getBottomRight(find.byKey(key2)).dx,
716 tester.getBottomRight(_findOverflowBar()).dx - 24.0,
717 ); // right
718 });
719
720 testWidgets('AlertDialog.buttonPadding custom values', (WidgetTester tester) async {
721 final GlobalKey key1 = GlobalKey();
722 final GlobalKey key2 = GlobalKey();
723
724 final AlertDialog dialog = AlertDialog(
725 title: const Text('title'),
726 content: const Text('content'),
727 actions: <Widget>[
728 ElevatedButton(key: key1, onPressed: () {}, child: const Text('button 1')),
729 ElevatedButton(key: key2, onPressed: () {}, child: const Text('button 2')),
730 ],
731 buttonPadding: const EdgeInsets.only(left: 10.0, right: 20.0),
732 );
733
734 await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(useMaterial3: false)));
735
736 await tester.tap(find.text('X'));
737 await tester.pumpAndSettle();
738
739 // Padding between both buttons
740 expect(
741 tester.getBottomLeft(find.byKey(key2)).dx,
742 tester.getBottomRight(find.byKey(key1)).dx + ((10.0 + 20.0) / 2),
743 );
744
745 // Padding between button and edges of the button bar
746 // First button
747 expect(
748 tester.getTopRight(find.byKey(key1)).dy,
749 tester.getTopRight(_findOverflowBar()).dy + ((10.0 + 20.0) / 2),
750 ); // top
751 expect(
752 tester.getBottomRight(find.byKey(key1)).dy,
753 tester.getBottomRight(_findOverflowBar()).dy - ((10.0 + 20.0) / 2),
754 ); // bottom
755
756 // Second button
757 expect(
758 tester.getTopRight(find.byKey(key2)).dy,
759 tester.getTopRight(_findOverflowBar()).dy + ((10.0 + 20.0) / 2),
760 ); // top
761 expect(
762 tester.getBottomRight(find.byKey(key2)).dy,
763 tester.getBottomRight(_findOverflowBar()).dy - ((10.0 + 20.0) / 2),
764 ); // bottom
765 expect(
766 tester.getBottomRight(find.byKey(key2)).dx,
767 tester.getBottomRight(_findOverflowBar()).dx - ((10.0 + 20.0) / 2),
768 ); // right
769 });
770
771 group('Dialog children padding is correct', () {
772 final List<double> textScaleFactors = <double>[0.5, 1.0, 1.5, 2.0, 3.0];
773 final Map<double, double> paddingScaleFactors = <double, double>{
774 0.5: 1.0,
775 1.0: 1.0,
776 1.5: 2.0 / 3.0,
777 2.0: 1.0 / 3.0,
778 3.0: 1.0 / 3.0,
779 };
780
781 final GlobalKey iconKey = GlobalKey();
782 final GlobalKey titleKey = GlobalKey();
783 final GlobalKey contentKey = GlobalKey();
784 final GlobalKey childrenKey = GlobalKey();
785
786 final Finder dialogFinder = find
787 .descendant(of: find.byType(Dialog), matching: find.byType(Material))
788 .first;
789 final Finder iconFinder = find.byKey(iconKey);
790 final Finder titleFinder = find.byKey(titleKey);
791 final Finder contentFinder = find.byKey(contentKey);
792 final Finder actionsFinder = _findOverflowBar();
793 final Finder childrenFinder = find.byKey(childrenKey);
794
795 Future<void> openDialog(
796 WidgetTester tester,
797 Widget dialog,
798 double textScaleFactor, {
799 bool isM3 = false,
800 }) async {
801 await tester.pumpWidget(
802 _buildAppWithDialog(
803 dialog,
804 textScaleFactor: textScaleFactor,
805 theme: ThemeData(useMaterial3: isM3),
806 ),
807 );
808
809 await tester.tap(find.text('X'));
810 await tester.pumpAndSettle();
811 }
812
813 void expectLeftEdgePadding(
814 WidgetTester tester, {
815 required Finder finder,
816 required double textScaleFactor,
817 required double unscaledValue,
818 }) {
819 expect(
820 tester.getTopLeft(dialogFinder).dx,
821 moreOrLessEquals(
822 tester.getTopLeft(finder).dx - unscaledValue * paddingScaleFactors[textScaleFactor]!,
823 ),
824 );
825 expect(
826 tester.getBottomLeft(dialogFinder).dx,
827 moreOrLessEquals(
828 tester.getBottomLeft(finder).dx - unscaledValue * paddingScaleFactors[textScaleFactor]!,
829 ),
830 );
831 }
832
833 void expectRightEdgePadding(
834 WidgetTester tester, {
835 required Finder finder,
836 required double textScaleFactor,
837 required double unscaledValue,
838 }) {
839 expect(
840 tester.getTopRight(dialogFinder).dx,
841 moreOrLessEquals(
842 tester.getTopRight(finder).dx + unscaledValue * paddingScaleFactors[textScaleFactor]!,
843 ),
844 );
845 expect(
846 tester.getBottomRight(dialogFinder).dx,
847 moreOrLessEquals(
848 tester.getBottomRight(finder).dx + unscaledValue * paddingScaleFactors[textScaleFactor]!,
849 ),
850 );
851 }
852
853 void expectTopEdgePadding(
854 WidgetTester tester, {
855 required Finder finder,
856 required double textScaleFactor,
857 required double unscaledValue,
858 }) {
859 expect(
860 tester.getTopLeft(dialogFinder).dy,
861 moreOrLessEquals(
862 tester.getTopLeft(finder).dy - unscaledValue * paddingScaleFactors[textScaleFactor]!,
863 ),
864 );
865 expect(
866 tester.getTopRight(dialogFinder).dy,
867 moreOrLessEquals(
868 tester.getTopRight(finder).dy - unscaledValue * paddingScaleFactors[textScaleFactor]!,
869 ),
870 );
871 }
872
873 void expectBottomEdgePadding(
874 WidgetTester tester, {
875 required Finder finder,
876 required double textScaleFactor,
877 required double unscaledValue,
878 }) {
879 expect(
880 tester.getBottomLeft(dialogFinder).dy,
881 moreOrLessEquals(
882 tester.getBottomRight(finder).dy + unscaledValue * paddingScaleFactors[textScaleFactor]!,
883 ),
884 );
885 expect(
886 tester.getBottomRight(dialogFinder).dy,
887 moreOrLessEquals(
888 tester.getBottomRight(finder).dy + unscaledValue * paddingScaleFactors[textScaleFactor]!,
889 ),
890 );
891 }
892
893 void expectVerticalInnerPadding(
894 WidgetTester tester, {
895 required Finder top,
896 required Finder bottom,
897 required double value,
898 }) {
899 expect(tester.getBottomLeft(top).dy, tester.getTopLeft(bottom).dy - value);
900 expect(tester.getBottomRight(top).dy, tester.getTopRight(bottom).dy - value);
901 }
902
903 final Widget icon = Icon(Icons.ac_unit, key: iconKey);
904 final Widget title = Text('title', key: titleKey);
905 final Widget content = Text('content', key: contentKey);
906 final List<Widget> actions = <Widget>[
907 ElevatedButton(onPressed: () {}, child: const Text('button')),
908 ];
909 final List<Widget> children = <Widget>[
910 SimpleDialogOption(key: childrenKey, child: const Text('child'), onPressed: () {}),
911 ];
912
913 for (final double textScaleFactor in textScaleFactors) {
914 testWidgets(
915 'AlertDialog padding is correct when only icon and actions are specified [textScaleFactor]=$textScaleFactor',
916 (WidgetTester tester) async {
917 final AlertDialog dialog = AlertDialog(icon: icon, actions: actions);
918
919 await openDialog(tester, dialog, textScaleFactor);
920
921 expectTopEdgePadding(
922 tester,
923 finder: iconFinder,
924 textScaleFactor: textScaleFactor,
925 unscaledValue: 24.0,
926 );
927 expectLeftEdgePadding(
928 tester,
929 finder: iconFinder,
930 textScaleFactor: textScaleFactor,
931 unscaledValue: 24.0,
932 );
933 expectRightEdgePadding(
934 tester,
935 finder: iconFinder,
936 textScaleFactor: textScaleFactor,
937 unscaledValue: 24.0,
938 );
939 expectVerticalInnerPadding(tester, top: iconFinder, bottom: actionsFinder, value: 24.0);
940 expectLeftEdgePadding(
941 tester,
942 finder: actionsFinder,
943 textScaleFactor: textScaleFactor,
944 unscaledValue: 0.0,
945 );
946 expectRightEdgePadding(
947 tester,
948 finder: actionsFinder,
949 textScaleFactor: textScaleFactor,
950 unscaledValue: 0.0,
951 );
952 expectBottomEdgePadding(
953 tester,
954 finder: actionsFinder,
955 textScaleFactor: textScaleFactor,
956 unscaledValue: 0.0,
957 );
958 },
959 );
960
961 testWidgets(
962 'AlertDialog padding is correct when only icon, title and actions are specified [textScaleFactor]=$textScaleFactor',
963 (WidgetTester tester) async {
964 final AlertDialog dialog = AlertDialog(icon: icon, title: title, actions: actions);
965
966 await openDialog(tester, dialog, textScaleFactor);
967
968 expectTopEdgePadding(
969 tester,
970 finder: iconFinder,
971 textScaleFactor: textScaleFactor,
972 unscaledValue: 24.0,
973 );
974 expectLeftEdgePadding(
975 tester,
976 finder: iconFinder,
977 textScaleFactor: textScaleFactor,
978 unscaledValue: 24.0,
979 );
980 expectRightEdgePadding(
981 tester,
982 finder: iconFinder,
983 textScaleFactor: textScaleFactor,
984 unscaledValue: 24.0,
985 );
986 expectVerticalInnerPadding(tester, top: iconFinder, bottom: titleFinder, value: 16.0);
987 expectLeftEdgePadding(
988 tester,
989 finder: titleFinder,
990 textScaleFactor: textScaleFactor,
991 unscaledValue: 24.0,
992 );
993 expectRightEdgePadding(
994 tester,
995 finder: titleFinder,
996 textScaleFactor: textScaleFactor,
997 unscaledValue: 24.0,
998 );
999 expectVerticalInnerPadding(tester, top: titleFinder, bottom: actionsFinder, value: 20.0);
1000 expectLeftEdgePadding(
1001 tester,
1002 finder: actionsFinder,
1003 textScaleFactor: textScaleFactor,
1004 unscaledValue: 0.0,
1005 );
1006 expectRightEdgePadding(
1007 tester,
1008 finder: actionsFinder,
1009 textScaleFactor: textScaleFactor,
1010 unscaledValue: 0.0,
1011 );
1012 expectBottomEdgePadding(
1013 tester,
1014 finder: actionsFinder,
1015 textScaleFactor: textScaleFactor,
1016 unscaledValue: 0.0,
1017 );
1018 },
1019 );
1020
1021 for (final bool isM3 in <bool>[true, false]) {
1022 testWidgets(
1023 'AlertDialog padding is correct when only icon, content and actions are specified [textScaleFactor]=$textScaleFactor [isM3]=$isM3',
1024 (WidgetTester tester) async {
1025 final AlertDialog dialog = AlertDialog(icon: icon, content: content, actions: actions);
1026
1027 await openDialog(tester, dialog, textScaleFactor, isM3: isM3);
1028
1029 expectTopEdgePadding(
1030 tester,
1031 finder: iconFinder,
1032 textScaleFactor: textScaleFactor,
1033 unscaledValue: 24.0,
1034 );
1035 expectLeftEdgePadding(
1036 tester,
1037 finder: iconFinder,
1038 textScaleFactor: textScaleFactor,
1039 unscaledValue: 24.0,
1040 );
1041 expectRightEdgePadding(
1042 tester,
1043 finder: iconFinder,
1044 textScaleFactor: textScaleFactor,
1045 unscaledValue: 24.0,
1046 );
1047 expectVerticalInnerPadding(
1048 tester,
1049 top: iconFinder,
1050 bottom: contentFinder,
1051 value: isM3 ? 16.0 : 20.0,
1052 );
1053 expectLeftEdgePadding(
1054 tester,
1055 finder: contentFinder,
1056 textScaleFactor: textScaleFactor,
1057 unscaledValue: 24.0,
1058 );
1059 expectRightEdgePadding(
1060 tester,
1061 finder: contentFinder,
1062 textScaleFactor: textScaleFactor,
1063 unscaledValue: 24.0,
1064 );
1065 expectVerticalInnerPadding(
1066 tester,
1067 top: contentFinder,
1068 bottom: actionsFinder,
1069 value: 24.0,
1070 );
1071 expectLeftEdgePadding(
1072 tester,
1073 finder: actionsFinder,
1074 textScaleFactor: textScaleFactor,
1075 unscaledValue: 0.0,
1076 );
1077 expectRightEdgePadding(
1078 tester,
1079 finder: actionsFinder,
1080 textScaleFactor: textScaleFactor,
1081 unscaledValue: 0.0,
1082 );
1083 expectBottomEdgePadding(
1084 tester,
1085 finder: actionsFinder,
1086 textScaleFactor: textScaleFactor,
1087 unscaledValue: 0.0,
1088 );
1089 },
1090 );
1091 }
1092
1093 testWidgets(
1094 'AlertDialog padding is correct when only title and actions are specified [textScaleFactor]=$textScaleFactor',
1095 (WidgetTester tester) async {
1096 final AlertDialog dialog = AlertDialog(title: title, actions: actions);
1097
1098 await openDialog(tester, dialog, textScaleFactor);
1099
1100 expectTopEdgePadding(
1101 tester,
1102 finder: titleFinder,
1103 textScaleFactor: textScaleFactor,
1104 unscaledValue: 24.0,
1105 );
1106 expectLeftEdgePadding(
1107 tester,
1108 finder: titleFinder,
1109 textScaleFactor: textScaleFactor,
1110 unscaledValue: 24.0,
1111 );
1112 expectRightEdgePadding(
1113 tester,
1114 finder: titleFinder,
1115 textScaleFactor: textScaleFactor,
1116 unscaledValue: 24.0,
1117 );
1118 expectVerticalInnerPadding(tester, top: titleFinder, bottom: actionsFinder, value: 20.0);
1119 expectLeftEdgePadding(
1120 tester,
1121 finder: actionsFinder,
1122 textScaleFactor: textScaleFactor,
1123 unscaledValue: 0.0,
1124 );
1125 expectRightEdgePadding(
1126 tester,
1127 finder: actionsFinder,
1128 textScaleFactor: textScaleFactor,
1129 unscaledValue: 0.0,
1130 );
1131 expectBottomEdgePadding(
1132 tester,
1133 finder: actionsFinder,
1134 textScaleFactor: textScaleFactor,
1135 unscaledValue: 0.0,
1136 );
1137 },
1138 );
1139
1140 testWidgets(
1141 'AlertDialog padding is correct when only content and actions are specified [textScaleFactor]=$textScaleFactor',
1142 (WidgetTester tester) async {
1143 final AlertDialog dialog = AlertDialog(content: content, actions: actions);
1144
1145 await openDialog(tester, dialog, textScaleFactor);
1146
1147 expectTopEdgePadding(
1148 tester,
1149 finder: contentFinder,
1150 textScaleFactor: textScaleFactor,
1151 unscaledValue: 20.0,
1152 );
1153 expectLeftEdgePadding(
1154 tester,
1155 finder: contentFinder,
1156 textScaleFactor: textScaleFactor,
1157 unscaledValue: 24.0,
1158 );
1159 expectRightEdgePadding(
1160 tester,
1161 finder: contentFinder,
1162 textScaleFactor: textScaleFactor,
1163 unscaledValue: 24.0,
1164 );
1165 expectVerticalInnerPadding(
1166 tester,
1167 top: contentFinder,
1168 bottom: actionsFinder,
1169 value: 24.0,
1170 );
1171 expectLeftEdgePadding(
1172 tester,
1173 finder: actionsFinder,
1174 textScaleFactor: textScaleFactor,
1175 unscaledValue: 0.0,
1176 );
1177 expectRightEdgePadding(
1178 tester,
1179 finder: actionsFinder,
1180 textScaleFactor: textScaleFactor,
1181 unscaledValue: 0.0,
1182 );
1183 expectBottomEdgePadding(
1184 tester,
1185 finder: actionsFinder,
1186 textScaleFactor: textScaleFactor,
1187 unscaledValue: 0.0,
1188 );
1189 },
1190 );
1191
1192 testWidgets(
1193 'AlertDialog padding is correct when title, content, and actions are specified [textScaleFactor]=$textScaleFactor',
1194 (WidgetTester tester) async {
1195 final AlertDialog dialog = AlertDialog(title: title, content: content, actions: actions);
1196
1197 await openDialog(tester, dialog, textScaleFactor);
1198
1199 expectTopEdgePadding(
1200 tester,
1201 finder: titleFinder,
1202 textScaleFactor: textScaleFactor,
1203 unscaledValue: 24.0,
1204 );
1205 expectLeftEdgePadding(
1206 tester,
1207 finder: titleFinder,
1208 textScaleFactor: textScaleFactor,
1209 unscaledValue: 24.0,
1210 );
1211 expectRightEdgePadding(
1212 tester,
1213 finder: titleFinder,
1214 textScaleFactor: textScaleFactor,
1215 unscaledValue: 24.0,
1216 );
1217 expectVerticalInnerPadding(tester, top: titleFinder, bottom: contentFinder, value: 20.0);
1218 expectLeftEdgePadding(
1219 tester,
1220 finder: contentFinder,
1221 textScaleFactor: textScaleFactor,
1222 unscaledValue: 24.0,
1223 );
1224 expectRightEdgePadding(
1225 tester,
1226 finder: contentFinder,
1227 textScaleFactor: textScaleFactor,
1228 unscaledValue: 24.0,
1229 );
1230 expectVerticalInnerPadding(
1231 tester,
1232 top: contentFinder,
1233 bottom: actionsFinder,
1234 value: 24.0,
1235 );
1236 expectLeftEdgePadding(
1237 tester,
1238 finder: actionsFinder,
1239 textScaleFactor: textScaleFactor,
1240 unscaledValue: 0.0,
1241 );
1242 expectRightEdgePadding(
1243 tester,
1244 finder: actionsFinder,
1245 textScaleFactor: textScaleFactor,
1246 unscaledValue: 0.0,
1247 );
1248 expectBottomEdgePadding(
1249 tester,
1250 finder: actionsFinder,
1251 textScaleFactor: textScaleFactor,
1252 unscaledValue: 0.0,
1253 );
1254 },
1255 );
1256
1257 testWidgets(
1258 'SimpleDialog padding is correct when only children are specified [textScaleFactor]=$textScaleFactor',
1259 (WidgetTester tester) async {
1260 final SimpleDialog dialog = SimpleDialog(children: children);
1261
1262 await openDialog(tester, dialog, textScaleFactor);
1263
1264 expectTopEdgePadding(
1265 tester,
1266 finder: childrenFinder,
1267 textScaleFactor: textScaleFactor,
1268 unscaledValue: 12.0,
1269 );
1270 expectLeftEdgePadding(
1271 tester,
1272 finder: childrenFinder,
1273 textScaleFactor: textScaleFactor,
1274 unscaledValue: 0.0,
1275 );
1276 expectRightEdgePadding(
1277 tester,
1278 finder: childrenFinder,
1279 textScaleFactor: textScaleFactor,
1280 unscaledValue: 0.0,
1281 );
1282 expectBottomEdgePadding(
1283 tester,
1284 finder: childrenFinder,
1285 textScaleFactor: textScaleFactor,
1286 unscaledValue: 16.0,
1287 );
1288 },
1289 );
1290
1291 testWidgets(
1292 'SimpleDialog padding is correct when title and children are specified [textScaleFactor]=$textScaleFactor',
1293 (WidgetTester tester) async {
1294 final SimpleDialog dialog = SimpleDialog(title: title, children: children);
1295
1296 await openDialog(tester, dialog, textScaleFactor);
1297
1298 expectTopEdgePadding(
1299 tester,
1300 finder: titleFinder,
1301 textScaleFactor: textScaleFactor,
1302 unscaledValue: 24.0,
1303 );
1304 expectLeftEdgePadding(
1305 tester,
1306 finder: titleFinder,
1307 textScaleFactor: textScaleFactor,
1308 unscaledValue: 24.0,
1309 );
1310 expectRightEdgePadding(
1311 tester,
1312 finder: titleFinder,
1313 textScaleFactor: textScaleFactor,
1314 unscaledValue: 24.0,
1315 );
1316 expectVerticalInnerPadding(tester, top: titleFinder, bottom: childrenFinder, value: 12.0);
1317 expectLeftEdgePadding(
1318 tester,
1319 finder: childrenFinder,
1320 textScaleFactor: textScaleFactor,
1321 unscaledValue: 0.0,
1322 );
1323 expectRightEdgePadding(
1324 tester,
1325 finder: childrenFinder,
1326 textScaleFactor: textScaleFactor,
1327 unscaledValue: 0.0,
1328 );
1329 expectBottomEdgePadding(
1330 tester,
1331 finder: childrenFinder,
1332 textScaleFactor: textScaleFactor,
1333 unscaledValue: 16.0,
1334 );
1335 },
1336 );
1337 }
1338 });
1339
1340 testWidgets('Dialogs can set the vertical direction of overflowing actions', (
1341 WidgetTester tester,
1342 ) async {
1343 final GlobalKey key1 = GlobalKey();
1344 final GlobalKey key2 = GlobalKey();
1345
1346 final AlertDialog dialog = AlertDialog(
1347 title: const Text('title'),
1348 content: const Text('content'),
1349 actions: <Widget>[
1350 ElevatedButton(
1351 key: key1,
1352 onPressed: () {},
1353 child: const Text('Looooooooooooooong button 1'),
1354 ),
1355 ElevatedButton(
1356 key: key2,
1357 onPressed: () {},
1358 child: const Text('Looooooooooooooong button 2'),
1359 ),
1360 ],
1361 actionsOverflowDirection: VerticalDirection.up,
1362 );
1363
1364 await tester.pumpWidget(_buildAppWithDialog(dialog));
1365
1366 await tester.tap(find.text('X'));
1367 await tester.pumpAndSettle();
1368
1369 final Rect buttonOneRect = tester.getRect(find.byKey(key1));
1370 final Rect buttonTwoRect = tester.getRect(find.byKey(key2));
1371 // Second [ElevatedButton] should appear above the first.
1372 expect(buttonTwoRect.bottom, lessThanOrEqualTo(buttonOneRect.top));
1373 });
1374
1375 testWidgets('Dialogs have no spacing by default for overflowing actions', (
1376 WidgetTester tester,
1377 ) async {
1378 final GlobalKey key1 = GlobalKey();
1379 final GlobalKey key2 = GlobalKey();
1380
1381 final AlertDialog dialog = AlertDialog(
1382 title: const Text('title'),
1383 content: const Text('content'),
1384 actions: <Widget>[
1385 ElevatedButton(
1386 key: key1,
1387 onPressed: () {},
1388 child: const Text('Looooooooooooooong button 1'),
1389 ),
1390 ElevatedButton(
1391 key: key2,
1392 onPressed: () {},
1393 child: const Text('Looooooooooooooong button 2'),
1394 ),
1395 ],
1396 );
1397
1398 await tester.pumpWidget(_buildAppWithDialog(dialog));
1399
1400 await tester.tap(find.text('X'));
1401 await tester.pumpAndSettle();
1402
1403 final Rect buttonOneRect = tester.getRect(find.byKey(key1));
1404 final Rect buttonTwoRect = tester.getRect(find.byKey(key2));
1405 expect(buttonOneRect.bottom, buttonTwoRect.top);
1406 });
1407
1408 testWidgets('Dialogs can set the button spacing of overflowing actions', (
1409 WidgetTester tester,
1410 ) async {
1411 final GlobalKey key1 = GlobalKey();
1412 final GlobalKey key2 = GlobalKey();
1413
1414 final AlertDialog dialog = AlertDialog(
1415 title: const Text('title'),
1416 content: const Text('content'),
1417 actions: <Widget>[
1418 ElevatedButton(
1419 key: key1,
1420 onPressed: () {},
1421 child: const Text('Looooooooooooooong button 1'),
1422 ),
1423 ElevatedButton(
1424 key: key2,
1425 onPressed: () {},
1426 child: const Text('Looooooooooooooong button 2'),
1427 ),
1428 ],
1429 actionsOverflowButtonSpacing: 10.0,
1430 );
1431
1432 await tester.pumpWidget(_buildAppWithDialog(dialog));
1433
1434 await tester.tap(find.text('X'));
1435 await tester.pumpAndSettle();
1436
1437 final Rect buttonOneRect = tester.getRect(find.byKey(key1));
1438 final Rect buttonTwoRect = tester.getRect(find.byKey(key2));
1439 expect(buttonOneRect.bottom, buttonTwoRect.top - 10.0);
1440 });
1441
1442 testWidgets('Dialogs can set the alignment of the OverflowBar', (WidgetTester tester) async {
1443 final GlobalKey key1 = GlobalKey();
1444 final GlobalKey key2 = GlobalKey();
1445
1446 final AlertDialog dialog = AlertDialog(
1447 title: const Text('title'),
1448 content: const Text('content'),
1449 actions: <Widget>[
1450 ElevatedButton(key: key1, onPressed: () {}, child: const Text('Loooooooooong button 1')),
1451 ElevatedButton(
1452 key: key2,
1453 onPressed: () {},
1454 child: const Text('Loooooooooooooonger button 2'),
1455 ),
1456 ],
1457 actionsOverflowAlignment: OverflowBarAlignment.center,
1458 );
1459
1460 await tester.pumpWidget(_buildAppWithDialog(dialog));
1461
1462 await tester.tap(find.text('X'));
1463 await tester.pumpAndSettle();
1464
1465 final Rect buttonOneRect = tester.getRect(find.byKey(key1));
1466 final Rect buttonTwoRect = tester.getRect(find.byKey(key2));
1467 expect(buttonOneRect.center.dx, buttonTwoRect.center.dx);
1468 });
1469
1470 testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async {
1471 late BuildContext outerContext;
1472 late BuildContext routeContext;
1473 late BuildContext dialogContext;
1474
1475 await tester.pumpWidget(
1476 Localizations(
1477 locale: const Locale('en', 'US'),
1478 delegates: const <LocalizationsDelegate<dynamic>>[
1479 DefaultWidgetsLocalizations.delegate,
1480 DefaultMaterialLocalizations.delegate,
1481 ],
1482 child: MediaQuery(
1483 data: const MediaQueryData(
1484 padding: EdgeInsets.all(50.0),
1485 viewInsets: EdgeInsets.only(left: 25.0, bottom: 75.0),
1486 ),
1487 child: Navigator(
1488 onGenerateRoute: (_) {
1489 return PageRouteBuilder<void>(
1490 pageBuilder:
1491 (
1492 BuildContext context,
1493 Animation<double> animation,
1494 Animation<double> secondaryAnimation,
1495 ) {
1496 outerContext = context;
1497 return Container();
1498 },
1499 );
1500 },
1501 ),
1502 ),
1503 ),
1504 );
1505
1506 showDialog<void>(
1507 context: outerContext,
1508 barrierDismissible: false,
1509 builder: (BuildContext context) {
1510 routeContext = context;
1511 return Dialog(
1512 child: Builder(
1513 builder: (BuildContext context) {
1514 dialogContext = context;
1515 return const Placeholder();
1516 },
1517 ),
1518 );
1519 },
1520 );
1521
1522 await tester.pump();
1523
1524 expect(MediaQuery.of(outerContext).padding, const EdgeInsets.all(50.0));
1525 expect(MediaQuery.of(routeContext).padding, EdgeInsets.zero);
1526 expect(MediaQuery.of(dialogContext).padding, EdgeInsets.zero);
1527 expect(MediaQuery.of(outerContext).viewInsets, const EdgeInsets.only(left: 25.0, bottom: 75.0));
1528 expect(MediaQuery.of(routeContext).viewInsets, const EdgeInsets.only(left: 25.0, bottom: 75.0));
1529 expect(MediaQuery.of(dialogContext).viewInsets, EdgeInsets.zero);
1530 });
1531
1532 testWidgets('Dialog widget insets by viewInsets', (WidgetTester tester) async {
1533 await tester.pumpWidget(
1534 const MediaQuery(
1535 data: MediaQueryData(viewInsets: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0)),
1536 child: Dialog(child: Placeholder()),
1537 ),
1538 );
1539 expect(
1540 tester.getRect(find.byType(Placeholder)),
1541 const Rect.fromLTRB(10.0 + 40.0, 20.0 + 24.0, 800.0 - (40.0 + 30.0), 600.0 - (24.0 + 40.0)),
1542 );
1543 await tester.pumpWidget(
1544 const MediaQuery(
1545 data: MediaQueryData(),
1546 child: Dialog(child: Placeholder()),
1547 ),
1548 );
1549 expect(
1550 // no change because this is an animation
1551 tester.getRect(find.byType(Placeholder)),
1552 const Rect.fromLTRB(10.0 + 40.0, 20.0 + 24.0, 800.0 - (40.0 + 30.0), 600.0 - (24.0 + 40.0)),
1553 );
1554 await tester.pump(const Duration(seconds: 1));
1555 expect(
1556 // animation finished
1557 tester.getRect(find.byType(Placeholder)),
1558 const Rect.fromLTRB(40.0, 24.0, 800.0 - 40.0, 600.0 - 24.0),
1559 );
1560 });
1561
1562 testWidgets('Dialog insetPadding added to outside of dialog', (WidgetTester tester) async {
1563 // The default testing screen (800, 600)
1564 const Rect screenRect = Rect.fromLTRB(0.0, 0.0, 800.0, 600.0);
1565
1566 // Test with no padding.
1567 await tester.pumpWidget(
1568 const MediaQuery(
1569 data: MediaQueryData(),
1570 child: Dialog(insetPadding: EdgeInsets.zero, child: Placeholder()),
1571 ),
1572 );
1573 await tester.pumpAndSettle();
1574 expect(tester.getRect(find.byType(Placeholder)), screenRect);
1575
1576 // Test with an insetPadding.
1577 await tester.pumpWidget(
1578 const MediaQuery(
1579 data: MediaQueryData(),
1580 child: Dialog(
1581 insetPadding: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0),
1582 child: Placeholder(),
1583 ),
1584 ),
1585 );
1586 await tester.pumpAndSettle();
1587 expect(
1588 tester.getRect(find.byType(Placeholder)),
1589 Rect.fromLTRB(
1590 screenRect.left + 10.0,
1591 screenRect.top + 20.0,
1592 screenRect.right - 30.0,
1593 screenRect.bottom - 40.0,
1594 ),
1595 );
1596 });
1597
1598 // Regression test for https://github.com/flutter/flutter/issues/78229.
1599 testWidgets('AlertDialog has correct semantics for content in iOS', (WidgetTester tester) async {
1600 final SemanticsTester semantics = SemanticsTester(tester);
1601
1602 await tester.pumpWidget(
1603 MaterialApp(
1604 theme: ThemeData(platform: TargetPlatform.iOS),
1605 home: const AlertDialog(
1606 title: Text('title'),
1607 content: Column(children: <Widget>[Text('some content'), Text('more content')]),
1608 actions: <Widget>[TextButton(onPressed: null, child: Text('action'))],
1609 ),
1610 ),
1611 );
1612
1613 expect(
1614 semantics,
1615 hasSemantics(
1616 TestSemantics.root(
1617 children: <TestSemantics>[
1618 TestSemantics(
1619 id: 1,
1620 textDirection: TextDirection.ltr,
1621 children: <TestSemantics>[
1622 TestSemantics(
1623 id: 2,
1624 children: <TestSemantics>[
1625 TestSemantics(
1626 id: 3,
1627 flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
1628 children: <TestSemantics>[
1629 TestSemantics(
1630 id: 4,
1631 role: SemanticsRole.alertDialog,
1632 children: <TestSemantics>[
1633 TestSemantics(id: 5, label: 'title', textDirection: TextDirection.ltr),
1634 // The content semantics does not merge into the semantics
1635 // node 4.
1636 TestSemantics(
1637 id: 6,
1638 children: <TestSemantics>[
1639 TestSemantics(
1640 id: 7,
1641 label: 'some content',
1642 textDirection: TextDirection.ltr,
1643 ),
1644 TestSemantics(
1645 id: 8,
1646 label: 'more content',
1647 textDirection: TextDirection.ltr,
1648 ),
1649 ],
1650 ),
1651 TestSemantics(
1652 id: 9,
1653 flags: <SemanticsFlag>[
1654 SemanticsFlag.isButton,
1655 SemanticsFlag.hasEnabledState,
1656 ],
1657 label: 'action',
1658 textDirection: TextDirection.ltr,
1659 ),
1660 ],
1661 ),
1662 ],
1663 ),
1664 ],
1665 ),
1666 ],
1667 ),
1668 ],
1669 ),
1670 ignoreTransform: true,
1671 ignoreId: true,
1672 ignoreRect: true,
1673 ),
1674 );
1675
1676 semantics.dispose();
1677 });
1678
1679 testWidgets('AlertDialog widget always contains alert route semantics for android', (
1680 WidgetTester tester,
1681 ) async {
1682 final SemanticsTester semantics = SemanticsTester(tester);
1683
1684 await tester.pumpWidget(
1685 MaterialApp(
1686 theme: ThemeData(platform: TargetPlatform.android),
1687 home: Material(
1688 child: Builder(
1689 builder: (BuildContext context) {
1690 return Center(
1691 child: ElevatedButton(
1692 child: const Text('X'),
1693 onPressed: () {
1694 showDialog<void>(
1695 context: context,
1696 builder: (BuildContext context) {
1697 return const AlertDialog(
1698 title: Text('Title'),
1699 content: Text('Y'),
1700 actions: <Widget>[],
1701 );
1702 },
1703 );
1704 },
1705 ),
1706 );
1707 },
1708 ),
1709 ),
1710 ),
1711 );
1712
1713 expect(
1714 semantics,
1715 isNot(includesNodeWith(label: 'Title', flags: <SemanticsFlag>[SemanticsFlag.namesRoute])),
1716 );
1717 expect(
1718 semantics,
1719 isNot(
1720 includesNodeWith(
1721 label: 'Alert',
1722 flags: <SemanticsFlag>[SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute],
1723 ),
1724 ),
1725 );
1726
1727 await tester.tap(find.text('X'));
1728 await tester.pumpAndSettle();
1729 // It does not use 'Title' as route semantics
1730 expect(
1731 semantics,
1732 isNot(includesNodeWith(label: 'Title', flags: <SemanticsFlag>[SemanticsFlag.namesRoute])),
1733 );
1734 expect(
1735 semantics,
1736 includesNodeWith(
1737 label: 'Alert',
1738 flags: <SemanticsFlag>[SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute],
1739 ),
1740 );
1741
1742 semantics.dispose();
1743 });
1744
1745 testWidgets('SimpleDialog does not introduce additional node', (WidgetTester tester) async {
1746 final SemanticsTester semantics = SemanticsTester(tester);
1747
1748 await tester.pumpWidget(
1749 MaterialApp(
1750 theme: ThemeData(platform: TargetPlatform.android),
1751 home: Material(
1752 child: Builder(
1753 builder: (BuildContext context) {
1754 return Center(
1755 child: ElevatedButton(
1756 child: const Text('X'),
1757 onPressed: () {
1758 showDialog<void>(
1759 context: context,
1760 builder: (BuildContext context) {
1761 return const SimpleDialog(title: Text('Title'), semanticLabel: 'label');
1762 },
1763 );
1764 },
1765 ),
1766 );
1767 },
1768 ),
1769 ),
1770 ),
1771 );
1772
1773 await tester.tap(find.text('X'));
1774 await tester.pumpAndSettle();
1775 // A scope route is not focusable in accessibility service.
1776 expect(
1777 semantics,
1778 includesNodeWith(
1779 label: 'label',
1780 flags: <SemanticsFlag>[SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute],
1781 ),
1782 );
1783
1784 semantics.dispose();
1785 });
1786
1787 // Regression test for https://github.com/flutter/flutter/issues/78229.
1788 testWidgets('SimpleDialog has correct semantics for title in iOS', (WidgetTester tester) async {
1789 final SemanticsTester semantics = SemanticsTester(tester);
1790
1791 await tester.pumpWidget(
1792 MaterialApp(
1793 theme: ThemeData(platform: TargetPlatform.iOS),
1794 home: const SimpleDialog(
1795 title: Text('title'),
1796 children: <Widget>[
1797 Text('content'),
1798 TextButton(onPressed: null, child: Text('action')),
1799 ],
1800 ),
1801 ),
1802 );
1803
1804 expect(
1805 semantics,
1806 hasSemantics(
1807 TestSemantics.root(
1808 children: <TestSemantics>[
1809 TestSemantics(
1810 id: 1,
1811 textDirection: TextDirection.ltr,
1812 children: <TestSemantics>[
1813 TestSemantics(
1814 id: 2,
1815 children: <TestSemantics>[
1816 TestSemantics(
1817 id: 3,
1818 flags: <SemanticsFlag>[SemanticsFlag.scopesRoute],
1819 children: <TestSemantics>[
1820 TestSemantics(
1821 id: 4,
1822 role: SemanticsRole.dialog,
1823 children: <TestSemantics>[
1824 // Title semantics does not merge into the semantics
1825 // node 4.
1826 TestSemantics(id: 5, label: 'title', textDirection: TextDirection.ltr),
1827 TestSemantics(
1828 id: 6,
1829 flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
1830 children: <TestSemantics>[
1831 TestSemantics(
1832 id: 7,
1833 label: 'content',
1834 textDirection: TextDirection.ltr,
1835 ),
1836 TestSemantics(
1837 id: 8,
1838 flags: <SemanticsFlag>[
1839 SemanticsFlag.isButton,
1840 SemanticsFlag.hasEnabledState,
1841 ],
1842 label: 'action',
1843 textDirection: TextDirection.ltr,
1844 ),
1845 ],
1846 ),
1847 ],
1848 ),
1849 ],
1850 ),
1851 ],
1852 ),
1853 ],
1854 ),
1855 ],
1856 ),
1857 ignoreTransform: true,
1858 ignoreId: true,
1859 ignoreRect: true,
1860 ),
1861 );
1862
1863 semantics.dispose();
1864 });
1865
1866 testWidgets('Dismissible.confirmDismiss defers to an AlertDialog', (WidgetTester tester) async {
1867 final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
1868 final List<int> dismissedItems = <int>[];
1869
1870 // Dismiss is confirmed IFF confirmDismiss() returns true.
1871 Future<bool?> confirmDismiss(DismissDirection dismissDirection) async {
1872 return showDialog<bool>(
1873 context: scaffoldKey.currentContext!,
1874 builder: (BuildContext context) {
1875 return AlertDialog(
1876 actions: <Widget>[
1877 TextButton(
1878 child: const Text('TRUE'),
1879 onPressed: () {
1880 Navigator.pop(context, true); // showDialog() returns true
1881 },
1882 ),
1883 TextButton(
1884 child: const Text('FALSE'),
1885 onPressed: () {
1886 Navigator.pop(context, false); // showDialog() returns false
1887 },
1888 ),
1889 ],
1890 );
1891 },
1892 );
1893 }
1894
1895 Widget buildDismissibleItem(int item, StateSetter setState) {
1896 return Dismissible(
1897 key: ValueKey<int>(item),
1898 confirmDismiss: confirmDismiss,
1899 onDismissed: (DismissDirection direction) {
1900 setState(() {
1901 expect(dismissedItems.contains(item), isFalse);
1902 dismissedItems.add(item);
1903 });
1904 },
1905 child: SizedBox(height: 100.0, child: Text(item.toString())),
1906 );
1907 }
1908
1909 Widget buildFrame() {
1910 return MaterialApp(
1911 home: StatefulBuilder(
1912 builder: (BuildContext context, StateSetter setState) {
1913 return Scaffold(
1914 key: scaffoldKey,
1915 body: Padding(
1916 padding: const EdgeInsets.all(16.0),
1917 child: ListView(
1918 itemExtent: 100.0,
1919 children: <int>[0, 1, 2, 3, 4]
1920 .where((int i) => !dismissedItems.contains(i))
1921 .map<Widget>((int item) => buildDismissibleItem(item, setState))
1922 .toList(),
1923 ),
1924 ),
1925 );
1926 },
1927 ),
1928 );
1929 }
1930
1931 Future<void> dismissItem(WidgetTester tester, int item) async {
1932 await tester.fling(
1933 find.text(item.toString()),
1934 const Offset(300.0, 0.0),
1935 1000.0,
1936 ); // fling to the right
1937 await tester.pump(); // start the slide
1938 await tester.pump(const Duration(seconds: 1)); // finish the slide and start shrinking...
1939 await tester.pump(); // first frame of shrinking animation
1940 await tester.pump(
1941 const Duration(seconds: 1),
1942 ); // finish the shrinking and call the callback...
1943 await tester.pump(); // rebuild after the callback removes the entry
1944 }
1945
1946 // Dismiss item 0 is confirmed via the AlertDialog
1947 await tester.pumpWidget(buildFrame());
1948 expect(dismissedItems, isEmpty);
1949 await dismissItem(tester, 0); // Causes the AlertDialog to appear per confirmDismiss
1950 await tester.pumpAndSettle();
1951 await tester.tap(find.text('TRUE')); // AlertDialog action
1952 await tester.pumpAndSettle();
1953 expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
1954 expect(find.text('FALSE'), findsNothing);
1955 expect(dismissedItems, <int>[0]);
1956 expect(find.text('0'), findsNothing);
1957
1958 // Dismiss item 1 is not confirmed via the AlertDialog
1959 await tester.pumpWidget(buildFrame());
1960 expect(dismissedItems, <int>[0]);
1961 await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss
1962 await tester.pumpAndSettle();
1963 await tester.tap(find.text('FALSE')); // AlertDialog action
1964 await tester.pumpAndSettle();
1965 expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
1966 expect(find.text('FALSE'), findsNothing);
1967 expect(dismissedItems, <int>[0]);
1968 expect(find.text('0'), findsNothing);
1969 expect(find.text('1'), findsOneWidget);
1970
1971 // Dismiss item 1 is not confirmed via the AlertDialog
1972 await tester.pumpWidget(buildFrame());
1973 expect(dismissedItems, <int>[0]);
1974 await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss
1975 await tester.pumpAndSettle();
1976 expect(find.text('FALSE'), findsOneWidget);
1977 expect(find.text('TRUE'), findsOneWidget);
1978 await tester.tapAt(Offset.zero); // Tap outside of the AlertDialog
1979 await tester.pumpAndSettle();
1980 expect(dismissedItems, <int>[0]);
1981 expect(find.text('0'), findsNothing);
1982 expect(find.text('1'), findsOneWidget);
1983 expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
1984 expect(find.text('FALSE'), findsNothing);
1985
1986 // Dismiss item 1 is confirmed via the AlertDialog
1987 await tester.pumpWidget(buildFrame());
1988 expect(dismissedItems, <int>[0]);
1989 await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss
1990 await tester.pumpAndSettle();
1991 await tester.tap(find.text('TRUE')); // AlertDialog action
1992 await tester.pumpAndSettle();
1993 expect(find.text('TRUE'), findsNothing); // Dialog was dismissed
1994 expect(find.text('FALSE'), findsNothing);
1995 expect(dismissedItems, <int>[0, 1]);
1996 expect(find.text('0'), findsNothing);
1997 expect(find.text('1'), findsNothing);
1998 });
1999
2000 // Regression test for https://github.com/flutter/flutter/issues/28505.
2001 testWidgets('showDialog only gets Theme from context on the first call', (
2002 WidgetTester tester,
2003 ) async {
2004 Widget buildFrame(Key builderKey) {
2005 return MaterialApp(
2006 home: Center(
2007 child: Builder(
2008 key: builderKey,
2009 builder: (BuildContext outerContext) {
2010 return ElevatedButton(
2011 onPressed: () {
2012 showDialog<void>(
2013 context: outerContext,
2014 builder: (BuildContext innerContext) {
2015 return const AlertDialog(title: Text('Title'));
2016 },
2017 );
2018 },
2019 child: const Text('Show Dialog'),
2020 );
2021 },
2022 ),
2023 ),
2024 );
2025 }
2026
2027 await tester.pumpWidget(buildFrame(UniqueKey()));
2028
2029 // Open the dialog.
2030 await tester.tap(find.byType(ElevatedButton));
2031 await tester.pumpAndSettle();
2032
2033 // Force the Builder to be recreated (new key) which causes outerContext to
2034 // be deactivated. If showDialog()'s implementation were to refer to
2035 // outerContext again, it would crash.
2036 await tester.pumpWidget(buildFrame(UniqueKey()));
2037 await tester.pump();
2038 });
2039
2040 testWidgets('showDialog safe area', (WidgetTester tester) async {
2041 await tester.pumpWidget(
2042 MaterialApp(
2043 builder: (BuildContext context, Widget? child) {
2044 return MediaQuery(
2045 // Set up the safe area to be 20 pixels in from each side
2046 data: const MediaQueryData(padding: EdgeInsets.all(20.0)),
2047 child: child!,
2048 );
2049 },
2050 home: const Center(child: Text('Test')),
2051 ),
2052 );
2053 final BuildContext context = tester.element(find.text('Test'));
2054
2055 // By default it should honor the safe area
2056 showDialog<void>(
2057 context: context,
2058 builder: (BuildContext context) {
2059 return const Placeholder();
2060 },
2061 );
2062 await tester.pumpAndSettle();
2063 expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 20.0));
2064 expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0));
2065
2066 // Dismiss it and test with useSafeArea off
2067 await tester.tapAt(const Offset(10.0, 10.0));
2068 showDialog<void>(
2069 context: context,
2070 builder: (BuildContext context) {
2071 return const Placeholder();
2072 },
2073 useSafeArea: false,
2074 );
2075 await tester.pumpAndSettle();
2076 // Should take up the whole screen
2077 expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero);
2078 expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
2079 });
2080
2081 testWidgets('showDialog uses root navigator by default', (WidgetTester tester) async {
2082 final DialogObserver rootObserver = DialogObserver();
2083 final DialogObserver nestedObserver = DialogObserver();
2084
2085 await tester.pumpWidget(
2086 MaterialApp(
2087 navigatorObservers: <NavigatorObserver>[rootObserver],
2088 home: Navigator(
2089 observers: <NavigatorObserver>[nestedObserver],
2090 onGenerateRoute: (RouteSettings settings) {
2091 return MaterialPageRoute<dynamic>(
2092 builder: (BuildContext context) {
2093 return ElevatedButton(
2094 onPressed: () {
2095 showDialog<void>(
2096 context: context,
2097 builder: (BuildContext innerContext) {
2098 return const AlertDialog(title: Text('Title'));
2099 },
2100 );
2101 },
2102 child: const Text('Show Dialog'),
2103 );
2104 },
2105 );
2106 },
2107 ),
2108 ),
2109 );
2110
2111 // Open the dialog.
2112 await tester.tap(find.byType(ElevatedButton));
2113
2114 expect(rootObserver.dialogCount, 1);
2115 expect(nestedObserver.dialogCount, 0);
2116 });
2117
2118 testWidgets('showDialog uses nested navigator if useRootNavigator is false', (
2119 WidgetTester tester,
2120 ) async {
2121 final DialogObserver rootObserver = DialogObserver();
2122 final DialogObserver nestedObserver = DialogObserver();
2123
2124 await tester.pumpWidget(
2125 MaterialApp(
2126 navigatorObservers: <NavigatorObserver>[rootObserver],
2127 home: Navigator(
2128 observers: <NavigatorObserver>[nestedObserver],
2129 onGenerateRoute: (RouteSettings settings) {
2130 return MaterialPageRoute<dynamic>(
2131 builder: (BuildContext context) {
2132 return ElevatedButton(
2133 onPressed: () {
2134 showDialog<void>(
2135 context: context,
2136 useRootNavigator: false,
2137 builder: (BuildContext innerContext) {
2138 return const AlertDialog(title: Text('Title'));
2139 },
2140 );
2141 },
2142 child: const Text('Show Dialog'),
2143 );
2144 },
2145 );
2146 },
2147 ),
2148 ),
2149 );
2150
2151 // Open the dialog.
2152 await tester.tap(find.byType(ElevatedButton));
2153
2154 expect(rootObserver.dialogCount, 0);
2155 expect(nestedObserver.dialogCount, 1);
2156 });
2157
2158 testWidgets('showDialog throws a friendly user message when context is not active', (
2159 WidgetTester tester,
2160 ) async {
2161 // Regression test for https://github.com/flutter/flutter/issues/12467
2162 await tester.pumpWidget(const MaterialApp(home: Center(child: Text('Test'))));
2163 final BuildContext context = tester.element(find.text('Test'));
2164
2165 await tester.pumpWidget(const MaterialApp(home: Center()));
2166
2167 Object? error;
2168 try {
2169 showDialog<void>(
2170 context: context,
2171 builder: (BuildContext innerContext) {
2172 return const AlertDialog(title: Text('Title'));
2173 },
2174 );
2175 } catch (exception) {
2176 error = exception;
2177 }
2178
2179 expect(error, isNotNull);
2180 expect(error, isFlutterError);
2181 if (error is FlutterError) {
2182 final ErrorSummary summary = error.diagnostics.first as ErrorSummary;
2183 expect(summary.toString(), 'This BuildContext is no longer valid.');
2184 }
2185 });
2186
2187 group('showDialog avoids overlapping display features', () {
2188 testWidgets('positioning with anchorPoint', (WidgetTester tester) async {
2189 await tester.pumpWidget(
2190 MaterialApp(
2191 builder: (BuildContext context, Widget? child) {
2192 return MediaQuery(
2193 // Display has a vertical hinge down the middle
2194 data: const MediaQueryData(
2195 size: Size(800, 600),
2196 displayFeatures: <DisplayFeature>[
2197 DisplayFeature(
2198 bounds: Rect.fromLTRB(390, 0, 410, 600),
2199 type: DisplayFeatureType.hinge,
2200 state: DisplayFeatureState.unknown,
2201 ),
2202 ],
2203 ),
2204 child: child!,
2205 );
2206 },
2207 home: const Center(child: Text('Test')),
2208 ),
2209 );
2210 final BuildContext context = tester.element(find.text('Test'));
2211
2212 showDialog<void>(
2213 context: context,
2214 builder: (BuildContext context) {
2215 return const Placeholder();
2216 },
2217 anchorPoint: const Offset(1000, 0),
2218 );
2219 await tester.pumpAndSettle();
2220
2221 // Should take the right side of the screen
2222 expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
2223 expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
2224 });
2225
2226 testWidgets('positioning with Directionality', (WidgetTester tester) async {
2227 await tester.pumpWidget(
2228 MaterialApp(
2229 builder: (BuildContext context, Widget? child) {
2230 return MediaQuery(
2231 // Display has a vertical hinge down the middle
2232 data: const MediaQueryData(
2233 size: Size(800, 600),
2234 displayFeatures: <DisplayFeature>[
2235 DisplayFeature(
2236 bounds: Rect.fromLTRB(390, 0, 410, 600),
2237 type: DisplayFeatureType.hinge,
2238 state: DisplayFeatureState.unknown,
2239 ),
2240 ],
2241 ),
2242 child: Directionality(textDirection: TextDirection.rtl, child: child!),
2243 );
2244 },
2245 home: const Center(child: Text('Test')),
2246 ),
2247 );
2248 final BuildContext context = tester.element(find.text('Test'));
2249
2250 showDialog<void>(
2251 context: context,
2252 builder: (BuildContext context) {
2253 return const Placeholder();
2254 },
2255 );
2256 await tester.pumpAndSettle();
2257
2258 // Since this is RTL, it should place the dialog on the right screen
2259 expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0));
2260 expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0));
2261 });
2262
2263 testWidgets('positioning by default', (WidgetTester tester) async {
2264 await tester.pumpWidget(
2265 MaterialApp(
2266 builder: (BuildContext context, Widget? child) {
2267 return MediaQuery(
2268 // Display has a vertical hinge down the middle
2269 data: const MediaQueryData(
2270 size: Size(800, 600),
2271 displayFeatures: <DisplayFeature>[
2272 DisplayFeature(
2273 bounds: Rect.fromLTRB(390, 0, 410, 600),
2274 type: DisplayFeatureType.hinge,
2275 state: DisplayFeatureState.unknown,
2276 ),
2277 ],
2278 ),
2279 child: child!,
2280 );
2281 },
2282 home: const Center(child: Text('Test')),
2283 ),
2284 );
2285 final BuildContext context = tester.element(find.text('Test'));
2286
2287 showDialog<void>(
2288 context: context,
2289 builder: (BuildContext context) {
2290 return const Placeholder();
2291 },
2292 );
2293 await tester.pumpAndSettle();
2294
2295 // By default it should place the dialog on the left screen
2296 expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero);
2297 expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(390.0, 600.0));
2298 });
2299 });
2300
2301 group('AlertDialog.scrollable: ', () {
2302 testWidgets('Title is scrollable', (WidgetTester tester) async {
2303 final Key titleKey = UniqueKey();
2304 final AlertDialog dialog = AlertDialog(
2305 title: Container(key: titleKey, color: Colors.green, height: 1000),
2306 scrollable: true,
2307 );
2308 await tester.pumpWidget(_buildAppWithDialog(dialog));
2309 await tester.tap(find.text('X'));
2310 await tester.pumpAndSettle();
2311
2312 final RenderBox box = tester.renderObject(find.byKey(titleKey));
2313 final Offset originalOffset = box.localToGlobal(Offset.zero);
2314 await tester.drag(find.byKey(titleKey), const Offset(0.0, -200.0));
2315 expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -200.0)));
2316 });
2317
2318 testWidgets('Content is scrollable', (WidgetTester tester) async {
2319 final Key contentKey = UniqueKey();
2320 final AlertDialog dialog = AlertDialog(
2321 content: Container(key: contentKey, color: Colors.orange, height: 1000),
2322 scrollable: true,
2323 );
2324 await tester.pumpWidget(_buildAppWithDialog(dialog));
2325 await tester.tap(find.text('X'));
2326 await tester.pumpAndSettle();
2327
2328 final RenderBox box = tester.renderObject(find.byKey(contentKey));
2329 final Offset originalOffset = box.localToGlobal(Offset.zero);
2330 await tester.drag(find.byKey(contentKey), const Offset(0.0, -200.0));
2331 expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -200.0)));
2332 });
2333
2334 testWidgets('Title and content are scrollable', (WidgetTester tester) async {
2335 final Key titleKey = UniqueKey();
2336 final Key contentKey = UniqueKey();
2337 final AlertDialog dialog = AlertDialog(
2338 title: Container(key: titleKey, color: Colors.green, height: 400),
2339 content: Container(key: contentKey, color: Colors.orange, height: 400),
2340 scrollable: true,
2341 );
2342 await tester.pumpWidget(_buildAppWithDialog(dialog));
2343 await tester.tap(find.text('X'));
2344 await tester.pumpAndSettle();
2345
2346 final RenderBox title = tester.renderObject(find.byKey(titleKey));
2347 final RenderBox content = tester.renderObject(find.byKey(contentKey));
2348 final Offset titleOriginalOffset = title.localToGlobal(Offset.zero);
2349 final Offset contentOriginalOffset = content.localToGlobal(Offset.zero);
2350
2351 // Dragging the title widget should scroll both the title
2352 // and the content widgets.
2353 await tester.drag(find.byKey(titleKey), const Offset(0.0, -200.0));
2354 expect(title.localToGlobal(Offset.zero), equals(titleOriginalOffset.translate(0.0, -200.0)));
2355 expect(
2356 content.localToGlobal(Offset.zero),
2357 equals(contentOriginalOffset.translate(0.0, -200.0)),
2358 );
2359
2360 // Dragging the content widget should scroll both the title
2361 // and the content widgets.
2362 await tester.drag(find.byKey(contentKey), const Offset(0.0, 200.0));
2363 expect(title.localToGlobal(Offset.zero), equals(titleOriginalOffset));
2364 expect(content.localToGlobal(Offset.zero), equals(contentOriginalOffset));
2365 });
2366 });
2367
2368 testWidgets('Dialog with RouteSettings', (WidgetTester tester) async {
2369 late RouteSettings currentRouteSetting;
2370
2371 await tester.pumpWidget(
2372 MaterialApp(
2373 navigatorObservers: <NavigatorObserver>[
2374 _ClosureNavigatorObserver(
2375 onDidChange: (Route<dynamic> newRoute) {
2376 currentRouteSetting = newRoute.settings;
2377 },
2378 ),
2379 ],
2380 home: const Material(
2381 child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))),
2382 ),
2383 ),
2384 );
2385
2386 final BuildContext context = tester.element(find.text('Go'));
2387 const RouteSettings exampleSetting = RouteSettings(name: 'simple');
2388
2389 final Future<int?> result = showDialog<int>(
2390 context: context,
2391 builder: (BuildContext context) {
2392 return SimpleDialog(
2393 title: const Text('Title'),
2394 children: <Widget>[
2395 SimpleDialogOption(
2396 child: const Text('X'),
2397 onPressed: () {
2398 Navigator.of(context).pop();
2399 },
2400 ),
2401 ],
2402 );
2403 },
2404 routeSettings: exampleSetting,
2405 );
2406
2407 await tester.pumpAndSettle();
2408 expect(find.text('Title'), findsOneWidget);
2409 expect(currentRouteSetting, exampleSetting);
2410
2411 await tester.tap(find.text('X'));
2412 await tester.pumpAndSettle();
2413
2414 expect(await result, isNull);
2415 await tester.pumpAndSettle();
2416 expect(currentRouteSetting.name, '/');
2417 });
2418
2419 testWidgets('showDialog - custom barrierLabel', (WidgetTester tester) async {
2420 final SemanticsTester semantics = SemanticsTester(tester);
2421
2422 await tester.pumpWidget(
2423 MaterialApp(
2424 theme: ThemeData(platform: TargetPlatform.iOS),
2425 home: Material(
2426 child: Builder(
2427 builder: (BuildContext context) {
2428 return Center(
2429 child: ElevatedButton(
2430 child: const Text('X'),
2431 onPressed: () {
2432 showDialog<void>(
2433 context: context,
2434 barrierLabel: 'Custom label',
2435 builder: (BuildContext context) {
2436 return const AlertDialog(
2437 title: Text('Title'),
2438 content: Text('Y'),
2439 actions: <Widget>[],
2440 );
2441 },
2442 );
2443 },
2444 ),
2445 );
2446 },
2447 ),
2448 ),
2449 ),
2450 );
2451
2452 expect(
2453 semantics,
2454 isNot(
2455 includesNodeWith(label: 'Custom label', flags: <SemanticsFlag>[SemanticsFlag.namesRoute]),
2456 ),
2457 );
2458 semantics.dispose();
2459 });
2460
2461 testWidgets('DialogRoute is state restorable', (WidgetTester tester) async {
2462 await tester.pumpWidget(
2463 const MaterialApp(restorationScopeId: 'app', home: _RestorableDialogTestWidget()),
2464 );
2465
2466 expect(find.byType(AlertDialog), findsNothing);
2467
2468 await tester.tap(find.text('X'));
2469 await tester.pumpAndSettle();
2470
2471 expect(find.byType(AlertDialog), findsOneWidget);
2472 final TestRestorationData restorationData = await tester.getRestorationData();
2473
2474 await tester.restartAndRestore();
2475
2476 expect(find.byType(AlertDialog), findsOneWidget);
2477
2478 // Tap on the barrier.
2479 await tester.tapAt(const Offset(10.0, 10.0));
2480 await tester.pumpAndSettle();
2481
2482 expect(find.byType(AlertDialog), findsNothing);
2483
2484 await tester.restoreFrom(restorationData);
2485 expect(find.byType(AlertDialog), findsOneWidget);
2486 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
2487
2488 testWidgets('AlertDialog.actionsAlignment', (WidgetTester tester) async {
2489 final Key actionKey = UniqueKey();
2490
2491 Widget buildFrame(MainAxisAlignment? alignment) {
2492 return MaterialApp(
2493 theme: ThemeData(useMaterial3: false),
2494 home: Scaffold(
2495 body: AlertDialog(
2496 content: const SizedBox(width: 800),
2497 actionsAlignment: alignment,
2498 actions: <Widget>[SizedBox(key: actionKey, width: 20, height: 20)],
2499 buttonPadding: EdgeInsets.zero,
2500 insetPadding: EdgeInsets.zero,
2501 ),
2502 ),
2503 );
2504 }
2505
2506 // Default configuration
2507 await tester.pumpWidget(buildFrame(null));
2508 expect(tester.getTopLeft(find.byType(AlertDialog)).dx, 0);
2509 expect(tester.getTopRight(find.byType(AlertDialog)).dx, 800);
2510 expect(tester.getSize(find.byType(OverflowBar)).width, 800);
2511 expect(tester.getTopLeft(find.byKey(actionKey)).dx, 800 - 20);
2512 expect(tester.getTopRight(find.byKey(actionKey)).dx, 800);
2513
2514 // All possible alignment values
2515
2516 await tester.pumpWidget(buildFrame(MainAxisAlignment.start));
2517 expect(tester.getTopLeft(find.byKey(actionKey)).dx, 0);
2518 expect(tester.getTopRight(find.byKey(actionKey)).dx, 20);
2519
2520 await tester.pumpWidget(buildFrame(MainAxisAlignment.center));
2521 expect(tester.getTopLeft(find.byKey(actionKey)).dx, (800 - 20) / 2);
2522 expect(tester.getTopRight(find.byKey(actionKey)).dx, (800 - 20) / 2 + 20);
2523
2524 await tester.pumpWidget(buildFrame(MainAxisAlignment.end));
2525 expect(tester.getTopLeft(find.byKey(actionKey)).dx, 800 - 20);
2526 expect(tester.getTopRight(find.byKey(actionKey)).dx, 800);
2527
2528 await tester.pumpWidget(buildFrame(MainAxisAlignment.spaceBetween));
2529 expect(tester.getTopLeft(find.byKey(actionKey)).dx, 0);
2530 expect(tester.getTopRight(find.byKey(actionKey)).dx, 20);
2531
2532 await tester.pumpWidget(buildFrame(MainAxisAlignment.spaceAround));
2533 expect(tester.getTopLeft(find.byKey(actionKey)).dx, (800 - 20) / 2);
2534 expect(tester.getTopRight(find.byKey(actionKey)).dx, (800 - 20) / 2 + 20);
2535
2536 await tester.pumpWidget(buildFrame(MainAxisAlignment.spaceEvenly));
2537 expect(tester.getTopLeft(find.byKey(actionKey)).dx, (800 - 20) / 2);
2538 expect(tester.getTopRight(find.byKey(actionKey)).dx, (800 - 20) / 2 + 20);
2539 });
2540
2541 testWidgets('Uses closed loop focus traversal', (WidgetTester tester) async {
2542 final FocusNode okNode = FocusNode();
2543 final FocusNode cancelNode = FocusNode();
2544
2545 Future<bool> nextFocus() async {
2546 final bool result = Actions.invoke(primaryFocus!.context!, const NextFocusIntent())! as bool;
2547 await tester.pump();
2548 return result;
2549 }
2550
2551 Future<bool> previousFocus() async {
2552 final bool result =
2553 Actions.invoke(primaryFocus!.context!, const PreviousFocusIntent())! as bool;
2554 await tester.pump();
2555 return result;
2556 }
2557
2558 final AlertDialog dialog = AlertDialog(
2559 content: const Text('Test dialog'),
2560 actions: <Widget>[
2561 TextButton(focusNode: okNode, onPressed: () {}, child: const Text('OK')),
2562 TextButton(focusNode: cancelNode, onPressed: () {}, child: const Text('Cancel')),
2563 ],
2564 );
2565 await tester.pumpWidget(_buildAppWithDialog(dialog));
2566 await tester.tap(find.text('X'));
2567 await tester.pumpAndSettle();
2568
2569 // Start at OK
2570 okNode.requestFocus();
2571 await tester.pump();
2572 expect(okNode.hasFocus, true);
2573 expect(cancelNode.hasFocus, false);
2574
2575 // OK -> Cancel
2576 expect(await nextFocus(), true);
2577 expect(okNode.hasFocus, false);
2578 expect(cancelNode.hasFocus, true);
2579
2580 // Cancel -> OK
2581 expect(await nextFocus(), true);
2582 expect(okNode.hasFocus, true);
2583 expect(cancelNode.hasFocus, false);
2584
2585 // Cancel <- OK
2586 expect(await previousFocus(), true);
2587 expect(okNode.hasFocus, false);
2588 expect(cancelNode.hasFocus, true);
2589
2590 // OK <- Cancel
2591 expect(await previousFocus(), true);
2592 expect(okNode.hasFocus, true);
2593 expect(cancelNode.hasFocus, false);
2594
2595 cancelNode.dispose();
2596 okNode.dispose();
2597 });
2598
2599 testWidgets('Adaptive AlertDialog shows correct widget on each platform', (
2600 WidgetTester tester,
2601 ) async {
2602 final AlertDialog dialog = AlertDialog.adaptive(
2603 content: Container(height: 5000.0, width: 300.0, color: Colors.green[500]),
2604 actions: <Widget>[TextButton(onPressed: () {}, child: const Text('OK'))],
2605 );
2606
2607 for (final TargetPlatform platform in <TargetPlatform>[
2608 TargetPlatform.iOS,
2609 TargetPlatform.macOS,
2610 ]) {
2611 await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(platform: platform)));
2612 await tester.pumpAndSettle();
2613
2614 await tester.tap(find.text('X'));
2615 await tester.pumpAndSettle();
2616
2617 expect(find.byType(CupertinoAlertDialog), findsOneWidget);
2618
2619 await tester.tapAt(const Offset(10.0, 10.0));
2620 await tester.pumpAndSettle();
2621 }
2622
2623 for (final TargetPlatform platform in <TargetPlatform>[
2624 TargetPlatform.android,
2625 TargetPlatform.fuchsia,
2626 TargetPlatform.linux,
2627 TargetPlatform.windows,
2628 ]) {
2629 await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(platform: platform)));
2630 await tester.pumpAndSettle();
2631
2632 await tester.tap(find.text('X'));
2633 await tester.pumpAndSettle();
2634
2635 expect(find.byType(CupertinoAlertDialog), findsNothing);
2636
2637 await tester.tapAt(const Offset(10.0, 10.0));
2638 await tester.pumpAndSettle();
2639 }
2640 });
2641
2642 testWidgets('showAdaptiveDialog should not allow dismiss on barrier on iOS by default', (
2643 WidgetTester tester,
2644 ) async {
2645 await tester.pumpWidget(
2646 MaterialApp(
2647 theme: ThemeData(platform: TargetPlatform.iOS),
2648 home: const Material(
2649 child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))),
2650 ),
2651 ),
2652 );
2653
2654 final BuildContext context = tester.element(find.text('Go'));
2655
2656 showDialog<void>(
2657 context: context,
2658 builder: (BuildContext context) {
2659 return Container(
2660 width: 100.0,
2661 height: 100.0,
2662 alignment: Alignment.center,
2663 child: const Text('Dialog1'),
2664 );
2665 },
2666 );
2667
2668 await tester.pumpAndSettle(const Duration(seconds: 1));
2669 expect(find.text('Dialog1'), findsOneWidget);
2670
2671 // Tap on the barrier.
2672 await tester.tapAt(const Offset(10.0, 10.0));
2673
2674 await tester.pumpAndSettle(const Duration(seconds: 1));
2675 expect(find.text('Dialog1'), findsNothing);
2676
2677 showAdaptiveDialog<void>(
2678 context: context,
2679 builder: (BuildContext context) {
2680 return Container(
2681 width: 100.0,
2682 height: 100.0,
2683 alignment: Alignment.center,
2684 child: const Text('Dialog2'),
2685 );
2686 },
2687 );
2688
2689 await tester.pumpAndSettle(const Duration(seconds: 1));
2690 expect(find.text('Dialog2'), findsOneWidget);
2691
2692 // Tap on the barrier, which shouldn't do anything this time.
2693 await tester.tapAt(const Offset(10.0, 10.0));
2694
2695 await tester.pumpAndSettle(const Duration(seconds: 1));
2696 expect(find.text('Dialog2'), findsOneWidget);
2697 });
2698
2699 testWidgets('Applies AnimationStyle to showAdaptiveDialog', (WidgetTester tester) async {
2700 const AnimationStyle animationStyle = AnimationStyle(
2701 duration: Duration(seconds: 1),
2702 curve: Curves.easeInOut,
2703 );
2704
2705 await tester.pumpWidget(
2706 const MaterialApp(
2707 home: Material(
2708 child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))),
2709 ),
2710 ),
2711 );
2712 final BuildContext context = tester.element(find.text('Go'));
2713 showAdaptiveDialog<void>(
2714 context: context,
2715 builder: (BuildContext context) {
2716 return Container(
2717 width: 100.0,
2718 height: 100.0,
2719 alignment: Alignment.center,
2720 child: const Text('Dialog1'),
2721 );
2722 },
2723 animationStyle: animationStyle,
2724 );
2725
2726 await tester.pumpAndSettle(const Duration(seconds: 1));
2727 expect(find.text('Dialog1'), findsOneWidget);
2728
2729 await tester.tapAt(const Offset(10.0, 10.0));
2730 await tester.pumpAndSettle(const Duration(seconds: 1));
2731 expect(find.text('Dialog1'), findsNothing);
2732 });
2733
2734 testWidgets('Uses open focus traversal when overridden', (WidgetTester tester) async {
2735 final FocusNode okNode = FocusNode();
2736 addTearDown(okNode.dispose);
2737 final FocusNode cancelNode = FocusNode();
2738 addTearDown(cancelNode.dispose);
2739
2740 Future<bool> nextFocus() async {
2741 final bool result = Actions.invoke(primaryFocus!.context!, const NextFocusIntent())! as bool;
2742 await tester.pump();
2743 return result;
2744 }
2745
2746 final AlertDialog dialog = AlertDialog(
2747 content: const Text('Test dialog'),
2748 actions: <Widget>[
2749 TextButton(focusNode: okNode, onPressed: () {}, child: const Text('OK')),
2750 TextButton(focusNode: cancelNode, onPressed: () {}, child: const Text('Cancel')),
2751 ],
2752 );
2753 await tester.pumpWidget(
2754 _buildAppWithDialog(dialog, traversalEdgeBehavior: TraversalEdgeBehavior.leaveFlutterView),
2755 );
2756 await tester.tap(find.text('X'));
2757 await tester.pumpAndSettle();
2758
2759 // Start at OK
2760 okNode.requestFocus();
2761 await tester.pump();
2762 expect(okNode.hasFocus, true);
2763 expect(cancelNode.hasFocus, false);
2764
2765 // OK -> Cancel
2766 expect(await nextFocus(), true);
2767 expect(okNode.hasFocus, false);
2768 expect(cancelNode.hasFocus, true);
2769
2770 // Cancel -> nothing
2771 expect(await nextFocus(), false);
2772 expect(okNode.hasFocus, false);
2773 expect(cancelNode.hasFocus, false);
2774 });
2775
2776 testWidgets('Dialog.insetPadding is nullable', (WidgetTester tester) async {
2777 const Dialog dialog = Dialog();
2778 expect(dialog.insetPadding, isNull);
2779 });
2780
2781 testWidgets('AlertDialog.insetPadding is nullable', (WidgetTester tester) async {
2782 const AlertDialog alertDialog = AlertDialog();
2783 expect(alertDialog.insetPadding, isNull);
2784 });
2785
2786 testWidgets('SimpleDialog.insetPadding is nullable', (WidgetTester tester) async {
2787 const SimpleDialog simpleDialog = SimpleDialog();
2788 expect(simpleDialog.insetPadding, isNull);
2789 });
2790
2791 // This is a regression test for https://github.com/flutter/flutter/issues/153983.
2792 testWidgets('Can pass a null value to AlertDialog.adaptive clip behavior', (
2793 WidgetTester tester,
2794 ) async {
2795 for (final Clip? clipBehavior in <Clip?>[null, ...Clip.values]) {
2796 AlertDialog.adaptive(clipBehavior: clipBehavior);
2797 }
2798 });
2799
2800 testWidgets('Setting DialogRoute.requestFocus to false does not request focus on the dialog', (
2801 WidgetTester tester,
2802 ) async {
2803 late BuildContext savedContext;
2804 final FocusNode focusNode = FocusNode();
2805 addTearDown(focusNode.dispose);
2806 const String dialogText = 'Dialog Text';
2807 await tester.pumpWidget(
2808 MaterialApp(
2809 home: Material(
2810 child: Builder(
2811 builder: (BuildContext context) {
2812 savedContext = context;
2813 return TextField(focusNode: focusNode);
2814 },
2815 ),
2816 ),
2817 ),
2818 );
2819 await tester.pump();
2820
2821 FocusNode? getTextFieldFocusNode() {
2822 return tester
2823 .widget<Focus>(find.descendant(of: find.byType(TextField), matching: find.byType(Focus)))
2824 .focusNode;
2825 }
2826
2827 // Initially, there is no dialog and the text field has no focus.
2828 expect(find.text(dialogText), findsNothing);
2829 expect(getTextFieldFocusNode()?.hasFocus, false);
2830
2831 // Request focus on the text field.
2832 focusNode.requestFocus();
2833 await tester.pump();
2834 expect(getTextFieldFocusNode()?.hasFocus, true);
2835
2836 // Bring up dialog.
2837 final NavigatorState navigator = Navigator.of(savedContext);
2838 navigator.push(
2839 DialogRoute<void>(
2840 context: savedContext,
2841 builder: (BuildContext context) => const Text(dialogText),
2842 ),
2843 );
2844 await tester.pump();
2845
2846 // The dialog is showing and the text field has lost focus.
2847 expect(find.text(dialogText), findsOneWidget);
2848 expect(getTextFieldFocusNode()?.hasFocus, false);
2849
2850 // Dismiss the dialog.
2851 navigator.pop();
2852 await tester.pump();
2853
2854 // The dialog is dismissed and the focus is shifted back to the text field.
2855 expect(find.text(dialogText), findsNothing);
2856 expect(getTextFieldFocusNode()?.hasFocus, true);
2857
2858 // Bring up dialog again with requestFocus to false.
2859 navigator.push(
2860 ModalBottomSheetRoute<void>(
2861 requestFocus: false,
2862 isScrollControlled: false,
2863 builder: (BuildContext context) => const Text(dialogText),
2864 ),
2865 );
2866 await tester.pump();
2867
2868 // The dialog is showing and the text field still has focus.
2869 expect(find.text(dialogText), findsOneWidget);
2870 expect(getTextFieldFocusNode()?.hasFocus, true);
2871 });
2872
2873 testWidgets('requestFocus works correctly in showDialog.', (WidgetTester tester) async {
2874 final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
2875 final FocusNode focusNode = FocusNode();
2876 addTearDown(focusNode.dispose);
2877 await tester.pumpWidget(
2878 MaterialApp(
2879 navigatorKey: navigatorKey,
2880 home: Scaffold(body: TextField(focusNode: focusNode)),
2881 ),
2882 );
2883 focusNode.requestFocus();
2884 await tester.pump();
2885 expect(focusNode.hasFocus, true);
2886
2887 showDialog<void>(
2888 context: navigatorKey.currentContext!,
2889 requestFocus: true,
2890 builder: (BuildContext context) => const Text('dialog'),
2891 );
2892 await tester.pumpAndSettle();
2893 expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, true);
2894 expect(focusNode.hasFocus, false);
2895
2896 navigatorKey.currentState!.pop();
2897 await tester.pumpAndSettle();
2898 expect(focusNode.hasFocus, true);
2899
2900 showDialog<void>(
2901 context: navigatorKey.currentContext!,
2902 requestFocus: false,
2903 builder: (BuildContext context) => const Text('dialog'),
2904 );
2905 await tester.pumpAndSettle();
2906 expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, false);
2907 expect(focusNode.hasFocus, true);
2908 });
2909
2910 testWidgets('Dialog respects the given constraints', (WidgetTester tester) async {
2911 await tester.pumpWidget(
2912 _buildAppWithDialog(
2913 const Dialog(
2914 constraints: BoxConstraints(maxWidth: 560),
2915 child: SizedBox(width: 1000, height: 100),
2916 ),
2917 ),
2918 );
2919
2920 await tester.tap(find.text('X'));
2921 await tester.pumpAndSettle();
2922
2923 expect(tester.getSize(find.byType(SizedBox)).width, 560);
2924 });
2925
2926 testWidgets('AlertDialog respects the default constraints', (WidgetTester tester) async {
2927 await tester.pumpWidget(
2928 _buildAppWithDialog(const AlertDialog(content: SizedBox(), contentPadding: EdgeInsets.zero)),
2929 );
2930
2931 await tester.tap(find.text('X'));
2932 await tester.pumpAndSettle();
2933
2934 expect(tester.getSize(find.byType(SizedBox)).width, 280);
2935 });
2936
2937 testWidgets('AlertDialog respects the given constraints', (WidgetTester tester) async {
2938 await tester.pumpWidget(
2939 _buildAppWithDialog(
2940 const AlertDialog(
2941 constraints: BoxConstraints(maxWidth: 560),
2942 content: SizedBox(width: 1000, height: 100),
2943 contentPadding: EdgeInsets.zero,
2944 ),
2945 ),
2946 );
2947
2948 await tester.tap(find.text('X'));
2949 await tester.pumpAndSettle();
2950
2951 expect(tester.getSize(find.byType(SizedBox)).width, 560);
2952 });
2953
2954 testWidgets('SimpleDialog respects the default constraints', (WidgetTester tester) async {
2955 await tester.pumpWidget(
2956 _buildAppWithDialog(
2957 const SimpleDialog(contentPadding: EdgeInsets.zero, children: <Widget>[SizedBox()]),
2958 ),
2959 );
2960
2961 await tester.tap(find.text('X'));
2962 await tester.pumpAndSettle();
2963
2964 expect(tester.getSize(find.byType(SizedBox)).width, 280);
2965 });
2966
2967 testWidgets('SimpleDialog respects the given constraints', (WidgetTester tester) async {
2968 await tester.pumpWidget(
2969 _buildAppWithDialog(
2970 const SimpleDialog(
2971 constraints: BoxConstraints(maxWidth: 560),
2972 contentPadding: EdgeInsets.zero,
2973 children: <Widget>[SizedBox(width: 1000, height: 100)],
2974 ),
2975 ),
2976 );
2977
2978 await tester.tap(find.text('X'));
2979 await tester.pumpAndSettle();
2980
2981 expect(tester.getSize(find.byType(SizedBox)).width, 560);
2982 });
2983
2984 testWidgets('test no back gesture on fullscreen dialogs', (WidgetTester tester) async {
2985 // no back button in app bar for RawDialogRoute with full screen dialog set to true
2986 await tester.pumpWidget(
2987 MaterialApp(
2988 home: Scaffold(
2989 body: Builder(
2990 builder: (BuildContext context) {
2991 return TextButton(
2992 child: const Text('X'),
2993 onPressed: () {
2994 Navigator.of(context).push(
2995 RawDialogRoute<void>(
2996 pageBuilder:
2997 (
2998 BuildContext context,
2999 Animation<double> animation,
3000 Animation<double> secondaryAnimation,
3001 ) {
3002 return Scaffold(
3003 appBar: AppBar(title: const Text('title')),
3004 body: const Text('body'),
3005 );
3006 },
3007 fullscreenDialog: true,
3008 ),
3009 );
3010 },
3011 );
3012 },
3013 ),
3014 ),
3015 ),
3016 );
3017
3018 await tester.tap(find.text('X'));
3019 await tester.pumpAndSettle();
3020
3021 expect(find.byType(BackButton), findsNothing);
3022 expect(find.byType(CloseButton), findsOneWidget);
3023 });
3024}
3025
3026@pragma('vm:entry-point')
3027class _RestorableDialogTestWidget extends StatelessWidget {
3028 const _RestorableDialogTestWidget();
3029
3030 @pragma('vm:entry-point')
3031 static Route<Object?> _materialDialogBuilder(BuildContext context, Object? arguments) {
3032 return DialogRoute<void>(
3033 context: context,
3034 builder: (BuildContext context) => const AlertDialog(title: Text('Material Alert!')),
3035 );
3036 }
3037
3038 @override
3039 Widget build(BuildContext context) {
3040 return Scaffold(
3041 body: Center(
3042 child: OutlinedButton(
3043 onPressed: () {
3044 Navigator.of(context).restorablePush(_materialDialogBuilder);
3045 },
3046 child: const Text('X'),
3047 ),
3048 ),
3049 );
3050 }
3051}
3052
3053class DialogObserver extends NavigatorObserver {
3054 int dialogCount = 0;
3055
3056 @override
3057 void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
3058 if (route is DialogRoute) {
3059 dialogCount++;
3060 }
3061 super.didPush(route, previousRoute);
3062 }
3063}
3064
3065class _ClosureNavigatorObserver extends NavigatorObserver {
3066 _ClosureNavigatorObserver({required this.onDidChange});
3067
3068 final void Function(Route<dynamic> newRoute) onDidChange;
3069
3070 @override
3071 void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(route);
3072
3073 @override
3074 void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(previousRoute!);
3075
3076 @override
3077 void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) =>
3078 onDidChange(previousRoute!);
3079
3080 @override
3081 void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => onDidChange(newRoute!);
3082}
3083