// 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. // This file is run as part of a reduced test set in CI on Mac and Windows // machines. @Tags(['reduced-test-set']) library; import 'dart:math'; 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'; void main() { testWidgets('Overall appearance is correct for the light theme', (WidgetTester tester) async { await tester.pumpWidget( TestScaffoldApp( theme: const CupertinoThemeData(brightness: Brightness.light), dialog: CupertinoAlertDialog( content: const Text('The content'), actions: [ CupertinoDialogAction(child: const Text('One'), onPressed: () {}), CupertinoDialogAction(child: const Text('Two'), onPressed: () {}), ], ), ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('One'))); await tester.pumpAndSettle(); // This golden file also verifies the structure of an alert dialog that // has a content, no title, and no overscroll for any sections (in contrast // to cupertinoAlertDialog.dark-theme.png). await expectLater( find.byType(CupertinoApp), matchesGoldenFile('cupertinoAlertDialog.overall-light-theme.png'), ); await gesture.up(); }); testWidgets('Overall appearance is correct for the dark theme', (WidgetTester tester) async { await tester.pumpWidget( TestScaffoldApp( theme: const CupertinoThemeData(brightness: Brightness.dark), dialog: CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content'), actions: List.generate( 20, (int i) => CupertinoDialogAction(onPressed: () {}, child: Text('Button $i')), ), ), ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button 0'))); await tester.pumpAndSettle(); // This golden file also verifies the structure of an action sheet that // has both a message and a title, and an overscrolled action section (in // contrast to cupertinoAlertDialog.light-theme.png). await expectLater( find.byType(CupertinoApp), matchesGoldenFile('cupertinoAlertDialog.overall-dark-theme.png'), ); await gesture.up(); }); testWidgets('Taps on button calls onPressed', (WidgetTester tester) async { var didDelete = false; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content'), actions: [ const CupertinoDialogAction(child: Text('Cancel')), CupertinoDialogAction( isDestructiveAction: true, onPressed: () { didDelete = true; Navigator.pop(context); }, child: const Text('Delete'), ), ], ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); expect(didDelete, isFalse); await tester.tap(find.text('Delete')); await tester.pump(); expect(didDelete, isTrue); expect(find.text('Delete'), findsNothing); }); testWidgets('Can tap after scrolling', (WidgetTester tester) async { int? wasPressed; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( actions: List.generate( 20, (int i) => CupertinoDialogAction( onPressed: () { expect(wasPressed, null); wasPressed = i; }, child: Text('Button $i'), ), ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); expect(find.text('Button 19').hitTestable(), findsNothing); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button 1'))); await tester.pumpAndSettle(); // The dragging gesture must be dispatched in at least two segments. // The first movement starts the gesture without setting a delta. await gesture.moveBy(const Offset(0, -20)); await tester.pumpAndSettle(); await gesture.moveBy(const Offset(0, -1000)); await tester.pumpAndSettle(); await gesture.up(); await tester.pumpAndSettle(); expect(find.text('Button 19').hitTestable(), findsOne); await tester.tap(find.text('Button 19')); await tester.pumpAndSettle(); expect(wasPressed, 19); }); testWidgets('Taps at the padding of buttons calls onPressed', (WidgetTester tester) async { // Ensures that the entire button responds to hit tests, not just the text // part. var wasPressed = false; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( actions: [ CupertinoDialogAction( child: const Text('One'), onPressed: () { expect(wasPressed, false); wasPressed = true; Navigator.pop(context); }, ), ], ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(wasPressed, isFalse); await tester.tapAt(tester.getTopLeft(find.text('One')) - const Offset(20, 0)); expect(wasPressed, isTrue); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('One'), findsNothing); }); testWidgets('Taps on a button can be slided to other buttons', (WidgetTester tester) async { int? pressed; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( actions: [ CupertinoDialogAction( child: const Text('One'), onPressed: () { expect(pressed, null); pressed = 1; Navigator.pop(context); }, ), CupertinoDialogAction( child: const Text('Two'), onPressed: () { expect(pressed, null); pressed = 2; Navigator.pop(context); }, ), ], ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); expect(pressed, null); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Two'))); await tester.pumpAndSettle(); await gesture.moveTo(tester.getCenter(find.text('One'))); await tester.pumpAndSettle(); await expectLater( find.byType(CupertinoAlertDialog), matchesGoldenFile('cupertinoAlertDialog.press-drag.png'), ); await gesture.up(); expect(pressed, 1); await tester.pumpAndSettle(); expect(find.text('One'), findsNothing); }); testWidgets('Taps on the content can be slided to other buttons', (WidgetTester tester) async { var wasPressed = false; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), actions: [ CupertinoDialogAction( child: const Text('One'), onPressed: () { expect(wasPressed, false); wasPressed = true; Navigator.pop(context); }, ), ], ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); expect(wasPressed, false); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('The title'))); await tester.pumpAndSettle(); await gesture.moveTo(tester.getCenter(find.text('One'))); await tester.pumpAndSettle(); await gesture.up(); expect(wasPressed, true); await tester.pumpAndSettle(); expect(find.text('One'), findsNothing); }); testWidgets('Taps on the barrier can not be slided to buttons', (WidgetTester tester) async { var wasPressed = false; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), actions: [ CupertinoDialogAction( child: const Text('Cancel'), onPressed: () { expect(wasPressed, false); wasPressed = true; Navigator.pop(context); }, ), ], ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); expect(wasPressed, false); // Press on the barrier. final TestGesture gesture = await tester.startGesture(const Offset(100, 100)); await tester.pumpAndSettle(); await gesture.moveTo(tester.getCenter(find.text('Cancel'))); await tester.pumpAndSettle(); await gesture.up(); expect(wasPressed, false); await tester.pumpAndSettle(); expect(find.text('Cancel'), findsOne); }); testWidgets('Sliding taps can still yield to scrolling after horizontal movement', ( WidgetTester tester, ) async { int? pressed; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( content: Text('Long message' * 200), actions: List.generate( 10, (int i) => CupertinoDialogAction( onPressed: () { expect(pressed, null); pressed = i; }, child: Text('Button $i'), ), ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); // Starts on a button. final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button 0'))); await tester.pumpAndSettle(); // Move horizontally. await gesture.moveBy(const Offset(-10, 2)); await gesture.moveBy(const Offset(-100, 2)); await tester.pumpAndSettle(); // Scroll up. await gesture.moveBy(const Offset(0, -40)); await gesture.moveBy(const Offset(0, -1000)); await tester.pumpAndSettle(); // Stop scrolling. await gesture.up(); await tester.pumpAndSettle(); // The actions section should have been scrolled up and Button 9 is visible. await tester.tap(find.text('Button 9')); expect(pressed, 9); }); testWidgets('Sliding taps is responsive even before the drag starts', ( WidgetTester tester, ) async { int? pressed; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( content: Text('Long message' * 200), actions: List.generate( 10, (int i) => CupertinoDialogAction( onPressed: () { expect(pressed, null); pressed = i; }, child: Text('Button $i'), ), ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); // Find the location right within the upper edge of button 1. final Offset start = tester.getTopLeft(find.widgetWithText(CupertinoDialogAction, 'Button 1')) + const Offset(30, 5); // Verify that the start location is within button 1. await tester.tapAt(start); expect(pressed, 1); pressed = null; final TestGesture gesture = await tester.startGesture(start); await tester.pumpAndSettle(); // Move slightly upwards without starting the drag await gesture.moveBy(const Offset(0, -10)); await tester.pumpAndSettle(); // Stop scrolling. await gesture.up(); await tester.pumpAndSettle(); expect(pressed, 0); }); testWidgets('Sliding taps only recognizes the primary pointer', (WidgetTester tester) async { int? pressed; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), actions: List.generate( 8, (int i) => CupertinoDialogAction( onPressed: () { expect(pressed, null); pressed = i; }, child: Text('Button $i'), ), ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); // Start gesture 1 at button 0 final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('Button 0'))); await gesture1.moveBy(const Offset(0, 20)); // Starts the gesture await tester.pumpAndSettle(); // Start gesture 2 at button 1. final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.text('Button 1'))); await gesture2.moveBy(const Offset(0, 20)); // Starts the gesture await tester.pumpAndSettle(); // Move gesture 1 to button 2 and release. await gesture1.moveTo(tester.getCenter(find.text('Button 2'))); await tester.pumpAndSettle(); await gesture1.up(); await tester.pumpAndSettle(); expect(pressed, 2); pressed = null; // Tap at button 3, which becomes the new primary pointer and is recognized. await tester.tap(find.text('Button 3')); await tester.pumpAndSettle(); expect(pressed, 3); pressed = null; // Move gesture 2 to button 4 and release. await gesture2.moveTo(tester.getCenter(find.text('Button 4'))); await tester.pumpAndSettle(); await gesture2.up(); await tester.pumpAndSettle(); // Non-primary pointers should not be recognized. expect(pressed, null); }); testWidgets('Non-primary pointers can trigger scroll', (WidgetTester tester) async { int? pressed; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( actions: List.generate( 12, (int i) => CupertinoDialogAction( onPressed: () { expect(pressed, null); pressed = i; }, child: Text('Button $i'), ), ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); // Start gesture 1 at button 0 final TestGesture gesture1 = await tester.startGesture(tester.getCenter(find.text('Button 0'))); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Button 11')).dy, greaterThan(400)); // Start gesture 2 at button 1 and scrolls. final TestGesture gesture2 = await tester.startGesture(tester.getCenter(find.text('Button 1'))); await gesture2.moveBy(const Offset(0, -20)); await gesture2.moveBy(const Offset(0, -500)); await tester.pumpAndSettle(); expect(tester.getTopLeft(find.text('Button 11')).dy, lessThan(400)); // Release gesture 1, which should not trigger any buttons. await gesture1.up(); await tester.pumpAndSettle(); expect(pressed, null); }); testWidgets('Taps on legacy button calls onPressed and renders correctly', ( WidgetTester tester, ) async { // Legacy buttons are implemented with [GestureDetector.onTap]. Apps that // use customized legacy buttons should continue to work. var wasPressed = false; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( actions: [ LegacyAction( child: const Text('Legacy'), onPressed: () { expect(wasPressed, false); wasPressed = true; Navigator.pop(context); }, ), CupertinoDialogAction(child: const Text('One'), onPressed: () {}), CupertinoDialogAction(child: const Text('Two'), onPressed: () {}), ], ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); expect(wasPressed, isFalse); // Push the legacy button and hold for a while to activate the pressing effect. final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Legacy'))); await tester.pump(const Duration(seconds: 1)); expect(wasPressed, isFalse); await expectLater( find.byType(CupertinoAlertDialog), matchesGoldenFile('cupertinoAlertDialog.legacyButton.png'), ); await gesture.up(); await tester.pumpAndSettle(); expect(wasPressed, isTrue); expect(find.text('Legacy'), findsNothing); }); testWidgets('Dialog not barrier dismissible by default', (WidgetTester tester) async { await tester.pumpWidget(createAppWithCenteredButton(const Text('Go'))); final BuildContext context = tester.element(find.text('Go')); showCupertinoDialog( context: context, builder: (BuildContext context) { return Container( width: 100.0, height: 100.0, alignment: Alignment.center, child: const Text('Dialog'), ); }, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog'), 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('Dialog'), findsOneWidget); }); testWidgets('Dialog configurable to be barrier dismissible', (WidgetTester tester) async { await tester.pumpWidget(createAppWithCenteredButton(const Text('Go'))); final BuildContext context = tester.element(find.text('Go')); showCupertinoDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return Container( width: 100.0, height: 100.0, alignment: Alignment.center, child: const Text('Dialog'), ); }, ); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog'), findsOneWidget); // Tap off the barrier. await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(const Duration(seconds: 1)); expect(find.text('Dialog'), findsNothing); }); testWidgets('Dialog destructive action style', (WidgetTester tester) async { await tester.pumpWidget( boilerplate(const CupertinoDialogAction(isDestructiveAction: true, child: Text('Ok'))), ); final DefaultTextStyle widget = tester.widget(find.byType(DefaultTextStyle)); expect(widget.style.color!.withAlpha(255), CupertinoColors.systemRed.color); }); testWidgets('Dialog default action style', (WidgetTester tester) async { await tester.pumpWidget( CupertinoTheme( data: const CupertinoThemeData(primaryColor: CupertinoColors.systemGreen), child: boilerplate(const CupertinoDialogAction(child: Text('Ok'))), ), ); final DefaultTextStyle widget = tester.widget(find.byType(DefaultTextStyle)); expect(widget.style.color!.withAlpha(255), CupertinoColors.systemGreen.color); expect(widget.style.fontFamily, 'CupertinoSystemText'); }); testWidgets('Dialog dark theme', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: MediaQuery( data: const MediaQueryData(platformBrightness: Brightness.dark), child: CupertinoAlertDialog( title: const Text('The Title'), content: const Text('Content'), actions: [ CupertinoDialogAction( isDefaultAction: true, onPressed: () {}, child: const Text('Cancel'), ), const CupertinoDialogAction(child: Text('OK')), ], ), ), ), ); final RichText cancelText = tester.widget( find.descendant(of: find.text('Cancel'), matching: find.byType(RichText)), ); expect( cancelText.text.style!.color!.value, 0xFF0A84FF, // dark elevated color of systemBlue. ); expect(find.byType(CupertinoAlertDialog), paints..rect(color: const Color(0xCC2D2D2D))); }); testWidgets('Has semantic annotations', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( const CupertinoApp( home: CupertinoAlertDialog( title: Text('The Title'), content: Text('Content'), actions: [ CupertinoDialogAction(child: Text('Cancel')), CupertinoDialogAction(child: Text('OK')), ], ), ), ); expect( semantics, hasSemantics( TestSemantics.root( children: [ TestSemantics( children: [ TestSemantics( children: [ TestSemantics( flags: [SemanticsFlag.scopesRoute], children: [ TestSemantics( flags: [ SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute, ], role: SemanticsRole.alertDialog, label: 'Alert', children: [ TestSemantics( flags: [SemanticsFlag.hasImplicitScrolling], children: [ TestSemantics(label: 'The Title'), TestSemantics(label: 'Content'), ], ), TestSemantics( flags: [SemanticsFlag.hasImplicitScrolling], children: [ TestSemantics( flags: [SemanticsFlag.isButton], label: 'Cancel', ), TestSemantics( flags: [SemanticsFlag.isButton], label: 'OK', ), ], ), ], ), ], ), ], ), ], ), ], ), ignoreId: true, ignoreRect: true, ignoreTransform: true, ), ); semantics.dispose(); }); testWidgets('Dialog default action style', (WidgetTester tester) async { await tester.pumpWidget( boilerplate(const CupertinoDialogAction(isDefaultAction: true, child: Text('Ok'))), ); final DefaultTextStyle widget = tester.widget(find.byType(DefaultTextStyle)); expect(widget.style.fontWeight, equals(FontWeight.w600)); }); testWidgets('Dialog default and destructive action styles', (WidgetTester tester) async { await tester.pumpWidget( boilerplate( const CupertinoDialogAction( isDefaultAction: true, isDestructiveAction: true, child: Text('Ok'), ), ), ); final DefaultTextStyle widget = tester.widget(find.byType(DefaultTextStyle)); expect(widget.style.color!.withAlpha(255), CupertinoColors.systemRed.color); expect(widget.style.fontWeight, equals(FontWeight.w600)); }); testWidgets('Dialog disabled action style', (WidgetTester tester) async { await tester.pumpWidget(boilerplate(const CupertinoDialogAction(child: Text('Ok')))); final DefaultTextStyle widget = tester.widget(find.byType(DefaultTextStyle)); expect(widget.style.color!.opacity, greaterThanOrEqualTo(127 / 255)); expect(widget.style.color!.opacity, lessThanOrEqualTo(128 / 255)); }); testWidgets('Dialog enabled action style', (WidgetTester tester) async { await tester.pumpWidget( boilerplate(CupertinoDialogAction(child: const Text('Ok'), onPressed: () {})), ); final DefaultTextStyle widget = tester.widget(find.byType(DefaultTextStyle)); expect(widget.style.color!.opacity, equals(1.0)); }); testWidgets('Pressing on disabled buttons does not trigger highlight', ( WidgetTester tester, ) async { var pressedEnable = false; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( actions: [ const CupertinoDialogAction(child: Text('Disabled')), CupertinoDialogAction( isDestructiveAction: true, onPressed: () { pressedEnable = true; Navigator.pop(context); }, child: const Text('Enabled'), ), ], ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Disabled'))); await tester.pumpAndSettle(const Duration(seconds: 1)); // This should look exactly like an idle dialog. await expectLater( find.byType(CupertinoAlertDialog), matchesGoldenFile('cupertinoAlertDialog.press_disabled.png'), ); // Verify that gestures that started on a disabled button can slide onto an // enabled button. await gesture.moveTo(tester.getCenter(find.text('Enabled'))); await tester.pumpAndSettle(); await expectLater( find.byType(CupertinoAlertDialog), matchesGoldenFile('cupertinoAlertDialog.press_disabled_slide_to_enabled.png'), ); expect(pressedEnable, false); await gesture.up(); expect(pressedEnable, true); }); testWidgets('Action buttons shows pressed highlight as soon as the pointer is down', ( WidgetTester tester, ) async { // Verifies that the the pressed color is not delayed for some milliseconds, // a symptom if the color relies on a tap gesture timing out. await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), actions: [ CupertinoDialogAction(child: const Text('One'), onPressed: () {}), CupertinoDialogAction(child: const Text('Two'), onPressed: () {}), ], ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); final TestGesture pointer = await tester.startGesture(tester.getCenter(find.text('Two'))); // Just `pump`, not `pumpAndSettle`, as we want to verify the very next frame. await tester.pump(); await expectLater( find.byType(CupertinoAlertDialog), matchesGoldenFile('cupertinoAlertDialog.pressed.png'), ); await pointer.up(); }); testWidgets('Message is scrollable, has correct padding with large text sizes', ( WidgetTester tester, ) async { final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery.withClampedTextScaling( minScaleFactor: 3.0, maxScaleFactor: 3.0, child: CupertinoAlertDialog( title: const Text('The Title'), content: Text('Very long content ' * 20), actions: const [ CupertinoDialogAction(child: Text('Cancel')), CupertinoDialogAction(isDestructiveAction: true, child: Text('OK')), ], scrollController: scrollController, ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); expect(scrollController.offset, 0.0); scrollController.jumpTo(100.0); expect(scrollController.offset, 100.0); // Set the scroll position back to zero. scrollController.jumpTo(0.0); await tester.pumpAndSettle(); // Expect the modal dialog box to take all available height. expect( tester.getSize(find.byType(ClipRSuperellipse)), equals(const Size(310.0, 560.0 - 24.0 * 2)), ); // Check sizes/locations of the text. The text is large so these 2 buttons are stacked. // Visually the "Cancel" button and "OK" button are the same height when using the // regular font. However, when using the test font, "Cancel" becomes 2 lines which // is why the height we're verifying for "Cancel" is larger than "OK". expect(tester.getSize(find.text('The Title')), equals(const Size(270.0, 132.0))); expect(tester.getTopLeft(find.text('The Title')), equals(const Offset(265.0, 80.0 + 24.0))); expect( tester.getSize(find.widgetWithText(CupertinoDialogAction, 'Cancel')), equals(const Size(310.0, 148.0)), ); expect( tester.getSize(find.widgetWithText(CupertinoDialogAction, 'OK')), equals(const Size(310.0, 98.0)), ); }); testWidgets('Dialog respects small constraints.', (WidgetTester tester) async { final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return Center( child: ConstrainedBox( // Constrain the dialog to a tiny size and ensure it respects // these exact constraints. constraints: BoxConstraints.tight(const Size(200.0, 100.0)), child: CupertinoAlertDialog( title: const Text('The Title'), content: const Text('The message'), actions: const [ CupertinoDialogAction(child: Text('Option 1')), CupertinoDialogAction(child: Text('Option 2')), CupertinoDialogAction(child: Text('Option 3')), ], scrollController: scrollController, ), ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); const topAndBottomMargin = 40.0; const double topAndBottomPadding = 24.0 * 2; const double leftAndRightPadding = 40.0 * 2; final Finder modalFinder = find.byType(ClipRSuperellipse); expect( tester.getSize(modalFinder), equals( const Size(200.0 - leftAndRightPadding, 100.0 - topAndBottomMargin - topAndBottomPadding), ), ); }); testWidgets('Button list is scrollable, has correct position with large text sizes.', ( WidgetTester tester, ) async { final actionScrollController = ScrollController(); addTearDown(actionScrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery.withClampedTextScaling( minScaleFactor: 3.0, maxScaleFactor: 3.0, child: CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content.'), actions: const [ CupertinoDialogAction(child: Text('One')), CupertinoDialogAction(child: Text('Two')), CupertinoDialogAction(child: Text('Three')), CupertinoDialogAction(child: Text('Chocolate Brownies')), CupertinoDialogAction(isDestructiveAction: true, child: Text('Cancel')), ], actionScrollController: actionScrollController, ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); // Check that the action buttons list is scrollable. expect(actionScrollController.offset, 0.0); actionScrollController.jumpTo(100.0); expect(actionScrollController.offset, 100.0); actionScrollController.jumpTo(0.0); // Check that the action buttons are aligned vertically. expect(tester.getCenter(find.widgetWithText(CupertinoDialogAction, 'One')).dx, equals(400.0)); expect(tester.getCenter(find.widgetWithText(CupertinoDialogAction, 'Two')).dx, equals(400.0)); expect(tester.getCenter(find.widgetWithText(CupertinoDialogAction, 'Three')).dx, equals(400.0)); expect( tester.getCenter(find.widgetWithText(CupertinoDialogAction, 'Chocolate Brownies')).dx, equals(400.0), ); expect( tester.getCenter(find.widgetWithText(CupertinoDialogAction, 'Cancel')).dx, equals(400.0), ); // Check that the action buttons are the correct heights. expect(tester.getSize(find.widgetWithText(CupertinoDialogAction, 'One')).height, equals(98.0)); expect(tester.getSize(find.widgetWithText(CupertinoDialogAction, 'Two')).height, equals(98.0)); expect( tester.getSize(find.widgetWithText(CupertinoDialogAction, 'Three')).height, equals(98.0), ); expect( tester.getSize(find.widgetWithText(CupertinoDialogAction, 'Chocolate Brownies')).height, equals(248.0), ); expect( tester.getSize(find.widgetWithText(CupertinoDialogAction, 'Cancel')).height, equals(148.0), ); }); testWidgets('Title Section is empty, Button section is not empty.', (WidgetTester tester) async { final actionScrollController = ScrollController(); addTearDown(actionScrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery.withNoTextScaling( child: CupertinoAlertDialog( actions: const [ CupertinoDialogAction(child: Text('One')), CupertinoDialogAction(child: Text('Two')), ], actionScrollController: actionScrollController, ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); // Check that the dialog size is the same as the actions section size. This // ensures that an empty content section doesn't accidentally render some // empty space in the dialog. final Finder contentSectionFinder = find.byElementPredicate((Element element) { return element.widget.runtimeType.toString() == '_CupertinoAlertActionSection'; }); final Finder modalBoundaryFinder = find.byType(ClipRSuperellipse); expect(tester.getSize(contentSectionFinder), tester.getSize(modalBoundaryFinder)); // Check that the title/message section is not displayed expect(actionScrollController.offset, 0.0); expect(tester.getTopLeft(find.widgetWithText(CupertinoDialogAction, 'One')).dy, equals(270.75)); // Check that the button's vertical size is the same. expect( tester.getSize(find.widgetWithText(CupertinoDialogAction, 'One')).height, equals(tester.getSize(find.widgetWithText(CupertinoDialogAction, 'Two')).height), ); }); testWidgets('Button section is empty, Title section is not empty.', (WidgetTester tester) async { final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery.withNoTextScaling( child: CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content.'), scrollController: scrollController, ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); // Check that there's no button action section. expect(scrollController.offset, 0.0); expect(find.widgetWithText(CupertinoDialogAction, 'One'), findsNothing); // Check that the dialog size is the same as the content section size. This // ensures that an empty button section doesn't accidentally render some // empty space in the dialog. final Finder contentSectionFinder = find.byElementPredicate((Element element) { return element.widget.runtimeType.toString() == '_CupertinoAlertContentSection'; }); final Finder modalBoundaryFinder = find.byType(ClipRSuperellipse); expect(tester.getSize(contentSectionFinder), tester.getSize(modalBoundaryFinder)); }); testWidgets('Actions section height for 1 button is height of button.', ( WidgetTester tester, ) async { final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The Title'), content: const Text('The message'), actions: const [CupertinoDialogAction(child: Text('OK'))], scrollController: scrollController, ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); final RenderBox okButtonBox = findActionButtonRenderBoxByTitle(tester, 'OK'); final RenderBox actionsSectionBox = findScrollableActionsSectionRenderBox(tester); expect(okButtonBox.size.width, actionsSectionBox.size.width); expect(okButtonBox.size.height, actionsSectionBox.size.height); }); testWidgets('Actions section height for 2 side-by-side buttons is height of tallest button.', ( WidgetTester tester, ) async { final scrollController = ScrollController(); addTearDown(scrollController.dispose); late double dividerWidth; // Will be set when the dialog builder runs. Needs a BuildContext. await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { dividerWidth = 0.3; return CupertinoAlertDialog( title: const Text('The Title'), content: const Text('The message'), actions: const [ CupertinoDialogAction(child: Text('OK')), CupertinoDialogAction(isDestructiveAction: true, child: Text('Cancel')), ], scrollController: scrollController, ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); final RenderBox okButtonBox = findActionButtonRenderBoxByTitle(tester, 'OK'); final RenderBox cancelButtonBox = findActionButtonRenderBoxByTitle(tester, 'Cancel'); final RenderBox actionsSectionBox = findScrollableActionsSectionRenderBox(tester); expect(okButtonBox.size.width, cancelButtonBox.size.width); expect( actionsSectionBox.size.width, okButtonBox.size.width + cancelButtonBox.size.width + dividerWidth, ); expect( actionsSectionBox.size.height, max(okButtonBox.size.height, cancelButtonBox.size.height), ); }); testWidgets( 'Actions section height for 2 stacked buttons with enough room is height of both buttons.', (WidgetTester tester) async { final scrollController = ScrollController(); addTearDown(scrollController.dispose); const dividerThickness = 0.3; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The Title'), content: const Text('The message'), actions: const [ CupertinoDialogAction(child: Text('OK')), CupertinoDialogAction( isDestructiveAction: true, child: Text('This is too long to fit'), ), ], scrollController: scrollController, ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); final RenderBox okButtonBox = findActionButtonRenderBoxByTitle(tester, 'OK'); final RenderBox longButtonBox = findActionButtonRenderBoxByTitle( tester, 'This is too long to fit', ); final RenderBox actionsSectionBox = findScrollableActionsSectionRenderBox(tester); expect(okButtonBox.size.width, longButtonBox.size.width); expect(okButtonBox.size.width, actionsSectionBox.size.width); expect( okButtonBox.size.height + dividerThickness + longButtonBox.size.height, actionsSectionBox.size.height, ); }, ); testWidgets( 'Actions section height for 2 stacked buttons without enough room and regular font is 1.5 buttons tall.', (WidgetTester tester) async { final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The Title'), content: Text('The message\n' * 40), actions: const [ CupertinoDialogAction(child: Text('OK')), CupertinoDialogAction( isDestructiveAction: true, child: Text('This is too long to fit'), ), ], scrollController: scrollController, ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); final RenderBox actionsSectionBox = findScrollableActionsSectionRenderBox(tester); expect(actionsSectionBox.size.height, 67.8); }, ); testWidgets( 'Actions section height for 2 stacked buttons without enough room and large accessibility font is 50% of dialog height.', (WidgetTester tester) async { final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery.withClampedTextScaling( minScaleFactor: 3.0, maxScaleFactor: 3.0, child: CupertinoAlertDialog( title: const Text('The Title'), content: Text('The message\n' * 20), actions: const [ CupertinoDialogAction(child: Text('This button is multi line')), CupertinoDialogAction( isDestructiveAction: true, child: Text('This button is multi line'), ), ], scrollController: scrollController, ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); final RenderBox actionsSectionBox = findScrollableActionsSectionRenderBox(tester); // The two multiline buttons with large text are taller than 50% of the // dialog height, but with the accessibility layout policy, the 2 buttons // should be in a scrollable area equal to half the dialog height. expect(actionsSectionBox.size.height, 280.0 - 24.0); }, ); testWidgets('Actions section height for 3 buttons without enough room is 1.5 buttons tall.', ( WidgetTester tester, ) async { final scrollController = ScrollController(); addTearDown(scrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The Title'), content: Text('The message\n' * 40), actions: const [ CupertinoDialogAction(child: Text('Option 1')), CupertinoDialogAction(child: Text('Option 2')), CupertinoDialogAction(child: Text('Option 3')), ], scrollController: scrollController, ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); await tester.pumpAndSettle(); final RenderBox option1ButtonBox = findActionButtonRenderBoxByTitle(tester, 'Option 1'); final RenderBox option2ButtonBox = findActionButtonRenderBoxByTitle(tester, 'Option 2'); final RenderBox actionsSectionBox = findScrollableActionsSectionRenderBox(tester); expect(option1ButtonBox.size.width, option2ButtonBox.size.width); expect(option1ButtonBox.size.width, actionsSectionBox.size.width); // Expected Height = button 1 + divider + 1/2 button 2 = 67.80000000000001 const expectedHeight = 67.80000000000001; expect(actionsSectionBox.size.height, moreOrLessEquals(expectedHeight)); }); testWidgets('Actions section correctly renders overscrolls', (WidgetTester tester) async { // Verifies that when the actions section overscrolls, the overscroll part // is correctly covered with background. final actionScrollController = ScrollController(); addTearDown(actionScrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( actions: List.generate( 12, (int i) => CupertinoDialogAction(onPressed: () {}, child: Text('Button ${'*' * i}')), ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button *'))); await tester.pumpAndSettle(); // The button should be pressed now, since the scrolling gesture has not // taken over. await expectLater( find.byType(CupertinoAlertDialog), matchesGoldenFile('cupertinoAlertDialog.overscroll.0.png'), ); // The dragging gesture must be dispatched in at least two segments. // After the first movement, the gesture is started, but the delta is still // zero. The second movement gives the delta. await gesture.moveBy(const Offset(0, 40)); await tester.pumpAndSettle(); await gesture.moveBy(const Offset(0, 100)); // Test the top overscroll. Use `pump` not `pumpAndSettle` to verify the // rendering result of the immediate next frame. await tester.pump(); await expectLater( find.byType(CupertinoAlertDialog), matchesGoldenFile('cupertinoAlertDialog.overscroll.1.png'), ); await gesture.moveBy(const Offset(0, -300)); // Test the bottom overscroll. Use `pump` not `pumpAndSettle` to verify the // rendering result of the immediate next frame. await tester.pump(); await expectLater( find.byType(CupertinoAlertDialog), matchesGoldenFile('cupertinoAlertDialog.overscroll.2.png'), ); await gesture.up(); }); testWidgets('Actions section correctly renders overscrolls with very far scrolls', ( WidgetTester tester, ) async { // When the scroll is really far, the overscroll might be longer than the // actions section, causing overflow if not controlled. final actionScrollController = ScrollController(); addTearDown(actionScrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( content: Text('content' * 1000), actions: List.generate( 4, (int i) => CupertinoActionSheetAction(onPressed: () {}, child: Text('Button $i')), ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Button 0'))); await tester.pumpAndSettle(); await gesture.moveBy(const Offset(0, 40)); // A short drag to start the gesture. await tester.pumpAndSettle(); // The drag is far enough to make the overscroll longer than the section. await gesture.moveBy(const Offset(0, 1000)); await tester.pumpAndSettle(); // The buttons should be out of the screen expect( tester.getTopLeft(find.text('Button 0')).dy, greaterThan(tester.getBottomLeft(find.byType(ClipRSuperellipse)).dy), ); await expectLater( find.byType(CupertinoAlertDialog), matchesGoldenFile('cupertinoAlertDialog.long-overscroll.0.png'), ); }); testWidgets('ScaleTransition animation for showCupertinoDialog()', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( child: Builder( builder: (BuildContext context) { return CupertinoButton( onPressed: () { showCupertinoDialog( context: context, builder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content'), actions: [ const CupertinoDialogAction(child: Text('Cancel')), CupertinoDialogAction( isDestructiveAction: true, onPressed: () { Navigator.pop(context); }, child: const Text('Delete'), ), ], ); }, ); }, child: const Text('Go'), ); }, ), ), ), ); await tester.tap(find.text('Go')); // Enter animation. await tester.pump(); Transform transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], moreOrLessEquals(1.3, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], moreOrLessEquals(1.205, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], moreOrLessEquals(1.100, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], moreOrLessEquals(1.043, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], moreOrLessEquals(1.017, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], moreOrLessEquals(1.006, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], moreOrLessEquals(1.002, epsilon: 0.001)); await tester.tap(find.text('Delete')); await tester.pump(); await tester.pump(const Duration(milliseconds: 50)); // No scaling on exit animation. expect(find.byType(Transform), findsNothing); }); testWidgets('FadeTransition animation for showCupertinoDialog()', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( child: Builder( builder: (BuildContext context) { return CupertinoButton( onPressed: () { showCupertinoDialog( context: context, builder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content'), actions: [ const CupertinoDialogAction(child: Text('Cancel')), CupertinoDialogAction( isDestructiveAction: true, onPressed: () { Navigator.pop(context); }, child: const Text('Delete'), ), ], ); }, ); }, child: const Text('Go'), ); }, ), ), ), ); await tester.tap(find.text('Go')); // Enter animation. await tester.pump(); final Finder fadeTransitionFinder = find.ancestor( of: find.byType(CupertinoAlertDialog), matching: find.byType(FadeTransition), ); FadeTransition transition = tester.firstWidget(fadeTransitionFinder); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.316, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.665, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.856, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.942, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.977, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.991, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.997, epsilon: 0.001)); await tester.tap(find.text('Delete')); // Exit animation, look at reverse FadeTransition. await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.997, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.681, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.333, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.143, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.057, epsilon: 0.001)); await tester.pump(const Duration(milliseconds: 50)); transition = tester.firstWidget(fadeTransitionFinder); expect(transition.opacity.value, moreOrLessEquals(0.022, epsilon: 0.001)); }); testWidgets('Actions are accessible by key', (WidgetTester tester) async { await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return const CupertinoAlertDialog( title: Text('The Title'), content: Text('The message'), actions: [ CupertinoDialogAction(key: Key('option_1'), child: Text('Option 1')), CupertinoDialogAction(key: Key('option_2'), child: Text('Option 2')), ], ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); expect(find.byKey(const Key('option_1')), findsOneWidget); expect(find.byKey(const Key('option_2')), findsOneWidget); expect(find.byKey(const Key('option_3')), findsNothing); }); testWidgets('Dialog widget insets by MediaQuery viewInsets', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: MediaQuery( data: MediaQueryData(), child: CupertinoAlertDialog(content: Placeholder(fallbackHeight: 200.0)), ), ), ); final Rect placeholderRectWithoutInsets = tester.getRect(find.byType(Placeholder)); await tester.pumpWidget( const CupertinoApp( home: MediaQuery( data: MediaQueryData(viewInsets: EdgeInsets.fromLTRB(40.0, 30.0, 20.0, 10.0)), child: CupertinoAlertDialog(content: Placeholder(fallbackHeight: 200.0)), ), ), ); // no change yet because padding is animated expect(tester.getRect(find.byType(Placeholder)), placeholderRectWithoutInsets); await tester.pump(const Duration(seconds: 1)); // once animation settles the dialog is padded by the new viewInsets expect( tester.getRect(find.byType(Placeholder)), placeholderRectWithoutInsets.translate(10, 10), ); }); testWidgets('showCupertinoDialog - custom barrierLabel', (WidgetTester tester) async { final semantics = SemanticsTester(tester); await tester.pumpWidget( CupertinoApp( home: Builder( builder: (BuildContext context) { return Center( child: CupertinoButton( child: const Text('X'), onPressed: () { showCupertinoDialog( context: context, barrierLabel: 'Custom label', builder: (BuildContext context) { return const CupertinoAlertDialog( title: Text('Title'), content: Text('Content'), actions: [ CupertinoDialogAction(child: Text('Yes')), CupertinoDialogAction(child: Text('No')), ], ); }, ); }, ), ); }, ), ), ); expect( semantics, isNot( includesNodeWith(label: 'Custom label', flags: [SemanticsFlag.namesRoute]), ), ); semantics.dispose(); }); testWidgets('showCupertinoDialog - custom barrierColor', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Builder( builder: (BuildContext context) { return Center( child: Column( children: [ CupertinoButton( child: const Text('Custom BarrierColor'), onPressed: () { showCupertinoDialog( context: context, barrierColor: Colors.red, builder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('Title'), content: const Text('Content'), actions: [ const CupertinoDialogAction(child: Text('Yes')), CupertinoDialogAction( child: const Text('No'), onPressed: () { Navigator.pop(context); }, ), ], ); }, ); }, ), ], ), ); }, ), ), ); await tester.tap(find.text('Custom BarrierColor')); await tester.pumpAndSettle(); expect(tester.widget(find.byType(ModalBarrier).last).color, equals(Colors.red)); await tester.tap(find.text('No')); await tester.pumpAndSettle(); expect( find.byWidgetPredicate( (Widget widget) => widget is ModalBarrier && widget.color == Colors.red, ), findsNothing, ); }); testWidgets('CupertinoDialogRoute is state restorable', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp(restorationScopeId: 'app', home: _RestorableDialogTestWidget()), ); expect(find.byType(CupertinoAlertDialog), findsNothing); await tester.tap(find.text('X')); await tester.pumpAndSettle(); expect(find.byType(CupertinoAlertDialog), findsOneWidget); final TestRestorationData restorationData = await tester.getRestorationData(); await tester.restartAndRestore(); expect(find.byType(CupertinoAlertDialog), findsOneWidget); // Tap on the barrier. await tester.tapAt(const Offset(10.0, 10.0)); await tester.pumpAndSettle(); expect(find.byType(CupertinoAlertDialog), findsNothing); await tester.restoreFrom(restorationData); expect(find.byType(CupertinoAlertDialog), findsOneWidget); }, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615 testWidgets( 'Conflicting scrollbars are not applied by ScrollBehavior to CupertinoAlertDialog', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/83819 final actionScrollController = ScrollController(); addTearDown(actionScrollController.dispose); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery.withNoTextScaling( child: CupertinoAlertDialog( title: const Text('Test Title'), content: const Text('Test Content'), actions: const [ CupertinoDialogAction(child: Text('One')), CupertinoDialogAction(child: Text('Two')), ], actionScrollController: actionScrollController, ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pump(); // The inherited ScrollBehavior should not apply scrollbars since they are // already built in to the widget. expect(find.byType(RawScrollbar), findsNothing); // Built in CupertinoScrollbars should only number 2: one for the actions, // one for the content. expect(find.byType(CupertinoScrollbar), findsNWidgets(2)); }, variant: TargetPlatformVariant.all(), ); testWidgets('CupertinoAlertDialog scrollbars controllers should be different', ( WidgetTester tester, ) async { // https://github.com/flutter/flutter/pull/81278 await tester.pumpWidget( const CupertinoApp( home: MediaQuery( data: MediaQueryData(), child: CupertinoAlertDialog( actions: [CupertinoDialogAction(child: Text('OK'))], content: Placeholder(fallbackHeight: 200.0), ), ), ), ); final List scrollbars = find .descendant( of: find.byType(CupertinoAlertDialog), matching: find.byType(CupertinoScrollbar), ) .evaluate() .map((Element e) => e.widget as CupertinoScrollbar) .toList(); expect(scrollbars.length, 2); expect(scrollbars[0].controller != scrollbars[1].controller, isTrue); }); group('showCupertinoDialog avoids overlapping display features', () { testWidgets('positioning using anchorPoint', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( 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')); showCupertinoDialog( 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.00)); expect(tester.getBottomRight(find.byType(Placeholder)), const Offset(800.0, 600.0)); }); testWidgets('positioning using Directionality', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( 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')); showCupertinoDialog( context: context, builder: (BuildContext context) { return const Placeholder(); }, ); 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('default positioning', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( 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')); showCupertinoDialog( 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)); }); }); testWidgets('Hovering over Cupertino alert dialog action updates cursor to clickable on Web', ( WidgetTester tester, ) async { await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery.withClampedTextScaling( minScaleFactor: 3.0, maxScaleFactor: 3.0, child: RepaintBoundary( child: CupertinoAlertDialog( title: const Text('Title'), content: const Text('text'), actions: [ CupertinoDialogAction(onPressed: () {}, child: const Text('NO')), CupertinoDialogAction(onPressed: () {}, child: const Text('OK')), ], ), ), ); }, ), ); await tester.tap(find.text('Go')); await tester.pumpAndSettle(); final TestGesture gesture = await tester.createGesture( kind: PointerDeviceKind.mouse, pointer: 1, ); await gesture.addPointer(location: const Offset(10, 10)); await tester.pumpAndSettle(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic, ); final Offset dialogAction = tester.getCenter(find.text('OK')); await gesture.moveTo(dialogAction); await tester.pumpAndSettle(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic, ); }); testWidgets('CupertinoAlertDialog divider spans full width and applies color', ( WidgetTester tester, ) async { const kCupertinoDialogWidth = 270.0; const kDividerThickness = 0.3; const expectedSize = Size(kCupertinoDialogWidth, kDividerThickness); await tester.pumpWidget( CupertinoApp( home: MediaQuery( data: const MediaQueryData(platformBrightness: Brightness.dark), child: CupertinoAlertDialog( title: const Text('The Title'), content: const Text('Content'), actions: [ CupertinoDialogAction( isDefaultAction: true, onPressed: () {}, child: const Text('Cancel'), ), const CupertinoDialogAction(child: Text('OK')), ], ), ), ), ); final Finder decoratedBoxFinder = find.byType(DecoratedBox); expect(decoratedBoxFinder, findsAny, reason: 'There should exist at least one DecoratedBox'); final Iterable elements = decoratedBoxFinder.evaluate().where(( Element decoratedBoxElement, ) { final decoratedBox = decoratedBoxElement.widget as DecoratedBox; return (decoratedBox.decoration is BoxDecoration?) && (decoratedBox.decoration as BoxDecoration?)?.color == CupertinoDynamicColor.resolve(CupertinoColors.separator, decoratedBoxElement) && tester.getSize(find.byWidget(decoratedBox)) == expectedSize; }); expect(elements.length, 1, reason: 'No DecoratedBox matches the specified criteria.'); }); testWidgets('Check for Directionality', (WidgetTester tester) async { Future pumpWidget({required bool isLTR}) async { await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: isLTR ? TextDirection.ltr : TextDirection.rtl, child: const CupertinoAlertDialog( actions: [ CupertinoDialogAction(isDefaultAction: true, child: Text('No')), CupertinoDialogAction(child: Text('Yes')), ], ), ), ), ); } await pumpWidget(isLTR: true); Offset yesButton = tester.getCenter(find.text('Yes')); Offset noButton = tester.getCenter(find.text('No')); expect(yesButton.dx > noButton.dx, true); await pumpWidget(isLTR: false); yesButton = tester.getCenter(find.text('Yes')); noButton = tester.getCenter(find.text('No')); expect(yesButton.dx > noButton.dx, false); }); testWidgets('CupertinoDialogAction.mouseCursor can customize the mouse cursor', ( WidgetTester tester, ) async { const SystemMouseCursor customCursor = SystemMouseCursors.grab; await tester.pumpWidget( CupertinoApp( home: Directionality( textDirection: TextDirection.ltr, child: CupertinoAlertDialog( actions: [ CupertinoDialogAction( mouseCursor: customCursor, child: const Text('Yes'), onPressed: () {}, ), ], ), ), ), ); final TestGesture gesture = await tester.createGesture( kind: PointerDeviceKind.mouse, pointer: 1, ); await gesture.addPointer(location: const Offset(10, 10)); await tester.pumpAndSettle(); expect( RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.basic, ); final Offset actionSheetAction = tester.getCenter(find.text('Yes')); await gesture.moveTo(actionSheetAction); await tester.pumpAndSettle(); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), customCursor); }); testWidgets('CupertinoDialogAction does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Center( child: SizedBox.shrink(child: CupertinoDialogAction(child: Text('X'))), ), ), ); expect(tester.getSize(find.byType(CupertinoDialogAction)), Size.zero); }); testWidgets('CupertinoActionSheetAction does not crash at zero area', ( WidgetTester tester, ) async { tester.view.physicalSize = Size.zero; final focusNode = FocusNode(); addTearDown(tester.view.reset); addTearDown(focusNode.dispose); await tester.pumpWidget( CupertinoApp( home: Center( child: CupertinoActionSheetAction( focusNode: focusNode, onPressed: () {}, child: const Text('X'), ), ), ), ); expect(tester.getSize(find.byType(CupertinoActionSheetAction)), Size.zero); focusNode.requestFocus(); await tester.pumpAndSettle(); }); testWidgets('CupertinoPopupSurface does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Center( child: SizedBox.shrink(child: CupertinoPopupSurface(child: Text('X'))), ), ), ); expect(tester.getSize(find.byType(CupertinoPopupSurface)), Size.zero); }); testWidgets('CupertinoAlertDialog does not crash at zero area', (WidgetTester tester) async { await tester.pumpWidget( const CupertinoApp( home: Center(child: SizedBox.shrink(child: CupertinoAlertDialog())), ), ); expect(tester.getSize(find.byType(CupertinoAlertDialog)), Size.zero); }); } RenderBox findActionButtonRenderBoxByTitle(WidgetTester tester, String title) { final RenderObject buttonBox = tester.renderObject( find.widgetWithText(CupertinoDialogAction, title), ); assert(buttonBox is RenderBox); return buttonBox as RenderBox; } RenderBox findScrollableActionsSectionRenderBox(WidgetTester tester) { final RenderObject actionsSection = tester.renderObject( find.byElementPredicate((Element element) { return element.widget.runtimeType.toString() == '_CupertinoAlertActionSection'; }), ); assert(actionsSection is RenderBox); return actionsSection as RenderBox; } Widget createAppWithButtonThatLaunchesDialog({required WidgetBuilder dialogBuilder}) { return CupertinoApp( home: Center( child: Builder( builder: (BuildContext context) { return CupertinoButton( onPressed: () { showCupertinoDialog(context: context, builder: dialogBuilder); }, child: const Text('Go'), ); }, ), ), ); } Widget boilerplate(Widget child) { return Directionality(textDirection: TextDirection.ltr, child: child); } Widget createAppWithCenteredButton(Widget child) { return CupertinoApp( home: Center(child: CupertinoButton(onPressed: null, child: child)), ); } @pragma('vm:entry-point') class _RestorableDialogTestWidget extends StatelessWidget { const _RestorableDialogTestWidget(); @pragma('vm:entry-point') static Route _dialogBuilder(BuildContext context, Object? arguments) { return CupertinoDialogRoute( context: context, builder: (BuildContext context) { return const CupertinoAlertDialog( title: Text('Title'), content: Text('Content'), actions: [ CupertinoDialogAction(child: Text('Yes')), CupertinoDialogAction(child: Text('No')), ], ); }, ); } @override Widget build(BuildContext context) { return CupertinoPageScaffold( navigationBar: const CupertinoNavigationBar(middle: Text('Home')), child: Center( child: CupertinoButton( onPressed: () { Navigator.of(context).restorablePush(_dialogBuilder); }, child: const Text('X'), ), ), ); } } // Shows an app that has a button with text "Go", and clicking this button // displays the `dialog` and hides the button. // // The `theme` will be applied to the app and determines the background. class TestScaffoldApp extends StatefulWidget { const TestScaffoldApp({super.key, required this.theme, required this.dialog}); final CupertinoThemeData theme; final Widget dialog; @override TestScaffoldAppState createState() => TestScaffoldAppState(); } class TestScaffoldAppState extends State { bool _pressedButton = false; @override Widget build(BuildContext context) { return CupertinoApp( // Hide the debug banner. Because this CupertinoApp is captured in golden // test as a whole. The debug banner contains tilted text, whose // anti-alias might cause false negative result. // https://github.com/flutter/flutter/pull/150442 debugShowCheckedModeBanner: false, theme: widget.theme, home: Builder( builder: (BuildContext context) => CupertinoPageScaffold( child: Center( child: _pressedButton ? Container() : CupertinoButton( onPressed: () { setState(() { _pressedButton = true; }); showCupertinoDialog( context: context, builder: (BuildContext context) { return widget.dialog; }, ); }, child: const Text('Go'), ), ), ), ), ); } } // Old-style action sheet buttons, which are implemented with // `GestureDetector.onTap`. class LegacyAction extends StatelessWidget { const LegacyAction({super.key, required this.onPressed, required this.child}); final VoidCallback onPressed; final Widget child; @override Widget build(BuildContext context) { return GestureDetector( onTap: onPressed, behavior: HitTestBehavior.opaque, child: ConstrainedBox( constraints: const BoxConstraints(minHeight: 45), child: Container( alignment: AlignmentDirectional.center, padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 10.0), child: child, ), ), ); } }