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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com