// Copyright 2017 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:math'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import '../rendering/mock_canvas.dart'; import '../widgets/semantics_tester.dart'; void main() { testWidgets('Alert dialog control test', (WidgetTester tester) async { bool didDelete = false; await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content'), actions: <Widget>[ 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('Dialog destructive action styles', (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.red, greaterThan(widget.style.color.blue)); expect(widget.style.color.alpha, lessThan(255)); }); testWidgets('Has semantic annotations', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); await tester.pumpWidget(const MaterialApp(home: Material( child: CupertinoAlertDialog( title: Text('The Title'), content: Text('Content'), actions: <Widget>[ CupertinoDialogAction(child: Text('Cancel')), CupertinoDialogAction(child: Text('OK')), ], ) ))); expect( semantics, hasSemantics( TestSemantics.root( children: <TestSemantics>[ TestSemantics( children: <TestSemantics>[ TestSemantics( flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], children: <TestSemantics>[ TestSemantics( flags: <SemanticsFlag>[SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute], label: 'Alert', children: <TestSemantics>[ TestSemantics( flags: <SemanticsFlag>[ SemanticsFlag.hasImplicitScrolling, ], children: <TestSemantics>[ TestSemantics(label: 'The Title'), TestSemantics(label: 'Content'), ], ), TestSemantics( flags: <SemanticsFlag>[ SemanticsFlag.hasImplicitScrolling, ], children: <TestSemantics>[ TestSemantics( flags: <SemanticsFlag>[SemanticsFlag.isButton], label: 'Cancel', ), TestSemantics( flags: <SemanticsFlag>[SemanticsFlag.isButton], label: 'OK', ), ], ), ], ), ], ), ], ), ], ), ignoreId: true, ignoreRect: true, ignoreTransform: true, ) ); semantics.dispose(); }); testWidgets('Dialog default action styles', (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.w400)); }); testWidgets('Default and destructive style', (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.fontWeight, equals(FontWeight.w400)); expect(widget.style.color.red, greaterThan(widget.style.color.blue)); }); testWidgets('Message is scrollable, has correct padding with large text sizes', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: 3.0), child: CupertinoAlertDialog( title: const Text('The Title'), content: Text('Very long content ' * 20), actions: const <Widget>[ 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(ClipRRect) ), equals(const Size(310.0, 560.0)), ); // 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, 162.0))); expect(tester.getTopLeft(find.text('The Title')), equals(const Offset(265.0, 80.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 = ScrollController(); 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 <Widget>[ 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 double topAndBottomMargin = 40.0; final Finder modalFinder = find.byType(ClipRRect); expect( tester.getSize(modalFinder), equals(const Size(200.0, 100.0 - topAndBottomMargin)), ); }); testWidgets('Button list is scrollable, has correct position with large text sizes.', (WidgetTester tester) async { final ScrollController actionScrollController = ScrollController(); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: 3.0), child: CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content.'), actions: const <Widget>[ 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 { const double textScaleFactor = 1.0; final ScrollController actionScrollController = ScrollController(); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: textScaleFactor), child: CupertinoAlertDialog( actions: const <Widget>[ 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(ClipRRect); 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(277.5)); // 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 { const double textScaleFactor = 1.0; final ScrollController scrollController = ScrollController(); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: textScaleFactor), 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(ClipRRect); expect( tester.getSize(contentSectionFinder), tester.getSize(modalBoundaryFinder), ); }); testWidgets('Actions section height for 1 button is height of button.', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The Title'), content: const Text('The message'), actions: const <Widget>[ 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 = ScrollController(); double dividerWidth; // Will be set when the dialog builder runs. Needs a BuildContext. await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { dividerWidth = 1.0 / MediaQuery.of(context).devicePixelRatio; return CupertinoAlertDialog( title: const Text('The Title'), content: const Text('The message'), actions: const <Widget>[ 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 = ScrollController(); double dividerThickness; // Will be set when the dialog builder runs. Needs a BuildContext. await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { dividerThickness = 1.0 / MediaQuery.of(context).devicePixelRatio; return CupertinoAlertDialog( title: const Text('The Title'), content: const Text('The message'), actions: const <Widget>[ 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 = ScrollController(); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The Title'), content: Text('The message\n' * 40), actions: const <Widget>[ 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.83333333333337, ); }); 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 = ScrollController(); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return MediaQuery( data: MediaQuery.of(context).copyWith(textScaleFactor: 3.0), child: CupertinoAlertDialog( title: const Text('The Title'), content: Text('The message\n' * 20), actions: const <Widget>[ 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 multi-line 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, ); }); testWidgets('Actions section height for 3 buttons without enough room is 1.5 buttons tall.', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The Title'), content: Text('The message\n' * 40), actions: const <Widget>[ 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.83333333333334 // Technically the following number is off by 0.00000000000003 but I think it's a // Dart precision issue. I ran the subtraction directly in dartpad and still // got 67.83333333333337. const double expectedHeight = 67.83333333333337; expect( actionsSectionBox.size.height, expectedHeight, ); }); testWidgets('Actions section overscroll is painted white.', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The Title'), content: const Text('The message'), actions: const <Widget>[ 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(); final RenderBox actionsSectionBox = findScrollableActionsSectionRenderBox(tester); // The way that overscroll white is accomplished in a scrollable action // section is that the custom RenderBox that lays out the buttons and draws // the dividers also paints a white background the size of Rect.largest. // That background ends up being clipped by the containing ScrollView. // // Here we test that the largest Rect is contained within the painted Path. // We don't test for exclusion because for some reason the Path is reporting // that even points beyond Rect.largest are within the Path. That's not an // issue for our use-case, so we don't worry about it. expect(actionsSectionBox, paints..path( includes: <Offset>[ Offset(Rect.largest.left, Rect.largest.top), Offset(Rect.largest.right, Rect.largest.bottom), ], )); }); testWidgets('Pressed button changes appearance and dividers disappear.', (WidgetTester tester) async { final ScrollController scrollController = ScrollController(); double dividerThickness; // Will be set when the dialog builder runs. Needs a BuildContext. await tester.pumpWidget( createAppWithButtonThatLaunchesDialog( dialogBuilder: (BuildContext context) { dividerThickness = 1.0 / MediaQuery.of(context).devicePixelRatio; return CupertinoAlertDialog( title: const Text('The Title'), content: const Text('The message'), actions: const <Widget>[ 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 Color normalButtonBackgroundColor = Color(0xc0ffffff); const Color pressedButtonBackgroundColor = Color(0x90ffffff); final RenderBox firstButtonBox = findActionButtonRenderBoxByTitle(tester, 'Option 1'); final RenderBox secondButtonBox = findActionButtonRenderBoxByTitle(tester, 'Option 2'); final RenderBox actionsSectionBox = findScrollableActionsSectionRenderBox(tester); final Offset pressedButtonCenter = Offset( secondButtonBox.size.width / 2.0, firstButtonBox.size.height + dividerThickness + (secondButtonBox.size.height / 2.0), ); final Offset topDividerCenter = Offset( secondButtonBox.size.width / 2.0, firstButtonBox.size.height + (0.5 * dividerThickness), ); final Offset bottomDividerCenter = Offset( secondButtonBox.size.width / 2.0, firstButtonBox.size.height + dividerThickness + secondButtonBox.size.height + (0.5 * dividerThickness), ); // Before pressing the button, verify following expectations: // - Background includes the button that will be pressed // - Background excludes the divider above and below the button that will be pressed // - Pressed button background does NOT include the button that will be pressed expect(actionsSectionBox, paints ..path( color: normalButtonBackgroundColor, includes: <Offset>[ pressedButtonCenter, ], excludes: <Offset>[ topDividerCenter, bottomDividerCenter, ], ) ..path( color: pressedButtonBackgroundColor, excludes: <Offset>[ pressedButtonCenter, ], ), ); // Press down on the button. final TestGesture gesture = await tester.press(find.widgetWithText(CupertinoDialogAction, 'Option 2')); await tester.pump(); // While pressing the button, verify following expectations: // - Background excludes the pressed button // - Background includes the divider above and below the pressed button // - Pressed button background includes the pressed expect(actionsSectionBox, paints ..path( color: normalButtonBackgroundColor, // The background should contain the divider above and below the pressed // button. While pressed, surrounding dividers disappear, which means // they become part of the background. includes: <Offset>[ topDividerCenter, bottomDividerCenter, ], // The background path should not include the tapped button background... excludes: <Offset>[ pressedButtonCenter, ], ) // For a pressed button, a dedicated path is painted with a pressed button // background color... ..path( color: pressedButtonBackgroundColor, includes: <Offset>[ pressedButtonCenter, ], ), ); // We must explicitly cause an "up" gesture to avoid a crash. // todo(mattcarroll) remove this call when #19540 is fixed await gesture.up(); }); testWidgets('ScaleTransition animation for showCupertinoDialog()', (WidgetTester tester) async { await tester.pumpWidget( CupertinoApp( home: Center( child: Builder( builder: (BuildContext context) { return CupertinoButton( onPressed: () { showCupertinoDialog<void>( context: context, builder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content'), actions: <Widget>[ 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], closeTo(1.2, 0.01)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], closeTo(1.182, 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], closeTo(1.108, 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], closeTo(1.044, 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], closeTo(1.015, 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], closeTo(1.003, 0.001)); await tester.pump(const Duration(milliseconds: 50)); transform = tester.widget(find.byType(Transform)); expect(transform.transform[0], closeTo(1.000, 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<void>( context: context, builder: (BuildContext context) { return CupertinoAlertDialog( title: const Text('The title'), content: const Text('The content'), actions: <Widget>[ 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(); FadeTransition transition = tester.firstWidget(find.byType(FadeTransition)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, closeTo(0.10, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, closeTo(0.156, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, closeTo(0.324, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, closeTo(0.606, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.firstWidget(find.byType(FadeTransition)); expect(transition.opacity.value, closeTo(1.0, 0.001)); await tester.tap(find.text('Delete')); // Exit animation, look at reverse FadeTransition. await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); expect(transition.opacity.value, closeTo(0.358, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); expect(transition.opacity.value, closeTo(0.231, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); expect(transition.opacity.value, closeTo(0.128, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); expect(transition.opacity.value, closeTo(0.056, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); expect(transition.opacity.value, closeTo(0.013, 0.001)); await tester.pump(const Duration(milliseconds: 25)); transition = tester.widgetList(find.byType(FadeTransition)).elementAt(1); expect(transition.opacity.value, closeTo(0.0, 0.001)); }); } RenderBox findActionButtonRenderBoxByTitle(WidgetTester tester, String title) { final RenderObject buttonBox = tester.renderObject(find.widgetWithText(CupertinoDialogAction, title)); assert(buttonBox is RenderBox); return buttonBox; } 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; } Widget createAppWithButtonThatLaunchesDialog({WidgetBuilder dialogBuilder}) { return MaterialApp( home: Material( child: Center( child: Builder(builder: (BuildContext context) { return RaisedButton( onPressed: () { showDialog<void>( context: context, builder: dialogBuilder, ); }, child: const Text('Go'), ); }), ), ), ); } Widget boilerplate(Widget child) { return Directionality( textDirection: TextDirection.ltr, child: child, ); }