// Copyright 2016 The Chromium 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/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:matcher/matcher.dart'; import '../widgets/semantics_tester.dart'; MaterialApp _appWithAlertDialog(WidgetTester tester, AlertDialog dialog, { ThemeData theme }) { return MaterialApp( theme: theme, home: Material( child: Builder( builder: (BuildContext context) { return Center( child: RaisedButton( child: const Text('X'), onPressed: () { showDialog<void>( context: context, builder: (BuildContext context) { return dialog; }, ); }, ), ); } ), ), ); } Material _getMaterialFromDialog(WidgetTester tester) { return tester.widget<Material>(find.descendant(of: find.byType(AlertDialog), matching: find.byType(Material))); } RenderParagraph _getTextRenderObjectFromDialog(WidgetTester tester, String text) { return tester.element<StatelessElement>(find.descendant(of: find.byType(AlertDialog), matching: find.text(text))).renderObject; } const ShapeBorder _defaultDialogShape = RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(2.0))); void main() { testWidgets('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>[ FlatButton( onPressed: () { didPressOk = true; }, child: const Text('OK'), ), ], ); await tester.pumpWidget(_appWithAlertDialog(tester, 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 AlertDialog dialog = AlertDialog( backgroundColor: customColor, actions: <Widget>[ ], ); await tester.pumpWidget(_appWithAlertDialog(tester, 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(_appWithAlertDialog(tester, dialog, theme: ThemeData(brightness: Brightness.dark))); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.color, Colors.grey[800]); expect(materialWidget.shape, _defaultDialogShape); expect(materialWidget.elevation, 24.0); }); testWidgets('Custom dialog elevation', (WidgetTester tester) async { const double customElevation = 12.0; const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], elevation: customElevation, ); await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.elevation, customElevation); }); testWidgets('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(_appWithAlertDialog(tester, 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 String contentText = 'Content'; const TextStyle contentTextStyle = TextStyle(color: Colors.pink); const AlertDialog dialog = AlertDialog( content: Text(contentText), contentTextStyle: contentTextStyle, actions: <Widget>[ ], ); await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final RenderParagraph content = _getTextRenderObjectFromDialog(tester, contentText); expect(content.text.style, contentTextStyle); }); testWidgets('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(_appWithAlertDialog(tester, 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 { const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], shape: null, ); await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.shape, _defaultDialogShape); }); testWidgets('Rectangular dialog shape', (WidgetTester tester) async { const ShapeBorder customBorder = Border(); const AlertDialog dialog = AlertDialog( actions: <Widget>[ ], shape: customBorder, ); await tester.pumpWidget(_appWithAlertDialog(tester, dialog)); await tester.tap(find.text('X')); await tester.pumpAndSettle(); final Material materialWidget = _getMaterialFromDialog(tester); expect(materialWidget.shape, customBorder); }); testWidgets('Simple dialog control test', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( child: Center( child: RaisedButton( 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)); }); testWidgets('Barrier dismissible', (WidgetTester tester) async { await tester.pumpWidget( const MaterialApp( home: Material( child: Center( child: RaisedButton( 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); }); testWidgets('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: RaisedButton( 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(); }); testWidgets('Dialogs removes MediaQuery padding and view insets', (WidgetTester tester) async { BuildContext outerContext; BuildContext routeContext; 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); }); 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( viewInsets: EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 0.0), ), 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 widget contains route semantics from title', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget( MaterialApp( home: Material( child: Builder( builder: (BuildContext context) { return Center( child: RaisedButton( 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], ))); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(semantics, includesNodeWith( label: 'Title', flags: <SemanticsFlag>[SemanticsFlag.namesRoute], )); semantics.dispose(); }); testWidgets('Dismissable.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) { return showDialog<bool>( context: _scaffoldKey.currentContext, barrierDismissible: true, // showDialog() returns null if tapped outside the dialog builder: (BuildContext context) { return AlertDialog( actions: <Widget>[ FlatButton( child: const Text('TRUE'), onPressed: () { Navigator.pop(context, true); // showDialog() returns true }, ), FlatButton( 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 RaisedButton( 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(RaisedButton)); 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(); }); }