// 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/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.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<void>( context: context, traversalEdgeBehavior: traversalEdgeBehavior, builder: (BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: textScaleFactor), child: dialog, ); }, ); }, ), ); }, ), ), ); } Material _getMaterialFromDialog(WidgetTester tester) { return tester.widget<Material>(find.descendant(of: find.byType(Dialog), matching: find.byType(Material))); } RenderParagraph _getTextRenderObjectFromDialog(WidgetTester tester, String text) { return tester.element<StatelessElement>(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 _findButtonBar() { 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 ThemeData material3Theme = ThemeData(useMaterial3: true, brightness: Brightness.dark); final ThemeData material2Theme = ThemeData(useMaterial3: false, brightness: Brightness.dark); testWidgetsWithLeakTracking('Dialog is scrollable', (WidgetTester tester) async { bool didPressOk = false; final AlertDialog dialog = AlertDialog( content: Container( height: 5000.0, width: 300.0, color: Colors.green[500], ), actions: <Widget>[ 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); }); testWidgetsWithLeakTracking('Dialog background color from AlertDialog', (WidgetTester tester) async { const Color customColor = Colors.pink; const AlertDialog dialog = AlertDialog( backgroundColor: customColor, actions: <Widget>[ ], ); 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 Defaults', (WidgetTester tester) async { const AlertDialog dialog = AlertDialog( title: Text('Title'), content: Text('Y'), actions: <Widget>[ ], ); 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); await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(); 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.surface); expect(material3Widget.shape, _defaultM3DialogShape); expect(material3Widget.elevation, 6.0); }); testWidgets('Dialog.fullscreen Defaults', (WidgetTester tester) async { const String dialogTextM2 = 'Fullscreen Dialog - M2'; const String dialogTextM3 = 'Fullscreen Dialog - M3'; 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); 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); 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); 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); }); testWidgetsWithLeakTracking('Custom dialog elevation', (WidgetTester tester) async { const double customElevation = 12.0; const Color shadowColor = Color(0xFF000001); const Color surfaceTintColor = Color(0xFF000002); const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], 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); }); testWidgetsWithLeakTracking('Custom Title Text Style', (WidgetTester tester) async { const String titleText = 'Title'; const TextStyle titleTextStyle = TextStyle(color: Colors.pink); const AlertDialog dialog = AlertDialog( title: Text(titleText), titleTextStyle: titleTextStyle, actions: <Widget>[ ], ); 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); }); testWidgetsWithLeakTracking('Custom Content Text Style', (WidgetTester tester) async { const String contentText = 'Content'; const TextStyle contentTextStyle = TextStyle(color: Colors.pink); const AlertDialog dialog = AlertDialog( content: Text(contentText), contentTextStyle: contentTextStyle, actions: <Widget>[ ], ); 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); }); testWidgetsWithLeakTracking('AlertDialog custom clipBehavior', (WidgetTester tester) async { const AlertDialog dialog = AlertDialog( actions: <Widget>[], 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); }); testWidgetsWithLeakTracking('SimpleDialog custom clipBehavior', (WidgetTester tester) async { const SimpleDialog dialog = SimpleDialog( clipBehavior: Clip.antiAlias, children: <Widget>[], ); await tester.pumpWidget(_buildAppWithDialog(dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.clipBehavior, Clip.antiAlias); }); testWidgetsWithLeakTracking('Custom dialog shape', (WidgetTester tester) async { const RoundedRectangleBorder customBorder = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16.0))); const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], 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); }); testWidgetsWithLeakTracking('Null dialog shape', (WidgetTester tester) async { final ThemeData theme = ThemeData(); const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], ); 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); }); testWidgetsWithLeakTracking('Rectangular dialog shape', (WidgetTester tester) async { const ShapeBorder customBorder = Border(); const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], 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); }); testWidgetsWithLeakTracking('Custom dialog alignment', (WidgetTester tester) async { const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], 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); }); testWidgetsWithLeakTracking('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<int?> result = showDialog<int>( context: context, builder: (BuildContext context) { return SimpleDialog( title: const Text('Title'), children: <Widget>[ 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)); }); testWidgetsWithLeakTracking('Can show dialog using navigator global key', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( navigatorKey: navigator, home: const Material( child: Center( child: Text('Go'), ), ), ), ); final Future<int?> result = showDialog<int>( context: navigator.currentContext!, builder: (BuildContext context) { return SimpleDialog( title: const Text('Title'), children: <Widget>[ 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)); }); testWidgetsWithLeakTracking('Custom padding on SimpleDialogOption', (WidgetTester tester) async { const EdgeInsets customPadding = EdgeInsets.fromLTRB(4, 10, 8, 6); final SimpleDialog dialog = SimpleDialog( title: const Text('Title'), children: <Widget>[ 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<void>( 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<void>( 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); }); testWidgetsWithLeakTracking('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<void>( context: context, builder: (BuildContext context) { return const Text('Dialog'); }, ); await tester.pumpAndSettle(); expect(tester.widget<ModalBarrier>(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<void>( context: context, builder: (BuildContext context) { return const Text('Dialog'); }, barrierColor: Colors.pink, ); await tester.pumpAndSettle(); expect(tester.widget<ModalBarrier>(find.byType(ModalBarrier).last).color, Colors.pink); }); testWidgetsWithLeakTracking('Dialog hides underlying semantics tree', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); const String 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 String alertText = 'A button in an overlay alert'; showDialog<void>( 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(); }); testWidgetsWithLeakTracking('AlertDialog.actionsPadding defaults', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: <Widget>[ 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(_findButtonBar()); expect(actionsSize.width, dialogSize.width); }); testWidgetsWithLeakTracking('AlertDialog.actionsPadding surrounds actions with padding', (WidgetTester tester) async { final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: <Widget>[ 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('AlertDialog.buttonPadding defaults', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: <Widget>[ 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(_findButtonBar()).dy + 8.0, ); // top expect( tester.getBottomRight(find.byKey(key1)).dy, tester.getBottomRight(_findButtonBar()).dy - 8.0, ); // bottom // Second button expect( tester.getTopRight(find.byKey(key2)).dy, tester.getTopRight(_findButtonBar()).dy + 8.0, ); // top expect( tester.getBottomRight(find.byKey(key2)).dy, tester.getBottomRight(_findButtonBar()).dy - 8.0, ); // bottom expect( tester.getBottomRight(find.byKey(key2)).dx, tester.getBottomRight(_findButtonBar()).dx - 8.0, ); // right // Dismiss it and test material 3 dialog await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(); 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(_findButtonBar()).dy, ); // top expect( tester.getBottomRight(find.byKey(key1)).dy, tester.getBottomRight(_findButtonBar()).dy - 24.0, ); // bottom // // Second button expect( tester.getTopRight(find.byKey(key2)).dy, tester.getTopRight(_findButtonBar()).dy, ); // top expect( tester.getBottomRight(find.byKey(key2)).dy, tester.getBottomRight(_findButtonBar()).dy - 24.0, ); // bottom expect( tester.getBottomRight(find.byKey(key2)).dx, tester.getBottomRight(_findButtonBar()).dx - 24.0, ); // right }); testWidgetsWithLeakTracking('AlertDialog.buttonPadding custom values', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: <Widget>[ 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(_findButtonBar()).dy + ((10.0 + 20.0) / 2), ); // top expect( tester.getBottomRight(find.byKey(key1)).dy, tester.getBottomRight(_findButtonBar()).dy - ((10.0 + 20.0) / 2), ); // bottom // Second button expect( tester.getTopRight(find.byKey(key2)).dy, tester.getTopRight(_findButtonBar()).dy + ((10.0 + 20.0) / 2), ); // top expect( tester.getBottomRight(find.byKey(key2)).dy, tester.getBottomRight(_findButtonBar()).dy - ((10.0 + 20.0) / 2), ); // bottom expect( tester.getBottomRight(find.byKey(key2)).dx, tester.getBottomRight(_findButtonBar()).dx - ((10.0 + 20.0) / 2), ); // right }); group('Dialog children padding is correct', () { final List<double> textScaleFactors = <double>[0.5, 1.0, 1.5, 2.0, 3.0]; final Map<double, double> paddingScaleFactors = <double, double>{ 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 = _findButtonBar(); final Finder childrenFinder = find.byKey(childrenKey); Future<void> 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 List<Widget> actions = <Widget>[ ElevatedButton( onPressed: () {}, child: const Text('button'), ), ]; final List<Widget> children = <Widget>[ SimpleDialogOption( key: childrenKey, child: const Text('child'), onPressed: () { }, ), ]; for (final double textScaleFactor in textScaleFactors) { testWidgetsWithLeakTracking('AlertDialog padding is correct when only icon and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog 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, ); }); testWidgetsWithLeakTracking('AlertDialog padding is correct when only icon, title and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog 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 bool isM3 in <bool>[true, false]) { testWidgetsWithLeakTracking('AlertDialog padding is correct when only icon, content and actions are specified [textScaleFactor]=$textScaleFactor [isM3]=$isM3', (WidgetTester tester) async { final AlertDialog 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, ); }); } testWidgetsWithLeakTracking('AlertDialog padding is correct when only title and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog 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, ); }); testWidgetsWithLeakTracking('AlertDialog padding is correct when only content and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog 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, ); }); testWidgetsWithLeakTracking('AlertDialog padding is correct when title, content, and actions are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final AlertDialog 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, ); }); testWidgetsWithLeakTracking('SimpleDialog padding is correct when only children are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final SimpleDialog 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, ); }); testWidgetsWithLeakTracking('SimpleDialog padding is correct when title and children are specified [textScaleFactor]=$textScaleFactor', (WidgetTester tester) async { final SimpleDialog 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, ); }); } }); testWidgetsWithLeakTracking('Dialogs can set the vertical direction of overflowing actions', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: <Widget>[ 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)); }); testWidgetsWithLeakTracking('Dialogs have no spacing by default for overflowing actions', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: <Widget>[ 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); }); testWidgetsWithLeakTracking('Dialogs can set the button spacing of overflowing actions', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: <Widget>[ 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); }); testWidgetsWithLeakTracking('Dialogs can set the alignment of the OverflowBar', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); final GlobalKey key2 = GlobalKey(); final AlertDialog dialog = AlertDialog( title: const Text('title'), content: const Text('content'), actions: <Widget>[ 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); }); testWidgetsWithLeakTracking('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 <LocalizationsDelegate<dynamic>>[ 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<void>( pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { outerContext = context; return Container(); }, ); }, ), ), )); showDialog<void>( 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); }); testWidgetsWithLeakTracking('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), ); }); testWidgetsWithLeakTracking('Dialog insetPadding added to outside of dialog', (WidgetTester tester) async { // The default testing screen (800, 600) const Rect 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: null, 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. testWidgetsWithLeakTracking('AlertDialog has correct semantics for content in iOS', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: const AlertDialog( title: Text('title'), content: Text('content'), actions: <Widget>[ TextButton(onPressed: null, child: Text('action')) ], ), ), ); expect(semantics, hasSemantics(TestSemantics.root( children: <TestSemantics>[ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: <TestSemantics>[ TestSemantics( id: 2, children: <TestSemantics>[ TestSemantics( id: 3, flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], children: <TestSemantics>[ TestSemantics( id: 4, children: <TestSemantics>[ TestSemantics( id: 5, label: 'title', textDirection: TextDirection.ltr, ), // The content semantics does not merge into the semantics // node 4. TestSemantics( id: 6, label: 'content', textDirection: TextDirection.ltr, ), TestSemantics( id: 7, flags: <SemanticsFlag>[ SemanticsFlag.isButton, SemanticsFlag.hasEnabledState, ], label: 'action', textDirection: TextDirection.ltr, ), ], ), ], ), ], ), ], ), ], ), ignoreTransform: true, ignoreId: true, ignoreRect: true)); semantics.dispose(); }); testWidgetsWithLeakTracking('AlertDialog widget always contains alert route semantics for android', (WidgetTester tester) async { final SemanticsTester 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<void>( context: context, builder: (BuildContext context) { return const AlertDialog( title: Text('Title'), content: Text('Y'), actions: <Widget>[], ); }, ); }, ), ); }, ), ), ), ); expect(semantics, isNot(includesNodeWith( label: 'Title', flags: <SemanticsFlag>[SemanticsFlag.namesRoute], ))); expect(semantics, isNot(includesNodeWith( label: 'Alert', flags: <SemanticsFlag>[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>[SemanticsFlag.namesRoute], ))); expect(semantics, includesNodeWith( label: 'Alert', flags: <SemanticsFlag>[SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute], )); semantics.dispose(); }); testWidgetsWithLeakTracking('SimpleDialog does not introduce additional node', (WidgetTester tester) async { final SemanticsTester 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<void>( 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>[SemanticsFlag.namesRoute, SemanticsFlag.scopesRoute], )); semantics.dispose(); }); // Regression test for https://github.com/flutter/flutter/issues/78229. testWidgetsWithLeakTracking('SimpleDialog has correct semantics for title in iOS', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( theme: ThemeData(platform: TargetPlatform.iOS), home: const SimpleDialog( title: Text('title'), children: <Widget>[ Text('content'), TextButton(onPressed: null, child: Text('action')), ], ), ), ); expect(semantics, hasSemantics(TestSemantics.root( children: <TestSemantics>[ TestSemantics( id: 1, textDirection: TextDirection.ltr, children: <TestSemantics>[ TestSemantics( id: 2, children: <TestSemantics>[ TestSemantics( id: 3, flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], children: <TestSemantics>[ TestSemantics( id: 4, children: <TestSemantics>[ // Title semantics does not merge into the semantics // node 4. TestSemantics( id: 5, label: 'title', textDirection: TextDirection.ltr, ), TestSemantics( id: 6, flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling], children: <TestSemantics>[ TestSemantics( id: 7, label: 'content', textDirection: TextDirection.ltr, ), TestSemantics( id: 8, flags: <SemanticsFlag>[ 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 GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>(); final List<int> dismissedItems = <int>[]; // Dismiss is confirmed IFF confirmDismiss() returns true. Future<bool?> confirmDismiss (DismissDirection dismissDirection) async { return showDialog<bool>( context: scaffoldKey.currentContext!, builder: (BuildContext context) { return AlertDialog( actions: <Widget>[ 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<int>(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: <int>[0, 1, 2, 3, 4] .where((int i) => !dismissedItems.contains(i)) .map<Widget>((int item) => buildDismissibleItem(item, setState)).toList(), ), ), ); }, ), ); } Future<void> 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, <int>[0]); expect(find.text('0'), findsNothing); // Dismiss item 1 is not confirmed via the AlertDialog await tester.pumpWidget(buildFrame()); expect(dismissedItems, <int>[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, <int>[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, <int>[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, <int>[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, <int>[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, <int>[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<void>( 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<void>( 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<void>( 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)); }); testWidgetsWithLeakTracking('showDialog uses root navigator by default', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); await tester.pumpWidget(MaterialApp( navigatorObservers: <NavigatorObserver>[rootObserver], home: Navigator( observers: <NavigatorObserver>[nestedObserver], onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<dynamic>( builder: (BuildContext context) { return ElevatedButton( onPressed: () { showDialog<void>( 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); }); testWidgetsWithLeakTracking('showDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); await tester.pumpWidget(MaterialApp( navigatorObservers: <NavigatorObserver>[rootObserver], home: Navigator( observers: <NavigatorObserver>[nestedObserver], onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<dynamic>( builder: (BuildContext context) { return ElevatedButton( onPressed: () { showDialog<void>( 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); }); testWidgetsWithLeakTracking('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<void>( 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 ErrorSummary summary = error.diagnostics.first as ErrorSummary; expect(summary.toString(), 'This BuildContext is no longer valid.'); } }); group('showDialog avoids overlapping display features', () { testWidgetsWithLeakTracking('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>[ 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<void>( 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)); }); testWidgetsWithLeakTracking('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>[ 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<void>( 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)); }); testWidgetsWithLeakTracking('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>[ 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<void>( 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 AlertDialog 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))); }); testWidgetsWithLeakTracking('Content is scrollable', (WidgetTester tester) async { final Key contentKey = UniqueKey(); final AlertDialog 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 AlertDialog 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: <NavigatorObserver>[ _ClosureNavigatorObserver(onDidChange: (Route<dynamic> 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 RouteSettings exampleSetting = RouteSettings(name: 'simple'); final Future<int?> result = showDialog<int>( context: context, builder: (BuildContext context) { return SimpleDialog( title: const Text('Title'), children: <Widget>[ 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, '/'); }); testWidgetsWithLeakTracking('showDialog - custom barrierLabel', (WidgetTester tester) async { final SemanticsTester 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<void>( context: context, barrierLabel: 'Custom label', builder: (BuildContext context) { return const AlertDialog( title: Text('Title'), content: Text('Y'), actions: <Widget>[], ); }, ); }, ), ); }, ), ), ), ); expect(semantics, isNot(includesNodeWith( label: 'Custom label', flags: <SemanticsFlag>[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 testWidgetsWithLeakTracking('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: <Widget>[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); }); testWidgetsWithLeakTracking('Uses closed loop focus traversal', (WidgetTester tester) async { final FocusNode okNode = FocusNode(); final FocusNode cancelNode = FocusNode(); Future<bool> nextFocus() async { final bool result = Actions.invoke( primaryFocus!.context!, const NextFocusIntent(), )! as bool; await tester.pump(); return result; } Future<bool> previousFocus() async { final bool result = Actions.invoke( primaryFocus!.context!, const PreviousFocusIntent(), )! as bool; await tester.pump(); return result; } final AlertDialog dialog = AlertDialog( content: const Text('Test dialog'), actions: <Widget>[ 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 AlertDialog dialog = AlertDialog.adaptive( content: Container( height: 5000.0, width: 300.0, color: Colors.green[500], ), actions: <Widget>[ TextButton( onPressed: () {}, child: const Text('OK'), ), ], ); for (final TargetPlatform platform in <TargetPlatform>[ 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 TargetPlatform platform in <TargetPlatform>[ 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<void>( 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<void>( 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('Uses open focus traversal when overridden', (WidgetTester tester) async { final FocusNode okNode = FocusNode(); final FocusNode cancelNode = FocusNode(); Future<bool> nextFocus() async { final bool result = Actions.invoke( primaryFocus!.context!, const NextFocusIntent(), )! as bool; await tester.pump(); return result; } final AlertDialog dialog = AlertDialog( content: const Text('Test dialog'), actions: <Widget>[ 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); }); } class _RestorableDialogTestWidget extends StatelessWidget { const _RestorableDialogTestWidget(); @pragma('vm:entry-point') static Route<Object?> _materialDialogBuilder(BuildContext context, Object? arguments) { return DialogRoute<void>( 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<dynamic> route, Route<dynamic>? previousRoute) { if (route is DialogRoute) { dialogCount++; } super.didPush(route, previousRoute); } } class _ClosureNavigatorObserver extends NavigatorObserver { _ClosureNavigatorObserver({required this.onDidChange}); final void Function(Route<dynamic> newRoute) onDidChange; @override void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(route); @override void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(previousRoute!); @override void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) => onDidChange(previousRoute!); @override void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) => onDidChange(newRoute!); }