// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import '../widgets/semantics_tester.dart'; MaterialApp _buildAppWithDialog( Widget dialog, { ThemeData? theme, double textScaleFactor = 1.0, TraversalEdgeBehavior? traversalEdgeBehavior, }) { return MaterialApp( theme: theme, home: Material( child: Builder( builder: (BuildContext context) { return Center( child: ElevatedButton( child: const Text('X'), onPressed: () { showDialog( context: context, traversalEdgeBehavior: traversalEdgeBehavior, builder: (BuildContext context) { return MediaQuery.withClampedTextScaling( minScaleFactor: textScaleFactor, maxScaleFactor: textScaleFactor, child: dialog, ); }, ); }, ), ); }, ), ), ); } Material _getMaterialFromDialog(WidgetTester tester) { return tester.widget( find.descendant(of: find.byType(Dialog), matching: find.byType(Material)), ); } RenderParagraph _getTextRenderObjectFromDialog(WidgetTester tester, String text) { return tester .element( find.descendant(of: find.byType(Dialog), matching: find.text(text)), ) .renderObject! as RenderParagraph; } // What was the AlertDialog's ButtonBar when many of these tests were written, // is now a Padding widget with an OverflowBar child. The Padding widget's size // and location match the original ButtonBar's size and location. Finder _findOverflowBar() { return find.ancestor(of: find.byType(OverflowBar), matching: find.byType(Padding)).first; } const ShapeBorder _defaultM2DialogShape = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(4.0)), ); final ShapeBorder _defaultM3DialogShape = RoundedRectangleBorder( borderRadius: BorderRadius.circular(28.0), ); void main() { final material3Theme = ThemeData(brightness: Brightness.dark); final material2Theme = ThemeData(useMaterial3: false, brightness: Brightness.dark); testWidgets('Dialog is scrollable', (WidgetTester tester) async { var didPressOk = false; final dialog = AlertDialog( content: Container(height: 5000.0, width: 300.0, color: Colors.green[500]), actions: [ TextButton( onPressed: () { didPressOk = true; }, child: const Text('OK'), ), ], ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(didPressOk, false); await tester.tap(find.text('OK')); expect(didPressOk, true); }); testWidgets('Dialog background color from AlertDialog', (WidgetTester tester) async { const Color customColor = Colors.pink; const dialog = AlertDialog(backgroundColor: customColor, actions: []); await tester.pumpWidget( _buildAppWithDialog(dialog, theme: ThemeData(brightness: Brightness.dark)), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.color, customColor); }); testWidgets('Dialog background defaults to ColorScheme.surfaceContainerHigh', ( WidgetTester tester, ) async { final theme = ThemeData( colorScheme: ThemeData().colorScheme.copyWith( surface: Colors.orange, background: Colors.green, surfaceContainerHigh: Colors.red, ), ); const dialog = Dialog(child: SizedBox(width: 200, height: 200)); await tester.pumpWidget(_buildAppWithDialog(dialog, theme: theme)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.color, theme.colorScheme.surfaceContainerHigh); }); testWidgets('Material2 - Dialog Defaults', (WidgetTester tester) async { const dialog = AlertDialog(title: Text('Title'), content: Text('Y'), actions: []); await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material2Theme)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.color, Colors.grey[800]); expect(materialWidget.shape, _defaultM2DialogShape); expect(materialWidget.elevation, 24.0); final Offset bottomLeft = tester.getBottomLeft( find.descendant(of: find.byType(Dialog), matching: find.byType(Material)), ); expect(bottomLeft.dy, 360.0); }); testWidgets('Material3 - Dialog Defaults', (WidgetTester tester) async { const dialog = AlertDialog(title: Text('Title'), content: Text('Y'), actions: []); await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material3Theme)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material material3Widget = _getMaterialFromDialog(tester); expect(material3Widget.color, material3Theme.colorScheme.surfaceContainerHigh); expect(material3Widget.shape, _defaultM3DialogShape); expect(material3Widget.elevation, 6.0); }); testWidgets('Material2 - Dialog.fullscreen Defaults', (WidgetTester tester) async { const dialogTextM2 = 'Fullscreen Dialog - M2'; await tester.pumpWidget( _buildAppWithDialog( theme: material2Theme, const Dialog.fullscreen(child: Text(dialogTextM2)), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(find.text(dialogTextM2), findsOneWidget); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.color, Colors.grey[800]); // Try to dismiss the fullscreen dialog with the escape key. await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pumpAndSettle(); expect(find.text(dialogTextM2), findsNothing); }); testWidgets('Material3 - Dialog.fullscreen Defaults', (WidgetTester tester) async { const dialogTextM3 = 'Fullscreen Dialog - M3'; await tester.pumpWidget( _buildAppWithDialog( theme: material3Theme, const Dialog.fullscreen(child: Text(dialogTextM3)), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(find.text(dialogTextM3), findsOneWidget); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.color, material3Theme.colorScheme.surface); // Try to dismiss the fullscreen dialog with the escape key. await tester.sendKeyEvent(LogicalKeyboardKey.escape); await tester.pumpAndSettle(); expect(find.text(dialogTextM3), findsNothing); }); testWidgets('Custom dialog elevation', (WidgetTester tester) async { const customElevation = 12.0; const shadowColor = Color(0xFF000001); const surfaceTintColor = Color(0xFF000002); const dialog = AlertDialog( actions: [], elevation: customElevation, shadowColor: shadowColor, surfaceTintColor: surfaceTintColor, ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.elevation, customElevation); expect(materialWidget.shadowColor, shadowColor); expect(materialWidget.surfaceTintColor, surfaceTintColor); }); testWidgets('Custom Title Text Style', (WidgetTester tester) async { const titleText = 'Title'; const titleTextStyle = TextStyle(color: Colors.pink); const dialog = AlertDialog( title: Text(titleText), titleTextStyle: titleTextStyle, actions: [], ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final RenderParagraph title = _getTextRenderObjectFromDialog(tester, titleText); expect(title.text.style, titleTextStyle); }); testWidgets('Custom Content Text Style', (WidgetTester tester) async { const contentText = 'Content'; const contentTextStyle = TextStyle(color: Colors.pink); const dialog = AlertDialog( content: Text(contentText), contentTextStyle: contentTextStyle, actions: [], ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(content.text.style, contentTextStyle); }); testWidgets('AlertDialog custom clipBehavior', (WidgetTester tester) async { const dialog = AlertDialog(actions: [], clipBehavior: Clip.antiAlias); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.clipBehavior, Clip.antiAlias); }); testWidgets('SimpleDialog custom clipBehavior', (WidgetTester tester) async { const dialog = SimpleDialog(clipBehavior: Clip.antiAlias, children: []); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.clipBehavior, Clip.antiAlias); }); testWidgets('Custom dialog shape', (WidgetTester tester) async { const customBorder = RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(16.0)), ); const dialog = AlertDialog(actions: [], shape: customBorder); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.shape, customBorder); }); testWidgets('Null dialog shape', (WidgetTester tester) async { final theme = ThemeData(); const dialog = AlertDialog(actions: []); await tester.pumpWidget(_buildAppWithDialog(dialog, theme: theme)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect( materialWidget.shape, theme.useMaterial3 ? _defaultM3DialogShape : _defaultM2DialogShape, ); }); testWidgets('Rectangular dialog shape', (WidgetTester tester) async { const ShapeBorder customBorder = Border(); const dialog = AlertDialog(actions: [], shape: customBorder); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.shape, customBorder); }); testWidgets('Custom dialog alignment', (WidgetTester tester) async { const dialog = AlertDialog(actions: [], alignment: Alignment.bottomLeft); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Offset bottomLeft = tester.getBottomLeft( find.descendant(of: find.byType(Dialog), matching: find.byType(Material)), ); expect(bottomLeft.dx, 40.0); expect(bottomLeft.dy, 576.0); }); testWidgets('Simple dialog control test', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))), ), ), ); final BuildContext context = tester.element(find.text('Go')); final Future result = showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: const Text('Title'), children: [ SimpleDialogOption( onPressed: () { Navigator.pop(context, 42); }, child: const Text('First option'), ), const SimpleDialogOption(child: Text('Second option')), ], ); }, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Title'), findsOneWidget); await tester.tap(find.text('First option')); expect(await result, equals(42)); }); testWidgets('Can show dialog using navigator global key', (WidgetTester tester) async { final navigator = GlobalKey(); await tester.pumpWidget( MaterialApp( navigatorKey: navigator, home: const Material(child: Center(child: Text('Go'))), ), ); final Future result = showDialog( context: navigator.currentContext!, builder: (BuildContext context) { return SimpleDialog( title: const Text('Title'), children: [ SimpleDialogOption( onPressed: () { Navigator.pop(context, 42); }, child: const Text('First option'), ), const SimpleDialogOption(child: Text('Second option')), ], ); }, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Title'), findsOneWidget); await tester.tap(find.text('First option')); expect(await result, equals(42)); }); testWidgets('Custom padding on SimpleDialogOption', (WidgetTester tester) async { const customPadding = EdgeInsets.fromLTRB(4, 10, 8, 6); final dialog = SimpleDialog( title: const Text('Title'), children: [ SimpleDialogOption( onPressed: () {}, padding: customPadding, child: const Text('First option'), ), ], ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Rect dialogRect = tester.getRect(find.byType(SimpleDialogOption)); final Rect textRect = tester.getRect(find.text('First option')); expect(textRect.left, dialogRect.left + customPadding.left); expect(textRect.top, dialogRect.top + customPadding.top); expect(textRect.right, dialogRect.right - customPadding.right); expect(textRect.bottom, dialogRect.bottom - customPadding.bottom); }); testWidgets('Barrier dismissible', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))), ), ), ); final BuildContext context = tester.element(find.text('Go')); showDialog( context: context, builder: (BuildContext context) { return Container( width: 100.0, height: 100.0, alignment: Alignment.center, child: const Text('Dialog1'), ); }, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog1'), findsOneWidget); // Tap on the barrier. await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog1'), findsNothing); showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return Container( width: 100.0, height: 100.0, alignment: Alignment.center, child: const Text('Dialog2'), ); }, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog2'), findsOneWidget); // Tap on the barrier, which shouldn't do anything this time. await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog2'), findsOneWidget); }); testWidgets('Barrier color', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: Center(child: Text('Test')))); final BuildContext context = tester.element(find.text('Test')); // Test default barrier color showDialog( context: context, builder: (BuildContext context) { return const Text('Dialog'); }, ); await tester.pumpAndSettle(); expect(tester.widget(find.byType(ModalBarrier).last).color, Colors.black54); // Dismiss it and test a custom barrier color await tester.tapAt(const Offset(10.0, 10.0)); showDialog( context: context, builder: (BuildContext context) { return const Text('Dialog'); }, barrierColor: Colors.pink, ); await tester.pumpAndSettle(); expect(tester.widget(find.byType(ModalBarrier).last).color, Colors.pink); }); testWidgets('Dialog hides underlying semantics tree', (WidgetTester tester) async { final semantics = SemanticsTester(tester); const buttonText = 'A button covered by dialog overlay'; await tester.pumpWidget( const MaterialApp( home: Material( child: Center(child: ElevatedButton(onPressed: null, child: Text(buttonText))), ), ), ); expect(semantics, includesNodeWith(label: buttonText)); final BuildContext context = tester.element(find.text(buttonText)); const alertText = 'A button in an overlay alert'; showDialog( context: context, builder: (BuildContext context) { return const AlertDialog(title: Text(alertText)); }, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(semantics, includesNodeWith(label: alertText)); expect(semantics, isNot(includesNodeWith(label: buttonText))); semantics.dispose(); }); testWidgets('AlertDialog.actionsPadding defaults', (WidgetTester tester) async { final dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: [ElevatedButton(onPressed: () {}, child: const Text('button'))], ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); // The [AlertDialog] is the entire screen, since it also contains the scrim. // The first [Material] child of [AlertDialog] is the actual dialog // itself. final Size dialogSize = tester.getSize( find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)).first, ); final Size actionsSize = tester.getSize(_findOverflowBar()); expect(actionsSize.width, dialogSize.width); }); testWidgets('AlertDialog.actionsPadding surrounds actions with padding', ( WidgetTester tester, ) async { final dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: [ElevatedButton(onPressed: () {}, child: const Text('button'))], // The OverflowBar is inset by the buttonPadding/2 + actionsPadding buttonPadding: EdgeInsets.zero, actionsPadding: const EdgeInsets.all(30.0), // custom padding value ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); // The [AlertDialog] is the entire screen, since it also contains the scrim. // The first [Material] child of [AlertDialog] is the actual dialog // itself. final Size dialogSize = tester.getSize( find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material)).first, ); final Size actionsSize = tester.getSize(find.byType(OverflowBar)); expect(actionsSize.width, dialogSize.width - (30.0 * 2)); }); testWidgets('Material2 - AlertDialog.buttonPadding defaults', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: [ ElevatedButton(key: key1, onPressed: () {}, child: const Text('button 1')), ElevatedButton(key: key2, onPressed: () {}, child: const Text('button 2')), ], ); await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material2Theme)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); // Padding between both buttons expect( tester.getBottomLeft(find.byKey(key2)).dx, tester.getBottomRight(find.byKey(key1)).dx + 8.0, ); // Padding between button and edges of the button bar // First button expect( tester.getTopRight(find.byKey(key1)).dy, tester.getTopRight(_findOverflowBar()).dy + 8.0, ); // top expect( tester.getBottomRight(find.byKey(key1)).dy, tester.getBottomRight(_findOverflowBar()).dy - 8.0, ); // bottom // Second button expect( tester.getTopRight(find.byKey(key2)).dy, tester.getTopRight(_findOverflowBar()).dy + 8.0, ); // top expect( tester.getBottomRight(find.byKey(key2)).dy, tester.getBottomRight(_findOverflowBar()).dy - 8.0, ); // bottom expect( tester.getBottomRight(find.byKey(key2)).dx, tester.getBottomRight(_findOverflowBar()).dx - 8.0, ); // right }); testWidgets('Material3 - AlertDialog.buttonPadding defaults', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: [ ElevatedButton(key: key1, onPressed: () {}, child: const Text('button 1')), ElevatedButton(key: key2, onPressed: () {}, child: const Text('button 2')), ], ); await tester.pumpWidget(_buildAppWithDialog(dialog, theme: material3Theme)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); // Padding between both buttons expect( tester.getBottomLeft(find.byKey(key2)).dx, tester.getBottomRight(find.byKey(key1)).dx + 8.0, ); // Padding between button and edges of the button bar // First button expect( tester.getTopRight(find.byKey(key1)).dy, tester.getTopRight(_findOverflowBar()).dy, ); // top expect( tester.getBottomRight(find.byKey(key1)).dy, tester.getBottomRight(_findOverflowBar()).dy - 24.0, ); // bottom // // Second button expect( tester.getTopRight(find.byKey(key2)).dy, tester.getTopRight(_findOverflowBar()).dy, ); // top expect( tester.getBottomRight(find.byKey(key2)).dy, tester.getBottomRight(_findOverflowBar()).dy - 24.0, ); // bottom expect( tester.getBottomRight(find.byKey(key2)).dx, tester.getBottomRight(_findOverflowBar()).dx - 24.0, ); // right }); testWidgets('AlertDialog.buttonPadding custom values', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: [ ElevatedButton(key: key1, onPressed: () {}, child: const Text('button 1')), ElevatedButton(key: key2, onPressed: () {}, child: const Text('button 2')), ], buttonPadding: const EdgeInsets.only(left: 10.0, right: 20.0), ); await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(useMaterial3: false))); await tester.tap(find.text('X')); await tester.pumpAndSettle(); // Padding between both buttons expect( tester.getBottomLeft(find.byKey(key2)).dx, tester.getBottomRight(find.byKey(key1)).dx + ((10.0 + 20.0) / 2), ); // Padding between button and edges of the button bar // First button expect( tester.getTopRight(find.byKey(key1)).dy, tester.getTopRight(_findOverflowBar()).dy + ((10.0 + 20.0) / 2), ); // top expect( tester.getBottomRight(find.byKey(key1)).dy, tester.getBottomRight(_findOverflowBar()).dy - ((10.0 + 20.0) / 2), ); // bottom // Second button expect( tester.getTopRight(find.byKey(key2)).dy, tester.getTopRight(_findOverflowBar()).dy + ((10.0 + 20.0) / 2), ); // top expect( tester.getBottomRight(find.byKey(key2)).dy, tester.getBottomRight(_findOverflowBar()).dy - ((10.0 + 20.0) / 2), ); // bottom expect( tester.getBottomRight(find.byKey(key2)).dx, tester.getBottomRight(_findOverflowBar()).dx - ((10.0 + 20.0) / 2), ); // right }); group('Dialog children padding is correct', () { final textScaleFactors = [0.5, 1.0, 1.5, 2.0, 3.0]; final paddingScaleFactors = { 0.5: 1.0, 1.0: 1.0, 1.5: 2.0 / 3.0, 2.0: 1.0 / 3.0, 3.0: 1.0 / 3.0, }; final GlobalKey iconKey = GlobalKey(); final GlobalKey titleKey = GlobalKey(); final GlobalKey contentKey = GlobalKey(); final GlobalKey childrenKey = GlobalKey(); final Finder dialogFinder = find .descendant(of: find.byType(Dialog), matching: find.byType(Material)) .first; final Finder iconFinder = find.byKey(iconKey); final Finder titleFinder = find.byKey(titleKey); final Finder contentFinder = find.byKey(contentKey); final Finder actionsFinder = _findOverflowBar(); final Finder childrenFinder = find.byKey(childrenKey); Future openDialog( WidgetTester tester, Widget dialog, double textScaleFactor, { bool isM3 = false, }) async { await tester.pumpWidget( _buildAppWithDialog( dialog, textScaleFactor: textScaleFactor, theme: ThemeData(useMaterial3: isM3), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); } void expectLeftEdgePadding( WidgetTester tester, { required Finder finder, required double textScaleFactor, required double unscaledValue, }) { expect( tester.getTopLeft(dialogFinder).dx, moreOrLessEquals( tester.getTopLeft(finder).dx - unscaledValue * paddingScaleFactors[textScaleFactor]!, ), ); expect( tester.getBottomLeft(dialogFinder).dx, moreOrLessEquals( tester.getBottomLeft(finder).dx - unscaledValue * paddingScaleFactors[textScaleFactor]!, ), ); } void expectRightEdgePadding( WidgetTester tester, { required Finder finder, required double textScaleFactor, required double unscaledValue, }) { expect( tester.getTopRight(dialogFinder).dx, moreOrLessEquals( tester.getTopRight(finder).dx + unscaledValue * paddingScaleFactors[textScaleFactor]!, ), ); expect( tester.getBottomRight(dialogFinder).dx, moreOrLessEquals( tester.getBottomRight(finder).dx + unscaledValue * paddingScaleFactors[textScaleFactor]!, ), ); } void expectTopEdgePadding( WidgetTester tester, { required Finder finder, required double textScaleFactor, required double unscaledValue, }) { expect( tester.getTopLeft(dialogFinder).dy, moreOrLessEquals( tester.getTopLeft(finder).dy - unscaledValue * paddingScaleFactors[textScaleFactor]!, ), ); expect( tester.getTopRight(dialogFinder).dy, moreOrLessEquals( tester.getTopRight(finder).dy - unscaledValue * paddingScaleFactors[textScaleFactor]!, ), ); } void expectBottomEdgePadding( WidgetTester tester, { required Finder finder, required double textScaleFactor, required double unscaledValue, }) { expect( tester.getBottomLeft(dialogFinder).dy, moreOrLessEquals( tester.getBottomRight(finder).dy + unscaledValue * paddingScaleFactors[textScaleFactor]!, ), ); expect( tester.getBottomRight(dialogFinder).dy, moreOrLessEquals( tester.getBottomRight(finder).dy + unscaledValue * paddingScaleFactors[textScaleFactor]!, ), ); } void expectVerticalInnerPadding( WidgetTester tester, { required Finder top, required Finder bottom, required double value, }) { expect(tester.getBottomLeft(top).dy, tester.getTopLeft(bottom).dy - value); expect(tester.getBottomRight(top).dy, tester.getTopRight(bottom).dy - value); } final Widget icon = Icon(Icons.ac_unit, key: iconKey); final Widget title = Text('title', key: titleKey); final Widget content = Text('content', key: contentKey); final actions = [ElevatedButton(onPressed: () {}, child: const Text('button'))]; final children = [ SimpleDialogOption(key: childrenKey, child: const Text('child'), onPressed: () {}), ]; for (final textScaleFactor in textScaleFactors) { testWidgets( 'AlertDialog padding is correct when only icon and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final dialog = AlertDialog(icon: icon, actions: actions); await openDialog(tester, dialog, textScaleFactor); expectTopEdgePadding( tester, finder: iconFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectLeftEdgePadding( tester, finder: iconFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: iconFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding(tester, top: iconFinder, bottom: actionsFinder, value: 24.0); expectLeftEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectRightEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectBottomEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); }, ); testWidgets( 'AlertDialog padding is correct when only icon, title and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final dialog = AlertDialog(icon: icon, title: title, actions: actions); await openDialog(tester, dialog, textScaleFactor); expectTopEdgePadding( tester, finder: iconFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectLeftEdgePadding( tester, finder: iconFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: iconFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding(tester, top: iconFinder, bottom: titleFinder, value: 16.0); expectLeftEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding(tester, top: titleFinder, bottom: actionsFinder, value: 20.0); expectLeftEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectRightEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectBottomEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); }, ); for (final isM3 in [true, false]) { testWidgets( 'AlertDialog padding is correct when only icon, content and actions are specified [textScaleFactor]=$textScaleFactor [isM3]=$isM3', (WidgetTester tester) async { final dialog = AlertDialog(icon: icon, content: content, actions: actions); await openDialog(tester, dialog, textScaleFactor, isM3: isM3); expectTopEdgePadding( tester, finder: iconFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectLeftEdgePadding( tester, finder: iconFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: iconFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding( tester, top: iconFinder, bottom: contentFinder, value: isM3 ? 16.0 : 20.0, ); expectLeftEdgePadding( tester, finder: contentFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: contentFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding( tester, top: contentFinder, bottom: actionsFinder, value: 24.0, ); expectLeftEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectRightEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectBottomEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); }, ); } testWidgets( 'AlertDialog padding is correct when only title and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final dialog = AlertDialog(title: title, actions: actions); await openDialog(tester, dialog, textScaleFactor); expectTopEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectLeftEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding(tester, top: titleFinder, bottom: actionsFinder, value: 20.0); expectLeftEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectRightEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectBottomEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); }, ); testWidgets( 'AlertDialog padding is correct when only content and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final dialog = AlertDialog(content: content, actions: actions); await openDialog(tester, dialog, textScaleFactor); expectTopEdgePadding( tester, finder: contentFinder, textScaleFactor: textScaleFactor, unscaledValue: 20.0, ); expectLeftEdgePadding( tester, finder: contentFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: contentFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding( tester, top: contentFinder, bottom: actionsFinder, value: 24.0, ); expectLeftEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectRightEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectBottomEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); }, ); testWidgets( 'AlertDialog padding is correct when title, content, and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final dialog = AlertDialog(title: title, content: content, actions: actions); await openDialog(tester, dialog, textScaleFactor); expectTopEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectLeftEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding(tester, top: titleFinder, bottom: contentFinder, value: 20.0); expectLeftEdgePadding( tester, finder: contentFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: contentFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding( tester, top: contentFinder, bottom: actionsFinder, value: 24.0, ); expectLeftEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectRightEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectBottomEdgePadding( tester, finder: actionsFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); }, ); testWidgets( 'SimpleDialog padding is correct when only children are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final dialog = SimpleDialog(children: children); await openDialog(tester, dialog, textScaleFactor); expectTopEdgePadding( tester, finder: childrenFinder, textScaleFactor: textScaleFactor, unscaledValue: 12.0, ); expectLeftEdgePadding( tester, finder: childrenFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectRightEdgePadding( tester, finder: childrenFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectBottomEdgePadding( tester, finder: childrenFinder, textScaleFactor: textScaleFactor, unscaledValue: 16.0, ); }, ); testWidgets( 'SimpleDialog padding is correct when title and children are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final dialog = SimpleDialog(title: title, children: children); await openDialog(tester, dialog, textScaleFactor); expectTopEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectLeftEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectRightEdgePadding( tester, finder: titleFinder, textScaleFactor: textScaleFactor, unscaledValue: 24.0, ); expectVerticalInnerPadding(tester, top: titleFinder, bottom: childrenFinder, value: 12.0); expectLeftEdgePadding( tester, finder: childrenFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectRightEdgePadding( tester, finder: childrenFinder, textScaleFactor: textScaleFactor, unscaledValue: 0.0, ); expectBottomEdgePadding( tester, finder: childrenFinder, textScaleFactor: textScaleFactor, unscaledValue: 16.0, ); }, ); } }); testWidgets('Dialogs can set the vertical direction of overflowing actions', ( WidgetTester tester, ) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: [ ElevatedButton( key: key1, onPressed: () {}, child: const Text('Looooooooooooooong button 1'), ), ElevatedButton( key: key2, onPressed: () {}, child: const Text('Looooooooooooooong button 2'), ), ], actionsOverflowDirection: VerticalDirection.up, ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Rect buttonOneRect = tester.getRect(find.byKey(key1)); final Rect buttonTwoRect = tester.getRect(find.byKey(key2)); // Second [ElevatedButton] should appear above the first. expect(buttonTwoRect.bottom, lessThanOrEqualTo(buttonOneRect.top)); }); testWidgets('Dialogs have no spacing by default for overflowing actions', ( WidgetTester tester, ) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: [ ElevatedButton( key: key1, onPressed: () {}, child: const Text('Looooooooooooooong button 1'), ), ElevatedButton( key: key2, onPressed: () {}, child: const Text('Looooooooooooooong button 2'), ), ], ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Rect buttonOneRect = tester.getRect(find.byKey(key1)); final Rect buttonTwoRect = tester.getRect(find.byKey(key2)); expect(buttonOneRect.bottom, buttonTwoRect.top); }); testWidgets('Dialogs can set the button spacing of overflowing actions', ( WidgetTester tester, ) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: [ ElevatedButton( key: key1, onPressed: () {}, child: const Text('Looooooooooooooong button 1'), ), ElevatedButton( key: key2, onPressed: () {}, child: const Text('Looooooooooooooong button 2'), ), ], actionsOverflowButtonSpacing: 10.0, ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Rect buttonOneRect = tester.getRect(find.byKey(key1)); final Rect buttonTwoRect = tester.getRect(find.byKey(key2)); expect(buttonOneRect.bottom, buttonTwoRect.top - 10.0); }); testWidgets('Dialogs can set the alignment of the OverflowBar', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: [ ElevatedButton(key: key1, onPressed: () {}, child: const Text('Loooooooooong button 1')), ElevatedButton( key: key2, onPressed: () {}, child: const Text('Loooooooooooooonger button 2'), ), ], actionsOverflowAlignment: OverflowBarAlignment.center, ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Rect buttonOneRect = tester.getRect(find.byKey(key1)); final Rect buttonTwoRect = tester.getRect(find.byKey(key2)); expect(buttonOneRect.center.dx, buttonTwoRect.center.dx); }); testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async { late BuildContext outerContext; late BuildContext routeContext; late BuildContext dialogContext; await tester.pumpWidget( Localizations( locale: const Locale('en', 'US'), delegates: const >[ DefaultWidgetsLocalizations.delegate, DefaultMaterialLocalizations.delegate, ], child: MediaQuery( data: const MediaQueryData( padding: EdgeInsets.all(50.0), viewInsets: EdgeInsets.only(left: 25.0, bottom: 75.0), ), child: Navigator( onGenerateRoute: (_) { return PageRouteBuilder( pageBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, ) { outerContext = context; return Container(); }, ); }, ), ), ), ); showDialog( context: outerContext, barrierDismissible: false, builder: (BuildContext context) { routeContext = context; return Dialog( child: Builder( builder: (BuildContext context) { dialogContext = context; return const Placeholder(); }, ), ); }, ); await tester.pump(); expect(MediaQuery.of(outerContext).padding, const EdgeInsets.all(50.0)); expect(MediaQuery.of(routeContext).padding, EdgeInsets.zero); expect(MediaQuery.of(dialogContext).padding, EdgeInsets.zero); expect(MediaQuery.of(outerContext).viewInsets, const EdgeInsets.only(left: 25.0, bottom: 75.0)); expect(MediaQuery.of(routeContext).viewInsets, const EdgeInsets.only(left: 25.0, bottom: 75.0)); expect(MediaQuery.of(dialogContext).viewInsets, EdgeInsets.zero); }); testWidgets('Dialog widget insets by viewInsets', (WidgetTester tester) async { await tester.pumpWidget( const MediaQuery( data: MediaQueryData(viewInsets: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0)), child: Dialog(child: Placeholder()), ), ); expect( tester.getRect(find.byType(Placeholder)), const Rect.fromLTRB(10.0 + 40.0, 20.0 + 24.0, 800.0 - (40.0 + 30.0), 600.0 - (24.0 + 40.0)), ); await tester.pumpWidget( const MediaQuery( data: MediaQueryData(), child: Dialog(child: Placeholder()), ), ); expect( // no change because this is an animation tester.getRect(find.byType(Placeholder)), const Rect.fromLTRB(10.0 + 40.0, 20.0 + 24.0, 800.0 - (40.0 + 30.0), 600.0 - (24.0 + 40.0)), ); await tester.pump(const Duration(seconds: 1)); expect( // animation finished tester.getRect(find.byType(Placeholder)), const Rect.fromLTRB(40.0, 24.0, 800.0 - 40.0, 600.0 - 24.0), ); }); testWidgets('Dialog insetPadding added to outside of dialog', (WidgetTester tester) async { // The default testing screen (800, 600) const screenRect = Rect.fromLTRB(0.0, 0.0, 800.0, 600.0); // Test with no padding. await tester.pumpWidget( const MediaQuery( data: MediaQueryData(), child: Dialog(insetPadding: EdgeInsets.zero, child: Placeholder()), ), ); await tester.pumpAndSettle(); expect(tester.getRect(find.byType(Placeholder)), screenRect); // Test with an insetPadding. await tester.pumpWidget( const MediaQuery( data: MediaQueryData(), child: Dialog( insetPadding: EdgeInsets.fromLTRB(10.0, 20.0, 30.0, 40.0), child: Placeholder(), ), ), ); await tester.pumpAndSettle(); expect( tester.getRect(find.byType(Placeholder)), Rect.fromLTRB( screenRect.left + 10.0, screenRect.top + 20.0, screenRect.right - 30.0, screenRect.bottom - 40.0, ), ); }); // Regression test for https://github.com/flutter/flutter/issues/78229. testWidgets('AlertDialog has correct semantics for content in iOS', (WidgetTester tester) async { final semantics = SemanticsTester(tester); const localizations = DefaultMaterialLocalizations(); // With the change to defaultTargetPlatform // (see https://github.com/flutter/flutter/issues/176566), // the actual platform (not theme.platform) determines the semantics. // By default: // On iOS/macOS, no "Alert" label should be added in title. // On other platforms, an "Alert" label is added. await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: const AlertDialog( title: Text('title'), content: Column(children: [Text('some content'), Text('more content')]), actions: [TextButton(onPressed: null, child: Text('action'))], ), ), ); final bool isIOSorMacOS = defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS; expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, role: SemanticsRole.alertDialog, children: isIOSorMacOS ? [ TestSemantics( id: 5, label: 'title', textDirection: TextDirection.ltr, ), // The content semantics does not merge into the semantics // node 4. TestSemantics( id: 6, children: [ TestSemantics( id: 7, label: 'some content', textDirection: TextDirection.ltr, ), TestSemantics( id: 8, label: 'more content', textDirection: TextDirection.ltr, ), ], ), TestSemantics( id: 9, flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, ], label: 'action', textDirection: TextDirection.ltr, ), ] : [ TestSemantics( id: 5, flags: [ SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute, ], label: localizations.alertDialogLabel, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 6, label: 'title', textDirection: TextDirection.ltr, ), TestSemantics( id: 7, children: [ TestSemantics( id: 8, label: 'some content', textDirection: TextDirection.ltr, ), TestSemantics( id: 9, label: 'more content', textDirection: TextDirection.ltr, ), ], ), TestSemantics( id: 10, flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, ], label: 'action', textDirection: TextDirection.ltr, ), ], ), ], ), ], ), ], ), ], ), ], ), ignoreTransform: true, ignoreId: true, ignoreRect: true, ), ); semantics.dispose(); }); testWidgets('AlertDialog widget always contains alert route semantics for android', ( WidgetTester tester, ) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Material( child: Builder( builder: (BuildContext context) { return Center( child: ElevatedButton( child: const Text('X'), onPressed: () { showDialog( context: context, builder: (BuildContext context) { return const AlertDialog( title: Text('Title'), content: Text('Y'), actions: [], ); }, ); }, ), ); }, ), ), ), ); expect( semantics, isNot(includesNodeWith(label: 'Title', flags: [SemanticsFlag.namesRoute])), ); expect( semantics, isNot( includesNodeWith( label: 'Alert', flags: [SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute], ), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); // It does not use 'Title' as route semantics expect( semantics, isNot(includesNodeWith(label: 'Title', flags: [SemanticsFlag.namesRoute])), ); expect( semantics, includesNodeWith( label: 'Alert', flags: [SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute], ), ); semantics.dispose(); }); testWidgets('SimpleDialog does not introduce additional node', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.android), home: Material( child: Builder( builder: (BuildContext context) { return Center( child: ElevatedButton( child: const Text('X'), onPressed: () { showDialog( context: context, builder: (BuildContext context) { return const SimpleDialog(title: Text('Title'), semanticLabel: 'label'); }, ); }, ), ); }, ), ), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); // A scope route is not focusable in accessibility service. expect( semantics, includesNodeWith( label: 'label', flags: [SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute], ), ); semantics.dispose(); }); // Regression test for https://github.com/flutter/flutter/issues/78229. testWidgets('SimpleDialog has correct semantics for title in iOS', (WidgetTester tester) async { final semantics = SemanticsTester(tester); const localizations = DefaultMaterialLocalizations(); // With the change to defaultTargetPlatform // (see https://github.com/flutter/flutter/issues/176566), // the actual platform (not theme.platform) determines the semantics. // By default: // On iOS/macOS, no "Alert" label should be added in title. // On other platforms, an "Alert" label is added. await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: const SimpleDialog( title: Text('title'), children: [ Text('content'), TextButton(onPressed: null, child: Text('action')), ], ), ), ); final bool isIOSorMacOS = defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS; expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 2, children: [ TestSemantics( id: 3, flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( id: 4, role: SemanticsRole.dialog, children: isIOSorMacOS ? [ // Title semantics does not merge into the semantics // node 4. TestSemantics( id: 5, label: 'title', textDirection: TextDirection.ltr, ), TestSemantics( id: 6, flags: [SemanticsFlag.hasImplicitScrolling], children: [ TestSemantics( id: 7, label: 'content', textDirection: TextDirection.ltr, ), TestSemantics( id: 8, flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, ], label: 'action', textDirection: TextDirection.ltr, ), ], ), ] : [ TestSemantics( id: 5, flags: [ SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute, ], label: localizations.dialogLabel, textDirection: TextDirection.ltr, children: [ TestSemantics( id: 6, label: 'title', textDirection: TextDirection.ltr, ), TestSemantics( id: 7, flags: [SemanticsFlag.hasImplicitScrolling], children: [ TestSemantics( id: 8, label: 'content', textDirection: TextDirection.ltr, ), TestSemantics( id: 9, flags: [ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, ], label: 'action', textDirection: TextDirection.ltr, ), ], ), ], ), ], ), ], ), ], ), ], ), ], ), ignoreTransform: true, ignoreId: true, ignoreRect: true, ), ); semantics.dispose(); }); testWidgets('Dismissible.confirmDismiss defers to an AlertDialog', (WidgetTester tester) async { final scaffoldKey = GlobalKey(); final dismissedItems = []; // Dismiss is confirmed IFF confirmDismiss() returns true. Future confirmDismiss(DismissDirection dismissDirection) async { return showDialog( context: scaffoldKey.currentContext!, builder: (BuildContext context) { return AlertDialog( actions: [ TextButton( child: const Text('TRUE'), onPressed: () { Navigator.pop(context, true); // showDialog() returns true }, ), TextButton( child: const Text('FALSE'), onPressed: () { Navigator.pop(context, false); // showDialog() returns false }, ), ], ); }, ); } Widget buildDismissibleItem(int item, StateSetter setState) { return Dismissible( key: ValueKey(item), confirmDismiss: confirmDismiss, onDismissed: (DismissDirection direction) { setState(() { expect(dismissedItems.contains(item), isFalse); dismissedItems.add(item); }); }, child: SizedBox(height: 100.0, child: Text(item.toString())), ); } Widget buildFrame() { return MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { return Scaffold( key: scaffoldKey, body: Padding( padding: const EdgeInsets.all(16.0), child: ListView( itemExtent: 100.0, children: [0, 1, 2, 3, 4] .where((int i) => !dismissedItems.contains(i)) .map((int item) => buildDismissibleItem(item, setState)) .toList(), ), ), ); }, ), ); } Future dismissItem(WidgetTester tester, int item) async { await tester.fling( find.text(item.toString()), const Offset(300.0, 0.0), 1000.0, ); // fling to the right await tester.pump(); // start the slide await tester.pump(const Duration(seconds: 1)); // finish the slide and start shrinking... await tester.pump(); // first frame of shrinking animation await tester.pump( const Duration(seconds: 1), ); // finish the shrinking and call the callback... await tester.pump(); // rebuild after the callback removes the entry } // Dismiss item 0 is confirmed via the AlertDialog await tester.pumpWidget(buildFrame()); expect(dismissedItems, isEmpty); await dismissItem(tester, 0); // Causes the AlertDialog to appear per confirmDismiss await tester.pumpAndSettle(); await tester.tap(find.text('TRUE')); // AlertDialog action await tester.pumpAndSettle(); expect(find.text('TRUE'), findsNothing); // Dialog was dismissed expect(find.text('FALSE'), findsNothing); expect(dismissedItems, [0]); expect(find.text('0'), findsNothing); // Dismiss item 1 is not confirmed via the AlertDialog await tester.pumpWidget(buildFrame()); expect(dismissedItems, [0]); await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss await tester.pumpAndSettle(); await tester.tap(find.text('FALSE')); // AlertDialog action await tester.pumpAndSettle(); expect(find.text('TRUE'), findsNothing); // Dialog was dismissed expect(find.text('FALSE'), findsNothing); expect(dismissedItems, [0]); expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); // Dismiss item 1 is not confirmed via the AlertDialog await tester.pumpWidget(buildFrame()); expect(dismissedItems, [0]); await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss await tester.pumpAndSettle(); expect(find.text('FALSE'), findsOneWidget); expect(find.text('TRUE'), findsOneWidget); await tester.tapAt(Offset.zero); // Tap outside of the AlertDialog await tester.pumpAndSettle(); expect(dismissedItems, [0]); expect(find.text('0'), findsNothing); expect(find.text('1'), findsOneWidget); expect(find.text('TRUE'), findsNothing); // Dialog was dismissed expect(find.text('FALSE'), findsNothing); // Dismiss item 1 is confirmed via the AlertDialog await tester.pumpWidget(buildFrame()); expect(dismissedItems, [0]); await dismissItem(tester, 1); // Causes the AlertDialog to appear per confirmDismiss await tester.pumpAndSettle(); await tester.tap(find.text('TRUE')); // AlertDialog action await tester.pumpAndSettle(); expect(find.text('TRUE'), findsNothing); // Dialog was dismissed expect(find.text('FALSE'), findsNothing); expect(dismissedItems, [0, 1]); expect(find.text('0'), findsNothing); expect(find.text('1'), findsNothing); }); // Regression test for https://github.com/flutter/flutter/issues/28505. testWidgets('showDialog only gets Theme from context on the first call', ( WidgetTester tester, ) async { Widget buildFrame(Key builderKey) { return MaterialApp( home: Center( child: Builder( key: builderKey, builder: (BuildContext outerContext) { return ElevatedButton( onPressed: () { showDialog( context: outerContext, builder: (BuildContext innerContext) { return const AlertDialog(title: Text('Title')); }, ); }, child: const Text('Show Dialog'), ); }, ), ), ); } await tester.pumpWidget(buildFrame(UniqueKey())); // Open the dialog. await tester.tap(find.byType(ElevatedButton)); await tester.pumpAndSettle(); // Force the Builder to be recreated (new key) which causes outerContext to // be deactivated. If showDialog()'s implementation were to refer to // outerContext again, it would crash. await tester.pumpWidget(buildFrame(UniqueKey())); await tester.pump(); }); testWidgets('showDialog safe area', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { return MediaQuery( // Set up the safe area to be 20 pixels in from each side data: const MediaQueryData(padding: EdgeInsets.all(20.0)), child: child!, ); }, home: const Center(child: Text('Test')), ), ); final BuildContext context = tester.element(find.text('Test')); // By default it should honor the safe area showDialog( context: context, builder: (BuildContext context) { return const Placeholder(); }, ); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(20.0, 20.0)); expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(780.0, 580.0)); // Dismiss it and test with useSafeArea off await tester.tapAt(const Offset(10.0, 10.0)); showDialog( context: context, builder: (BuildContext context) { return const Placeholder(); }, useSafeArea: false, ); await tester.pumpAndSettle(); // Should take up the whole screen expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero); expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); testWidgets('showDialog uses root navigator by default', (WidgetTester tester) async { final rootObserver = DialogObserver(); final nestedObserver = DialogObserver(); await tester.pumpWidget( MaterialApp( navigatorObservers: [rootObserver], home: Navigator( observers: [nestedObserver], onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { return ElevatedButton( onPressed: () { showDialog( context: context, builder: (BuildContext innerContext) { return const AlertDialog(title: Text('Title')); }, ); }, child: const Text('Show Dialog'), ); }, ); }, ), ), ); // Open the dialog. await tester.tap(find.byType(ElevatedButton)); expect(rootObserver.dialogCount, 1); expect(nestedObserver.dialogCount, 0); }); testWidgets('showDialog uses nested navigator if useRootNavigator is false', ( WidgetTester tester, ) async { final rootObserver = DialogObserver(); final nestedObserver = DialogObserver(); await tester.pumpWidget( MaterialApp( navigatorObservers: [rootObserver], home: Navigator( observers: [nestedObserver], onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) { return ElevatedButton( onPressed: () { showDialog( context: context, useRootNavigator: false, builder: (BuildContext innerContext) { return const AlertDialog(title: Text('Title')); }, ); }, child: const Text('Show Dialog'), ); }, ); }, ), ), ); // Open the dialog. await tester.tap(find.byType(ElevatedButton)); expect(rootObserver.dialogCount, 0); expect(nestedObserver.dialogCount, 1); }); testWidgets('showDialog throws a friendly user message when context is not active', ( WidgetTester tester, ) async { // Regression test for https://github.com/flutter/flutter/issues/12467 await tester.pumpWidget(const MaterialApp(home: Center(child: Text('Test')))); final BuildContext context = tester.element(find.text('Test')); await tester.pumpWidget(const MaterialApp(home: Center())); Object? error; try { showDialog( context: context, builder: (BuildContext innerContext) { return const AlertDialog(title: Text('Title')); }, ); } catch (exception) { error = exception; } expect(error, isNotNull); expect(error, isFlutterError); if (error is FlutterError) { final summary = error.diagnostics.first as ErrorSummary; expect(summary.toString(), 'This BuildContext is no longer valid.'); } }); group('showDialog avoids overlapping display features', () { testWidgets('positioning with anchorPoint', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { return MediaQuery( // Display has a vertical hinge down the middle data: const MediaQueryData( size: Size(800, 600), displayFeatures: [ DisplayFeature( bounds: Rect.fromLTRB(390, 0, 410, 600), type: DisplayFeatureType.hinge, state: DisplayFeatureState.unknown, ), ], ), child: child!, ); }, home: const Center(child: Text('Test')), ), ); final BuildContext context = tester.element(find.text('Test')); showDialog( context: context, builder: (BuildContext context) { return const Placeholder(); }, anchorPoint: const Offset(1000, 0), ); await tester.pumpAndSettle(); // Should take the right side of the screen expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0)); expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); testWidgets('positioning with Directionality', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { return MediaQuery( // Display has a vertical hinge down the middle data: const MediaQueryData( size: Size(800, 600), displayFeatures: [ DisplayFeature( bounds: Rect.fromLTRB(390, 0, 410, 600), type: DisplayFeatureType.hinge, state: DisplayFeatureState.unknown, ), ], ), child: Directionality(textDirection: TextDirection.rtl, child: child!), ); }, home: const Center(child: Text('Test')), ), ); final BuildContext context = tester.element(find.text('Test')); showDialog( context: context, builder: (BuildContext context) { return const Placeholder(); }, ); await tester.pumpAndSettle(); // Since this is RTL, it should place the dialog on the right screen expect(tester.getTopLeft(find.byType(Placeholder)), const Offset(410.0, 0.0)); expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); testWidgets('positioning by default', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( builder: (BuildContext context, Widget? child) { return MediaQuery( // Display has a vertical hinge down the middle data: const MediaQueryData( size: Size(800, 600), displayFeatures: [ DisplayFeature( bounds: Rect.fromLTRB(390, 0, 410, 600), type: DisplayFeatureType.hinge, state: DisplayFeatureState.unknown, ), ], ), child: child!, ); }, home: const Center(child: Text('Test')), ), ); final BuildContext context = tester.element(find.text('Test')); showDialog( context: context, builder: (BuildContext context) { return const Placeholder(); }, ); await tester.pumpAndSettle(); // By default it should place the dialog on the left screen expect(tester.getTopLeft(find.byType(Placeholder)), Offset.zero); expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(390.0, 600.0)); }); }); group('AlertDialog.scrollable: ', () { testWidgets('Title is scrollable', (WidgetTester tester) async { final Key titleKey = UniqueKey(); final dialog = AlertDialog( title: Container(key: titleKey, color: Colors.green, height: 1000), scrollable: true, ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final RenderBox box = tester.renderObject(find.byKey(titleKey)); final Offset originalOffset = box.localToGlobal(Offset.zero); await tester.drag(find.byKey(titleKey), const Offset(0.0, -200.0)); expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -200.0))); }); testWidgets('Content is scrollable', (WidgetTester tester) async { final Key contentKey = UniqueKey(); final dialog = AlertDialog( content: Container(key: contentKey, color: Colors.orange, height: 1000), scrollable: true, ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final RenderBox box = tester.renderObject(find.byKey(contentKey)); final Offset originalOffset = box.localToGlobal(Offset.zero); await tester.drag(find.byKey(contentKey), const Offset(0.0, -200.0)); expect(box.localToGlobal(Offset.zero), equals(originalOffset.translate(0.0, -200.0))); }); testWidgets('Title and content are scrollable', (WidgetTester tester) async { final Key titleKey = UniqueKey(); final Key contentKey = UniqueKey(); final dialog = AlertDialog( title: Container(key: titleKey, color: Colors.green, height: 400), content: Container(key: contentKey, color: Colors.orange, height: 400), scrollable: true, ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final RenderBox title = tester.renderObject(find.byKey(titleKey)); final RenderBox content = tester.renderObject(find.byKey(contentKey)); final Offset titleOriginalOffset = title.localToGlobal(Offset.zero); final Offset contentOriginalOffset = content.localToGlobal(Offset.zero); // Dragging the title widget should scroll both the title // and the content widgets. await tester.drag(find.byKey(titleKey), const Offset(0.0, -200.0)); expect(title.localToGlobal(Offset.zero), equals(titleOriginalOffset.translate(0.0, -200.0))); expect( content.localToGlobal(Offset.zero), equals(contentOriginalOffset.translate(0.0, -200.0)), ); // Dragging the content widget should scroll both the title // and the content widgets. await tester.drag(find.byKey(contentKey), const Offset(0.0, 200.0)); expect(title.localToGlobal(Offset.zero), equals(titleOriginalOffset)); expect(content.localToGlobal(Offset.zero), equals(contentOriginalOffset)); }); }); testWidgets('Dialog with RouteSettings', (WidgetTester tester) async { late RouteSettings currentRouteSetting; await tester.pumpWidget( MaterialApp( navigatorObservers: [ _ClosureNavigatorObserver( onDidChange: (Route newRoute) { currentRouteSetting = newRoute.settings; }, ), ], home: const Material( child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))), ), ), ); final BuildContext context = tester.element(find.text('Go')); const exampleSetting = RouteSettings(name: 'simple'); final Future result = showDialog( context: context, builder: (BuildContext context) { return SimpleDialog( title: const Text('Title'), children: [ SimpleDialogOption( child: const Text('X'), onPressed: () { Navigator.of(context).pop(); }, ), ], ); }, routeSettings: exampleSetting, ); await tester.pumpAndSettle(); expect(find.text('Title'), findsOneWidget); expect(currentRouteSetting, exampleSetting); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(await result, isNull); await tester.pumpAndSettle(); expect(currentRouteSetting.name, '/'); }); testWidgets('showDialog - custom barrierLabel', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: Material( child: Builder( builder: (BuildContext context) { return Center( child: ElevatedButton( child: const Text('X'), onPressed: () { showDialog( context: context, barrierLabel: 'Custom label', builder: (BuildContext context) { return const AlertDialog( title: Text('Title'), content: Text('Y'), actions: [], ); }, ); }, ), ); }, ), ), ), ); expect( semantics, isNot( includesNodeWith(label: 'Custom label', flags: [SemanticsFlag.namesRoute]), ), ); semantics.dispose(); }); testWidgets('DialogRoute is state restorable', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp(restorationScopeId: 'app', home: _RestorableDialogTestWidget()), ); expect(find.byType(AlertDialog), findsNothing); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(find.byType(AlertDialog), findsOneWidget); final TestRestorationData restorationData = await tester.getRestorationData(); await tester.restartAndRestore(); expect(find.byType(AlertDialog), findsOneWidget); // Tap on the barrier. await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(); expect(find.byType(AlertDialog), findsNothing); await tester.restoreFrom(restorationData); expect(find.byType(AlertDialog), findsOneWidget); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 testWidgets('AlertDialog.actionsAlignment', (WidgetTester tester) async { final Key actionKey = UniqueKey(); Widget buildFrame(MainAxisAlignment? alignment) { return MaterialApp( theme: ThemeData(useMaterial3: false), home: Scaffold( body: AlertDialog( content: const SizedBox(width: 800), actionsAlignment: alignment, actions: [SizedBox(key: actionKey, width: 20, height: 20)], buttonPadding: EdgeInsets.zero, insetPadding: EdgeInsets.zero, ), ), ); } // Default configuration await tester.pumpWidget(buildFrame(null)); expect(tester.getTopLeft(find.byType(AlertDialog)).dx, 0); expect(tester.getTopRight(find.byType(AlertDialog)).dx, 800); expect(tester.getSize(find.byType(OverflowBar)).width, 800); expect(tester.getTopLeft(find.byKey(actionKey)).dx, 800 - 20); expect(tester.getTopRight(find.byKey(actionKey)).dx, 800); // All possible alignment values await tester.pumpWidget(buildFrame(MainAxisAlignment.start)); expect(tester.getTopLeft(find.byKey(actionKey)).dx, 0); expect(tester.getTopRight(find.byKey(actionKey)).dx, 20); await tester.pumpWidget(buildFrame(MainAxisAlignment.center)); expect(tester.getTopLeft(find.byKey(actionKey)).dx, (800 - 20) / 2); expect(tester.getTopRight(find.byKey(actionKey)).dx, (800 - 20) / 2 + 20); await tester.pumpWidget(buildFrame(MainAxisAlignment.end)); expect(tester.getTopLeft(find.byKey(actionKey)).dx, 800 - 20); expect(tester.getTopRight(find.byKey(actionKey)).dx, 800); await tester.pumpWidget(buildFrame(MainAxisAlignment.spaceBetween)); expect(tester.getTopLeft(find.byKey(actionKey)).dx, 0); expect(tester.getTopRight(find.byKey(actionKey)).dx, 20); await tester.pumpWidget(buildFrame(MainAxisAlignment.spaceAround)); expect(tester.getTopLeft(find.byKey(actionKey)).dx, (800 - 20) / 2); expect(tester.getTopRight(find.byKey(actionKey)).dx, (800 - 20) / 2 + 20); await tester.pumpWidget(buildFrame(MainAxisAlignment.spaceEvenly)); expect(tester.getTopLeft(find.byKey(actionKey)).dx, (800 - 20) / 2); expect(tester.getTopRight(find.byKey(actionKey)).dx, (800 - 20) / 2 + 20); }); testWidgets('Uses closed loop focus traversal', (WidgetTester tester) async { final okNode = FocusNode(); final cancelNode = FocusNode(); Future nextFocus() async { final result = Actions.invoke(primaryFocus!.context!, const NextFocusIntent())! as bool; await tester.pump(); return result; } Future previousFocus() async { final result = Actions.invoke(primaryFocus!.context!, const PreviousFocusIntent())! as bool; await tester.pump(); return result; } final dialog = AlertDialog( content: const Text('Test dialog'), actions: [ TextButton(focusNode: okNode, onPressed: () {}, child: const Text('OK')), TextButton(focusNode: cancelNode, onPressed: () {}, child: const Text('Cancel')), ], ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); // Start at OK okNode.requestFocus(); await tester.pump(); expect(okNode.hasFocus, true); expect(cancelNode.hasFocus, false); // OK -> Cancel expect(await nextFocus(), true); expect(okNode.hasFocus, false); expect(cancelNode.hasFocus, true); // Cancel -> OK expect(await nextFocus(), true); expect(okNode.hasFocus, true); expect(cancelNode.hasFocus, false); // Cancel <- OK expect(await previousFocus(), true); expect(okNode.hasFocus, false); expect(cancelNode.hasFocus, true); // OK <- Cancel expect(await previousFocus(), true); expect(okNode.hasFocus, true); expect(cancelNode.hasFocus, false); cancelNode.dispose(); okNode.dispose(); }); testWidgets('Adaptive AlertDialog shows correct widget on each platform', ( WidgetTester tester, ) async { final dialog = AlertDialog.adaptive( content: Container(height: 5000.0, width: 300.0, color: Colors.green[500]), actions: [TextButton(onPressed: () {}, child: const Text('OK'))], ); for (final platform in [TargetPlatform.iOS, TargetPlatform.macOS]) { await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(platform: platform))); await tester.pumpAndSettle(); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(find.byType(CupertinoAlertDialog), findsOneWidget); await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(); } for (final platform in [ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows, ]) { await tester.pumpWidget(_buildAppWithDialog(dialog, theme: ThemeData(platform: platform))); await tester.pumpAndSettle(); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(find.byType(CupertinoAlertDialog), findsNothing); await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(); } }); testWidgets('showAdaptiveDialog should not allow dismiss on barrier on iOS by default', ( WidgetTester tester, ) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: const Material( child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))), ), ), ); final BuildContext context = tester.element(find.text('Go')); showDialog( context: context, builder: (BuildContext context) { return Container( width: 100.0, height: 100.0, alignment: Alignment.center, child: const Text('Dialog1'), ); }, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog1'), findsOneWidget); // Tap on the barrier. await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog1'), findsNothing); showAdaptiveDialog( context: context, builder: (BuildContext context) { return Container( width: 100.0, height: 100.0, alignment: Alignment.center, child: const Text('Dialog2'), ); }, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog2'), findsOneWidget); // Tap on the barrier, which shouldn't do anything this time. await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog2'), findsOneWidget); }); testWidgets('Applies AnimationStyle to showAdaptiveDialog', (WidgetTester tester) async { const animationStyle = AnimationStyle(duration: Duration(seconds: 1), curve: Curves.easeInOut); await tester.pumpWidget( const MaterialApp( home: Material( child: Center(child: ElevatedButton(onPressed: null, child: Text('Go'))), ), ), ); final BuildContext context = tester.element(find.text('Go')); showAdaptiveDialog( context: context, builder: (BuildContext context) { return Container( width: 100.0, height: 100.0, alignment: Alignment.center, child: const Text('Dialog1'), ); }, animationStyle: animationStyle, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog1'), findsOneWidget); await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog1'), findsNothing); }); testWidgets('Uses open focus traversal when overridden', (WidgetTester tester) async { final okNode = FocusNode(); addTearDown(okNode.dispose); final cancelNode = FocusNode(); addTearDown(cancelNode.dispose); Future nextFocus() async { final result = Actions.invoke(primaryFocus!.context!, const NextFocusIntent())! as bool; await tester.pump(); return result; } final dialog = AlertDialog( content: const Text('Test dialog'), actions: [ TextButton(focusNode: okNode, onPressed: () {}, child: const Text('OK')), TextButton(focusNode: cancelNode, onPressed: () {}, child: const Text('Cancel')), ], ); await tester.pumpWidget( _buildAppWithDialog(dialog, traversalEdgeBehavior: TraversalEdgeBehavior.leaveFlutterView), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); // Start at OK okNode.requestFocus(); await tester.pump(); expect(okNode.hasFocus, true); expect(cancelNode.hasFocus, false); // OK -> Cancel expect(await nextFocus(), true); expect(okNode.hasFocus, false); expect(cancelNode.hasFocus, true); // Cancel -> nothing expect(await nextFocus(), false); expect(okNode.hasFocus, false); expect(cancelNode.hasFocus, false); }); testWidgets('Dialog.insetPadding is nullable', (WidgetTester tester) async { const dialog = Dialog(); expect(dialog.insetPadding, isNull); }); testWidgets('AlertDialog.insetPadding is nullable', (WidgetTester tester) async { const alertDialog = AlertDialog(); expect(alertDialog.insetPadding, isNull); }); testWidgets('SimpleDialog.insetPadding is nullable', (WidgetTester tester) async { const simpleDialog = SimpleDialog(); expect(simpleDialog.insetPadding, isNull); }); // This is a regression test for https://github.com/flutter/flutter/issues/153983. testWidgets('Can pass a null value to AlertDialog.adaptive clip behavior', ( WidgetTester tester, ) async { for (final clipBehavior in [null, ...Clip.values]) { AlertDialog.adaptive(clipBehavior: clipBehavior); } }); testWidgets('Setting DialogRoute.requestFocus to false does not request focus on the dialog', ( WidgetTester tester, ) async { late BuildContext savedContext; final focusNode = FocusNode(); addTearDown(focusNode.dispose); const dialogText = 'Dialog Text'; await tester.pumpWidget( MaterialApp( home: Material( child: Builder( builder: (BuildContext context) { savedContext = context; return TextField(focusNode: focusNode); }, ), ), ), ); await tester.pump(); FocusNode? getTextFieldFocusNode() { return tester .widget(find.descendant(of: find.byType(TextField), matching: find.byType(Focus))) .focusNode; } // Initially, there is no dialog and the text field has no focus. expect(find.text(dialogText), findsNothing); expect(getTextFieldFocusNode()?.hasFocus, false); // Request focus on the text field. focusNode.requestFocus(); await tester.pump(); expect(getTextFieldFocusNode()?.hasFocus, true); // Bring up dialog. final NavigatorState navigator = Navigator.of(savedContext); navigator.push( DialogRoute( context: savedContext, builder: (BuildContext context) => const Text(dialogText), ), ); await tester.pump(); // The dialog is showing and the text field has lost focus. expect(find.text(dialogText), findsOneWidget); expect(getTextFieldFocusNode()?.hasFocus, false); // Dismiss the dialog. navigator.pop(); await tester.pump(); // The dialog is dismissed and the focus is shifted back to the text field. expect(find.text(dialogText), findsNothing); expect(getTextFieldFocusNode()?.hasFocus, true); // Bring up dialog again with requestFocus to false. navigator.push( ModalBottomSheetRoute( requestFocus: false, isScrollControlled: false, builder: (BuildContext context) => const Text(dialogText), ), ); await tester.pump(); // The dialog is showing and the text field still has focus. expect(find.text(dialogText), findsOneWidget); expect(getTextFieldFocusNode()?.hasFocus, true); }); testWidgets('requestFocus works correctly in showDialog.', (WidgetTester tester) async { final navigatorKey = GlobalKey(); final focusNode = FocusNode(); addTearDown(focusNode.dispose); await tester.pumpWidget( MaterialApp( navigatorKey: navigatorKey, home: Scaffold(body: TextField(focusNode: focusNode)), ), ); focusNode.requestFocus(); await tester.pump(); expect(focusNode.hasFocus, true); showDialog( context: navigatorKey.currentContext!, requestFocus: true, builder: (BuildContext context) => const Text('dialog'), ); await tester.pumpAndSettle(); expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, true); expect(focusNode.hasFocus, false); navigatorKey.currentState!.pop(); await tester.pumpAndSettle(); expect(focusNode.hasFocus, true); showDialog( context: navigatorKey.currentContext!, requestFocus: false, builder: (BuildContext context) => const Text('dialog'), ); await tester.pumpAndSettle(); expect(FocusScope.of(tester.element(find.text('dialog'))).hasFocus, false); expect(focusNode.hasFocus, true); }); testWidgets('Dialog respects the given constraints', (WidgetTester tester) async { await tester.pumpWidget( _buildAppWithDialog( const Dialog( constraints: BoxConstraints(maxWidth: 560), child: SizedBox(width: 1000, height: 100), ), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(tester.getSize(find.byType(SizedBox)).width, 560); }); testWidgets('AlertDialog respects the default constraints', (WidgetTester tester) async { await tester.pumpWidget( _buildAppWithDialog(const AlertDialog(content: SizedBox(), contentPadding: EdgeInsets.zero)), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(tester.getSize(find.byType(SizedBox)).width, 280); }); testWidgets('AlertDialog respects the given constraints', (WidgetTester tester) async { await tester.pumpWidget( _buildAppWithDialog( const AlertDialog( constraints: BoxConstraints(maxWidth: 560), content: SizedBox(width: 1000, height: 100), contentPadding: EdgeInsets.zero, ), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(tester.getSize(find.byType(SizedBox)).width, 560); }); testWidgets('SimpleDialog respects the default constraints', (WidgetTester tester) async { await tester.pumpWidget( _buildAppWithDialog( const SimpleDialog(contentPadding: EdgeInsets.zero, children: [SizedBox()]), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(tester.getSize(find.byType(SizedBox)).width, 280); }); testWidgets('SimpleDialog respects the given constraints', (WidgetTester tester) async { await tester.pumpWidget( _buildAppWithDialog( const SimpleDialog( constraints: BoxConstraints(maxWidth: 560), contentPadding: EdgeInsets.zero, children: [SizedBox(width: 1000, height: 100)], ), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(tester.getSize(find.byType(SizedBox)).width, 560); }); testWidgets('test no back gesture on fullscreen dialogs', (WidgetTester tester) async { // no back button in app bar for RawDialogRoute with full screen dialog set to true await tester.pumpWidget( MaterialApp( home: Scaffold( body: Builder( builder: (BuildContext context) { return TextButton( child: const Text('X'), onPressed: () { Navigator.of(context).push( RawDialogRoute( pageBuilder: ( BuildContext context, Animation animation, Animation secondaryAnimation, ) { return Scaffold( appBar: AppBar(title: const Text('title')), body: const Text('body'), ); }, fullscreenDialog: true, ), ); }, ); }, ), ), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(find.byType(BackButton), findsNothing); expect(find.byType(CloseButton), findsOneWidget); }); testWidgets('SimpleDialog does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Center( child: SizedBox.shrink(child: SimpleDialog(title: Text('X'))), ), ), ); expect(tester.getSize(find.byType(SimpleDialog)), Size.zero); }); testWidgets('SimpleDialogOption does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( child: Center( child: SizedBox.shrink(child: SimpleDialogOption(child: Text('X'))), ), ), ), ); expect(tester.getSize(find.byType(SimpleDialogOption)), Size.zero); }); testWidgets('Dialog does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Center( child: SizedBox.shrink(child: Dialog(child: Text('X'))), ), ), ); expect(tester.getSize(find.byType(Dialog)).isEmpty, isTrue); }); testWidgets('AlertDialog does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Center( child: SizedBox.shrink(child: AlertDialog(content: Text('X'))), ), ), ); expect(tester.getSize(find.byType(AlertDialog)).isEmpty, isTrue); }); // Regression test for https://github.com/flutter/flutter/issues/177001 group('Dialog semantics for mismatched platforms', () { Future pumpDialogWithTheme({ required WidgetTester tester, required Widget dialog, required TargetPlatform themePlatform, }) async { await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: themePlatform), home: Material( child: Builder( builder: (BuildContext context) { return Center( child: ElevatedButton( child: const Text('X'), onPressed: () { showDialog(context: context, builder: (BuildContext context) => dialog); }, ), ); }, ), ), ), ); await tester.tap(find.text('X')); await tester.pumpAndSettle(); } testWidgets('AlertDialog', (WidgetTester tester) async { final semantics = SemanticsTester(tester); const localizations = DefaultMaterialLocalizations(); final bool isIOSorMacOS = defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS; // Test with theme.platform = Android on different real platforms. await pumpDialogWithTheme( tester: tester, themePlatform: TargetPlatform.android, dialog: const AlertDialog(title: Text('Title'), content: Text('Y'), actions: []), ); // Dismiss the first dialog. Navigator.of(tester.element(find.text('Title'))).pop(); await tester.pumpAndSettle(); // Test with theme.platform = iOS on different real platforms. await pumpDialogWithTheme( tester: tester, themePlatform: TargetPlatform.iOS, dialog: const AlertDialog(title: Text('Title'), content: Text('Y'), actions: []), ); if (isIOSorMacOS) { // On iOS/macOS, semantic label should be omitted and namesRoute flag should not exist. expect( semantics, isNot( includesNodeWith( label: localizations.alertDialogLabel, flags: [SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute], ), ), ); } else { // On other platforms, semantic label should be added and namesRoute should exist. expect( semantics, includesNodeWith( label: localizations.alertDialogLabel, flags: [SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute], ), ); } semantics.dispose(); }, variant: TargetPlatformVariant.all()); testWidgets('SimpleDialog', (WidgetTester tester) async { final semantics = SemanticsTester(tester); const localizations = DefaultMaterialLocalizations(); final bool isIOSorMacOS = defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS; // Test with theme.platform = Android on different real platforms. await pumpDialogWithTheme( tester: tester, themePlatform: TargetPlatform.android, dialog: const SimpleDialog( title: Text('Title'), children: [ Text('content'), TextButton(onPressed: null, child: Text('action')), ], ), ); // Dismiss the first dialog. Navigator.of(tester.element(find.text('Title'))).pop(); await tester.pumpAndSettle(); // Test with theme.platform = iOS on different real platforms. await pumpDialogWithTheme( tester: tester, themePlatform: TargetPlatform.iOS, dialog: const SimpleDialog( title: Text('Title'), children: [ Text('content'), TextButton(onPressed: null, child: Text('action')), ], ), ); if (isIOSorMacOS) { // On iOS/macOS, semantic label should be omitted and namesRoute flag should not exist. expect( semantics, isNot( includesNodeWith( label: localizations.dialogLabel, flags: [SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute], ), ), ); } else { // On other platforms, semantic label should be added and namesRoute should exist. expect( semantics, includesNodeWith( label: localizations.dialogLabel, flags: [SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute], ), ); } semantics.dispose(); }, variant: TargetPlatformVariant.all()); }); testWidgets('Dialog has hitTestBehavior.opaque to prevent dismissal on empty areas', ( WidgetTester tester, ) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( home: Material( child: Builder( builder: (BuildContext context) { return ElevatedButton( onPressed: () { showDialog( context: context, builder: (BuildContext context) => const Dialog(child: SizedBox(width: 200, height: 200)), ); }, child: const Text('Show Dialog'), ); }, ), ), ), ); await tester.tap(find.text('Show Dialog')); await tester.pumpAndSettle(); expect(find.byType(Dialog), findsOneWidget); final Semantics routeSemantics = tester.widget( find.ancestor(of: find.byType(Dialog), matching: find.byType(Semantics)).first, ); expect(routeSemantics.properties.hitTestBehavior, SemanticsHitTestBehavior.opaque); semantics.dispose(); }); testWidgets('AlertDialog has hitTestBehavior.opaque via Dialog', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( home: Material( child: Builder( builder: (BuildContext context) { return ElevatedButton( onPressed: () { showDialog( context: context, builder: (BuildContext context) => AlertDialog( title: const Text('Test Dialog'), content: const SizedBox(width: 200, height: 100), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('OK'), ), ], ), ); }, child: const Text('Show Dialog'), ); }, ), ), ), ); await tester.tap(find.text('Show Dialog')); await tester.pumpAndSettle(); expect(find.byType(AlertDialog), findsOneWidget); expect(find.byType(Dialog), findsOneWidget); // Find the route-level Semantics with hitTestBehavior.opaque // (wraps the entire dialog content, above the Dialog widget) final Semantics routeSemantics = tester.widget( find.ancestor(of: find.byType(Dialog), matching: find.byType(Semantics)).first, ); expect(routeSemantics.properties.hitTestBehavior, SemanticsHitTestBehavior.opaque); semantics.dispose(); }); } @pragma('vm:entry-point') class _RestorableDialogTestWidget extends StatelessWidget { const _RestorableDialogTestWidget(); @pragma('vm:entry-point') static Route _materialDialogBuilder(BuildContext context, Object? arguments) { return DialogRoute( context: context, builder: (BuildContext context) => const AlertDialog(title: Text('Material Alert!')), ); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: OutlinedButton( onPressed: () { Navigator.of(context).restorablePush(_materialDialogBuilder); }, child: const Text('X'), ), ), ); } } class DialogObserver extends NavigatorObserver { int dialogCount = 0; @override void didPush(Route route, Route? previousRoute) { if (route is DialogRoute) { dialogCount++; } super.didPush(route, previousRoute); } } class _ClosureNavigatorObserver extends NavigatorObserver { _ClosureNavigatorObserver({required this.onDidChange}); final void Function(Route newRoute) onDidChange; @override void didPush(Route route, Route? previousRoute) => onDidChange(route); @override void didPop(Route route, Route? previousRoute) => onDidChange(previousRoute!); @override void didRemove(Route route, Route? previousRoute) => onDidChange(previousRoute!); @override void didReplace({Route? newRoute, Route? oldRoute}) => onDidChange(newRoute!); }