| // 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(MaterialApp(home: const 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( |
| children: <TestSemantics>[ |
| TestSemantics(label: 'The Title'), |
| TestSemantics(label: 'Content'), |
| ], |
| ), |
| TestSemantics( |
| 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, |
| ); |
| } |