blob: 8fb23eb6fd425ab84dcf03b827ca670e65c9e0c7 [file] [log] [blame]
// 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(<String>['reduced-test-set'])
library;
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.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),
actionSheet: CupertinoActionSheet(
message: const Text('The title'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), 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 action sheet that
// has a message, no title, and no overscroll for any sections (in contrast
// to cupertinoActionSheet.dark-theme.png).
await expectLater(
find.byType(CupertinoApp),
matchesGoldenFile('cupertinoActionSheet.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),
actionSheet: CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: List<Widget>.generate(
20,
(int i) => CupertinoActionSheetAction(onPressed: () {}, child: Text('Button $i')),
),
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);
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 cupertinoActionSheet.light-theme.png).
await expectLater(
find.byType(CupertinoApp),
matchesGoldenFile('cupertinoActionSheet.overall-dark-theme.png'),
);
await gesture.up();
});
testWidgets('Button appearance is correct with text scaling', (WidgetTester tester) async {
// Verifies layout of action button in various text scaling by drawing
// buttons in all 12 iOS text scales in one golden image.
// The following function returns a CupertinoActionSheetAction that:
// * Has a fixed width
// * Is unconstrained in height
// * Is aligned center in a grid of fixed height
// * Is surrounded by a black border
const double buttonWidth = 400;
const double rowHeight = 100;
Widget testButton(double contextBodySize) {
const standardHigBody = 17.0;
final double contextScaleFactor = contextBodySize / standardHigBody;
return OverrideMediaQuery(
transformer: (MediaQueryData data) {
return data.copyWith(textScaler: TextScaler.linear(contextScaleFactor));
},
child: SizedBox(
height: rowHeight,
child: Center(
child: UnconstrainedBox(
child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(width: buttonWidth),
child: DecoratedBox(
decoration: BoxDecoration(border: Border.all()),
child: CupertinoActionSheetAction(onPressed: () {}, child: const Text('Button')),
),
),
),
),
),
);
}
await tester.pumpWidget(
CupertinoApp(
home: CupertinoPageScaffold(
child: Center(
child: Column(
children: <Widget>[
Row(children: <Widget>[/*xs*/ testButton(14), /*s*/ testButton(15)]),
Row(children: <Widget>[/*m*/ testButton(16), /*l*/ testButton(17)]),
Row(children: <Widget>[/*xl*/ testButton(19), /*xxl*/ testButton(21)]),
Row(children: <Widget>[/*xxxl*/ testButton(23), /*ax1*/ testButton(28)]),
Row(children: <Widget>[/*ax2*/ testButton(33), /*ax3*/ testButton(40)]),
Row(children: <Widget>[/*ax4*/ testButton(47), /*ax5*/ testButton(53)]),
],
),
),
),
),
);
final Iterable<RichText> buttons = tester.widgetList<RichText>(
find.text('Button', findRichText: true),
);
final Iterable<double?> sizes = buttons.map((RichText text) {
return text.textScaler.scale(text.text.style!.fontSize!);
});
expect(
sizes,
<double>[
21,
21,
21,
21,
23,
24,
24,
28,
33,
40,
47,
53,
].map((double size) => moreOrLessEquals(size, epsilon: 0.001)),
);
await expectLater(
find.byType(Column),
matchesGoldenFile('cupertinoActionSheet.textScaling.png'),
);
});
testWidgets('Verify that a tap on modal barrier dismisses an action sheet', (
WidgetTester tester,
) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(title: Text('Action Sheet')),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(find.text('Action Sheet'), findsOneWidget);
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pump();
expect(find.text('Action Sheet'), findsNothing);
});
testWidgets('Verify that a tap on title section (not buttons) does not dismiss an action sheet', (
WidgetTester tester,
) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(title: Text('Action Sheet')),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 5));
expect(find.text('Action Sheet'), findsOneWidget);
await tester.tap(find.text('Action Sheet'));
await tester.pump();
expect(find.text('Action Sheet'), findsOneWidget);
});
testWidgets('Action sheet destructive text style', (WidgetTester tester) async {
await tester.pumpWidget(
boilerplate(
CupertinoActionSheetAction(
isDestructiveAction: true,
child: const Text('Ok'),
onPressed: () {},
),
),
);
final DefaultTextStyle widget = tester.widget(find.widgetWithText(DefaultTextStyle, 'Ok'));
expect(
widget.style.color,
const CupertinoDynamicColor.withBrightnessAndContrast(
color: Color.fromARGB(255, 255, 59, 48),
darkColor: Color.fromARGB(255, 255, 69, 58),
highContrastColor: Color.fromARGB(255, 215, 0, 21),
darkHighContrastColor: Color.fromARGB(255, 255, 105, 97),
),
);
});
testWidgets('Action sheet dark mode', (WidgetTester tester) async {
final Widget action = CupertinoActionSheetAction(child: const Text('action'), onPressed: () {});
Brightness brightness = Brightness.light;
late StateSetter stateSetter;
TextStyle actionTextStyle(String text) {
return tester
.widget<DefaultTextStyle>(
find.descendant(
of: find.widgetWithText(CupertinoActionSheetAction, text),
matching: find.byType(DefaultTextStyle),
),
)
.style;
}
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
StatefulBuilder(
builder: (BuildContext context, StateSetter setter) {
stateSetter = setter;
return CupertinoTheme(
data: CupertinoThemeData(
brightness: brightness,
primaryColor: const CupertinoDynamicColor.withBrightnessAndContrast(
color: Color.fromARGB(255, 0, 122, 255),
darkColor: Color.fromARGB(255, 10, 132, 255),
highContrastColor: Color.fromARGB(255, 0, 64, 221),
darkHighContrastColor: Color.fromARGB(255, 64, 156, 255),
),
),
child: CupertinoActionSheet(actions: <Widget>[action]),
);
},
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(actionTextStyle('action').color!.value, const Color.fromARGB(255, 0, 122, 255).value);
stateSetter(() {
brightness = Brightness.dark;
});
await tester.pump();
expect(actionTextStyle('action').color!.value, const Color.fromARGB(255, 10, 132, 255).value);
});
testWidgets('Action sheet default text style', (WidgetTester tester) async {
await tester.pumpWidget(
boilerplate(
CupertinoActionSheetAction(
isDefaultAction: true,
child: const Text('Ok'),
onPressed: () {},
),
),
);
final DefaultTextStyle widget = tester.widget(find.widgetWithText(DefaultTextStyle, 'Ok'));
expect(widget.style.fontWeight, equals(FontWeight.w600));
});
testWidgets('Action sheet text styles are correct when both title and message are included', (
WidgetTester tester,
) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(title: Text('Action Sheet'), message: Text('An action sheet')),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
final DefaultTextStyle titleStyle = tester.firstWidget(
find.widgetWithText(DefaultTextStyle, 'Action Sheet'),
);
final DefaultTextStyle messageStyle = tester.firstWidget(
find.widgetWithText(DefaultTextStyle, 'An action sheet'),
);
expect(titleStyle.style.fontWeight, FontWeight.w600);
expect(messageStyle.style.fontWeight, FontWeight.w400);
});
testWidgets('Action sheet text styles are correct when title but no message is included', (
WidgetTester tester,
) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(title: Text('Action Sheet')),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
final DefaultTextStyle titleStyle = tester.firstWidget(
find.widgetWithText(DefaultTextStyle, 'Action Sheet'),
);
expect(titleStyle.style.fontWeight, FontWeight.w400);
});
testWidgets('Action sheet text styles are correct when message but no title is included', (
WidgetTester tester,
) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
const CupertinoActionSheet(message: Text('An action sheet')),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
final DefaultTextStyle messageStyle = tester.firstWidget(
find.widgetWithText(DefaultTextStyle, 'An action sheet'),
);
expect(messageStyle.style.fontWeight, FontWeight.w600);
});
testWidgets('Content section but no actions', (WidgetTester tester) async {
final scrollController = ScrollController();
addTearDown(scrollController.dispose);
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message.'),
messageScrollController: scrollController,
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Content section should be at the bottom left of action sheet
// (minus padding).
expect(
tester.getBottomLeft(find.byType(ClipRSuperellipse)),
tester.getBottomLeft(find.byType(CupertinoActionSheet)) - const Offset(-8.0, 8.0),
);
// Check that the dialog size is the same as the content section size
// (minus padding).
expect(
tester.getSize(find.byType(ClipRSuperellipse)).height,
tester.getSize(find.byType(CupertinoActionSheet)).height - 16.0,
);
expect(
tester.getSize(find.byType(ClipRSuperellipse)).width,
tester.getSize(find.byType(CupertinoActionSheet)).width - 16.0,
);
});
testWidgets('Actions but no content section', (WidgetTester tester) async {
final actionScrollController = ScrollController();
addTearDown(actionScrollController.dispose);
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
actionScrollController: actionScrollController,
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
final Finder finder = find.byElementPredicate((Element element) {
return element.widget.runtimeType.toString() == '_ActionSheetActionSection';
});
// Check that the title/message section is not displayed (action section is
// at the top of the action sheet + padding).
expect(
tester.getTopLeft(finder),
tester.getTopLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, 8.0),
);
expect(
tester.getTopLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, 8.0),
tester.getTopLeft(find.widgetWithText(CupertinoActionSheetAction, 'One')),
);
expect(
tester.getBottomLeft(find.byType(CupertinoActionSheet)) + const Offset(8.0, -8.0),
tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Two')),
);
});
testWidgets('Action section is scrollable', (WidgetTester tester) async {
final actionScrollController = ScrollController();
addTearDown(actionScrollController.dispose);
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return MediaQuery.withClampedTextScaling(
minScaleFactor: 3.0,
maxScaleFactor: 3.0,
child: CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message.'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Three'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Four'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Five'), onPressed: () {}),
],
actionScrollController: actionScrollController,
),
);
},
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// 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(CupertinoActionSheetAction, 'One')).dx,
equals(400.0),
);
expect(
tester.getCenter(find.widgetWithText(CupertinoActionSheetAction, 'Two')).dx,
equals(400.0),
);
expect(
tester.getCenter(find.widgetWithText(CupertinoActionSheetAction, 'Three')).dx,
equals(400.0),
);
expect(
tester.getCenter(find.widgetWithText(CupertinoActionSheetAction, 'Four')).dx,
equals(400.0),
);
expect(
tester.getCenter(find.widgetWithText(CupertinoActionSheetAction, 'Five')).dx,
equals(400.0),
);
// Check that the action buttons are the correct heights.
expect(
tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'One')).height,
equals(95.4),
);
expect(
tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'Two')).height,
equals(95.4),
);
expect(
tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'Three')).height,
equals(95.4),
);
expect(
tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'Four')).height,
equals(95.4),
);
expect(
tester.getSize(find.widgetWithText(CupertinoActionSheetAction, 'Five')).height,
equals(95.4),
);
});
testWidgets('Content section is scrollable', (WidgetTester tester) async {
final messageScrollController = ScrollController();
addTearDown(messageScrollController.dispose);
late double screenHeight;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
screenHeight = MediaQuery.heightOf(context);
return MediaQuery.withClampedTextScaling(
minScaleFactor: 3.0,
maxScaleFactor: 3.0,
child: CupertinoActionSheet(
title: const Text('The title'),
message: Text('Very long content' * 200),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
messageScrollController: messageScrollController,
),
);
},
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(messageScrollController.offset, 0.0);
messageScrollController.jumpTo(100.0);
expect(messageScrollController.offset, 100.0);
// Set the scroll position back to zero.
messageScrollController.jumpTo(0.0);
// Expect the action sheet to take all available height.
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, screenHeight);
});
testWidgets('CupertinoActionSheet scrollbars controllers should be different', (
WidgetTester tester,
) async {
// https://github.com/flutter/flutter/pull/81278
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: Text('Very long content' * 200),
actions: <Widget>[CupertinoActionSheetAction(child: const Text('One'), onPressed: () {})],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
final List<CupertinoScrollbar> scrollbars = find
.descendant(
of: find.byType(CupertinoActionSheet),
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);
});
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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
actions: List<Widget>.generate(
12,
(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 *')));
await tester.pumpAndSettle();
// The button should be pressed now, since the scrolling gesture has not
// taken over.
await expectLater(
find.byType(CupertinoActionSheet),
matchesGoldenFile('cupertinoActionSheet.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(CupertinoActionSheet),
matchesGoldenFile('cupertinoActionSheet.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(CupertinoActionSheet),
matchesGoldenFile('cupertinoActionSheet.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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
message: Text('message' * 300),
actions: List<Widget>.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.pump();
// The buttons should be out of the screen
expect(
tester.getTopLeft(find.text('Button 0')).dy,
greaterThan(tester.getBottomLeft(find.byType(CupertinoActionSheet)).dy),
);
await expectLater(
find.byType(CupertinoActionSheet),
matchesGoldenFile('cupertinoActionSheet.long-overscroll.0.png'),
);
});
testWidgets('Takes maximum vertical space with one action and long content', (
WidgetTester tester,
) async {
// Ensure that if the actions section is shorter than
// _kActionSheetActionsSectionMinHeight, the content section can be assigned
// with the remaining vertical space to fill up the maximal height.
late double screenHeight;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
screenHeight = MediaQuery.heightOf(context);
return CupertinoActionSheet(
message: Text('content ' * 1000),
actions: <Widget>[
CupertinoActionSheetAction(onPressed: () {}, child: const Text('Button 0')),
],
);
},
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
// Expect the action sheet to take all available height.
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, screenHeight);
});
testWidgets('Taps on button calls onPressed', (WidgetTester tester) async {
var wasPressed = false;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
actions: <Widget>[
CupertinoActionSheetAction(
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.tap(find.text('One'));
expect(wasPressed, isTrue);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('One'), findsNothing);
});
testWidgets('Can tap after scrolling', (WidgetTester tester) async {
int? wasPressed;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
actions: List<Widget>.generate(
20,
(int i) => CupertinoActionSheetAction(
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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
actions: <Widget>[
CupertinoActionSheetAction(
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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
actions: <Widget>[
CupertinoActionSheetAction(
child: const Text('One'),
onPressed: () {
expect(pressed, null);
pressed = 1;
Navigator.pop(context);
},
),
CupertinoActionSheetAction(
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(CupertinoActionSheet),
matchesGoldenFile('cupertinoActionSheet.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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
title: const Text('The title'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(
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);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('The title')));
await tester.pumpAndSettle();
await gesture.moveTo(tester.getCenter(find.text('Cancel')));
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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
title: const Text('The title'),
cancelButton: CupertinoActionSheetAction(
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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
message: Text('Long message' * 200),
actions: List<Widget>.generate(
10,
(int i) => CupertinoActionSheetAction(
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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
message: Text('Long message' * 200),
actions: List<Widget>.generate(
10,
(int i) => CupertinoActionSheetAction(
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(CupertinoActionSheetAction, '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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
title: const Text('The title'),
actions: List<Widget>.generate(
8,
(int i) => CupertinoActionSheetAction(
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(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
actions: List<Widget>.generate(
12,
(int i) => CupertinoActionSheetAction(
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.
//
// Regression test for https://github.com/flutter/flutter/issues/150980 .
var wasPressed = false;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
actions: <Widget>[
LegacyAction(
child: const Text('Legacy'),
onPressed: () {
expect(wasPressed, false);
wasPressed = true;
Navigator.pop(context);
},
),
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(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(CupertinoActionSheet),
matchesGoldenFile('cupertinoActionSheet.legacyButton.png'),
);
await gesture.up();
await tester.pumpAndSettle();
expect(wasPressed, isTrue);
expect(find.text('Legacy'), findsNothing);
});
testWidgets('Action sheet width is correct when given infinite horizontal space', (
WidgetTester tester,
) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Row(
children: <Widget>[
CupertinoActionSheet(
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
),
],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(tester.getSize(find.byType(CupertinoActionSheet)).width, 600.0);
});
testWidgets('Action sheet height is correct when given infinite vertical space', (
WidgetTester tester,
) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Column(
children: <Widget>[
CupertinoActionSheet(
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
),
],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, moreOrLessEquals(130.64));
});
testWidgets('1 action button with cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: Text('Very long content' * 200),
actions: <Widget>[CupertinoActionSheetAction(child: const Text('One'), onPressed: () {})],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
// Action section is size of one action button.
expect(findScrollableActionsSectionRenderBox(tester).size.height, 57.17);
});
testWidgets('2 action buttons with cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: Text('Very long content' * 200),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.0));
});
testWidgets('3 action buttons with cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: Text('Very long content' * 200),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Three'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.0));
});
testWidgets('4+ action buttons with cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: Text('Very long content' * 200),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Three'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Four'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.0));
});
testWidgets('1 action button without cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: Text('Very long content' * 200),
actions: <Widget>[CupertinoActionSheetAction(child: const Text('One'), onPressed: () {})],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, 57.17);
});
testWidgets('2+ action buttons without cancel button', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: Text('Very long content' * 200),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
expect(findScrollableActionsSectionRenderBox(tester).size.height, moreOrLessEquals(84.0));
});
testWidgets('Action sheet with just cancel button is correct', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
// The action sheet consists of only a cancel button, so the height should
// be cancel button height + padding.
const double expectedHeight =
57.17 // button height
+
8 // bottom edge padding
+
8; // top edge padding, since the screen has no top view padding
expect(tester.getSize(find.byType(CupertinoActionSheet)).height, expectedHeight);
expect(tester.getSize(find.byType(CupertinoActionSheet)).width, 600.0);
});
testWidgets('Cancel button tap calls onPressed', (WidgetTester tester) async {
var wasPressed = false;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return CupertinoActionSheet(
cancelButton: CupertinoActionSheetAction(
child: const Text('Cancel'),
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);
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('Cancel')));
await tester.pumpAndSettle();
// Verify that the cancel button shows the pressed color.
await expectLater(
find.byType(CupertinoActionSheet),
matchesGoldenFile('cupertinoActionSheet.pressedCancel.png'),
);
await gesture.up();
expect(wasPressed, isTrue);
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Cancel'), findsNothing);
});
testWidgets('Layout is correct when cancel button is present', (WidgetTester tester) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(
tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Cancel')).dy,
moreOrLessEquals(592.0),
);
expect(
tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'One')).dy,
moreOrLessEquals(469.36),
);
expect(
tester.getBottomLeft(find.widgetWithText(CupertinoActionSheetAction, 'Two')).dy,
moreOrLessEquals(526.83),
);
});
// Verify that on a phone with the given `viewSize` and `viewPadding`, the the
// main sheet of a full-height action sheet will have a size of
// `expectedSize`.
//
// The `viewSize` and `viewPadding` can be captured on simulator. Changing
// `expectedSize` should be accompanied by screenshot comparison.
Future<void> verifyMaximumSize(
WidgetTester tester, {
required Size viewSize,
required EdgeInsets viewPadding,
required Size expectedSize,
}) async {
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
await binding.setSurfaceSize(viewSize);
addTearDown(() => binding.setSurfaceSize(null));
await tester.pumpWidget(
OverrideMediaQuery(
transformer: (MediaQueryData data) {
return data.copyWith(size: viewSize, viewPadding: viewPadding, padding: viewPadding);
},
child: createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
actions: List<Widget>.generate(
20,
(int i) => CupertinoActionSheetAction(onPressed: () {}, child: Text('Button $i')),
),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
final Finder mainSheet = find.byElementPredicate((Element element) {
return element.widget.runtimeType.toString() == '_ActionSheetMainSheet';
});
expect(tester.getSize(mainSheet), expectedSize);
}
testWidgets('The maximum size is correct on iPhone SE gen 3', (WidgetTester tester) async {
const double expectedHeight =
667 // View height
-
20 // Top view padding
-
20 // Top widget padding
-
8; // Bottom edge padding
await verifyMaximumSize(
tester,
viewSize: const Size(375, 667),
viewPadding: const EdgeInsets.fromLTRB(0, 20, 0, 0),
expectedSize: const Size(359, expectedHeight),
);
});
testWidgets('The maximum size is correct on iPhone 13 Pro', (WidgetTester tester) async {
const double expectedHeight =
844 // View height
-
47 // Top view padding
-
47 // Top widget padding
-
34; // Bottom view padding
await verifyMaximumSize(
tester,
viewSize: const Size(390, 844),
viewPadding: const EdgeInsets.fromLTRB(0, 47, 0, 34),
expectedSize: const Size(374, expectedHeight),
);
});
testWidgets('The maximum size is correct on iPhone 15 Plus', (WidgetTester tester) async {
const double expectedHeight =
932 // View height
-
59 // Top view padding
-
54 // Top widget padding
-
34; // Bottom view padding
await verifyMaximumSize(
tester,
viewSize: const Size(430, 932),
viewPadding: const EdgeInsets.fromLTRB(0, 59, 0, 34),
expectedSize: const Size(414, expectedHeight),
);
});
testWidgets('The maximum size is correct on iPhone 13 Pro landscape', (
WidgetTester tester,
) async {
const double expectedWidth =
390 // View height
-
8 * 2; // Edge padding
const double expectedHeight =
390 // View height
-
8 // Top edge padding
-
21; // Bottom view padding
await verifyMaximumSize(
tester,
viewSize: const Size(844, 390),
viewPadding: const EdgeInsets.fromLTRB(47, 0, 47, 21),
expectedSize: const Size(expectedWidth, expectedHeight),
);
});
testWidgets('Action buttons shows pressed color 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(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(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(CupertinoActionSheet),
matchesGoldenFile('cupertinoActionSheet.pressed.png'),
);
await pointer.up();
});
testWidgets('Enter/exit animation is correct', (WidgetTester tester) async {
final enterRecorder = AnimationSheetBuilder(frameSize: const Size(600, 600));
addTearDown(enterRecorder.dispose);
final Widget target = createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
);
await tester.pumpWidget(enterRecorder.record(target));
// Enter animation
await tester.tap(find.text('Go'));
await tester.pumpFrames(enterRecorder.record(target), const Duration(milliseconds: 400));
await expectLater(
enterRecorder.collate(5),
matchesGoldenFile('cupertinoActionSheet.enter.png'),
);
final exitRecorder = AnimationSheetBuilder(frameSize: const Size(600, 600));
addTearDown(exitRecorder.dispose);
await tester.pumpWidget(exitRecorder.record(target));
// Exit animation
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pumpFrames(exitRecorder.record(target), const Duration(milliseconds: 450));
// Action sheet has disappeared
expect(find.byType(CupertinoActionSheet), findsNothing);
await expectLater(exitRecorder.collate(5), matchesGoldenFile('cupertinoActionSheet.exit.png'));
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
testWidgets('Animation is correct if entering is canceled halfway', (WidgetTester tester) async {
final recorder = AnimationSheetBuilder(frameSize: const Size(600, 600));
addTearDown(recorder.dispose);
final Widget target = createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
);
await tester.pumpWidget(recorder.record(target));
// Enter animation
await tester.tap(find.text('Go'));
await tester.pumpFrames(recorder.record(target), const Duration(milliseconds: 200));
// Exit animation
await tester.tapAt(const Offset(20.0, 20.0));
await tester.pumpFrames(recorder.record(target), const Duration(milliseconds: 450));
// Action sheet has disappeared
expect(find.byType(CupertinoActionSheet), findsNothing);
await expectLater(
recorder.collate(5),
matchesGoldenFile('cupertinoActionSheet.interrupted-enter.png'),
);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/56001
testWidgets('Action sheet semantics', (WidgetTester tester) async {
final semantics = SemanticsTester(tester);
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
cancelButton: CupertinoActionSheetAction(child: const Text('Cancel'), onPressed: () {}),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
expect(
semantics,
hasSemantics(
TestSemantics.root(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.scopesRoute, SemanticsFlag.namesRoute],
label: 'Alert',
role: SemanticsRole.dialog,
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics>[
TestSemantics(label: 'The title'),
TestSemantics(label: 'The message'),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.hasImplicitScrolling],
children: <TestSemantics>[
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
label: 'One',
),
TestSemantics(
flags: <SemanticsFlag>[
SemanticsFlag.isButton,
SemanticsFlag.isFocusable,
],
actions: <SemanticsAction>[
SemanticsAction.tap,
SemanticsAction.focus,
],
label: 'Two',
),
],
),
TestSemantics(
flags: <SemanticsFlag>[SemanticsFlag.isButton, SemanticsFlag.isFocusable],
actions: <SemanticsAction>[SemanticsAction.tap, SemanticsAction.focus],
label: 'Cancel',
),
],
),
],
),
],
),
],
),
ignoreId: true,
ignoreRect: true,
ignoreTransform: true,
),
);
semantics.dispose();
});
testWidgets(
'Conflicting scrollbars are not applied by ScrollBehavior to CupertinoActionSheet',
(WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/83819
final actionScrollController = ScrollController();
addTearDown(actionScrollController.dispose);
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
Builder(
builder: (BuildContext context) {
return MediaQuery.withClampedTextScaling(
minScaleFactor: 3.0,
maxScaleFactor: 3.0,
child: CupertinoActionSheet(
title: const Text('The title'),
message: const Text('The message.'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
],
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('Hovering over Cupertino action sheet action updates cursor to clickable on Web', (
WidgetTester tester,
) async {
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('Message'),
actions: <Widget>[CupertinoActionSheetAction(child: const Text('One'), onPressed: () {})],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
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('One'));
await gesture.moveTo(actionSheetAction);
await tester.pumpAndSettle();
expect(
RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1),
kIsWeb ? SystemMouseCursors.click : SystemMouseCursors.basic,
);
});
testWidgets('CupertinoActionSheet action cursor behavior', (WidgetTester tester) async {
const SystemMouseCursor customCursor = SystemMouseCursors.grab;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(
mouseCursor: customCursor,
onPressed: () {},
child: const Text('One'),
),
],
),
),
);
await tester.tap(find.text('Go'));
await tester.pump();
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('One'));
await gesture.moveTo(actionSheetAction);
await tester.pumpAndSettle();
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), customCursor);
});
testWidgets(
'Action sheets emits haptic vibration on sliding into a button',
(WidgetTester tester) async {
var vibrationCount = 0;
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (
MethodCall methodCall,
) async {
if (methodCall.method == 'HapticFeedback.vibrate') {
expect(methodCall.arguments, 'HapticFeedbackType.selectionClick');
vibrationCount += 1;
}
return null;
});
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('The title'),
actions: <Widget>[
CupertinoActionSheetAction(child: const Text('One'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Two'), onPressed: () {}),
CupertinoActionSheetAction(child: const Text('Three'), onPressed: () {}),
],
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
final TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('One')));
await tester.pumpAndSettle();
// Tapping down on a button should not emit vibration.
expect(vibrationCount, 0);
await gesture.moveTo(tester.getCenter(find.text('Two')));
await tester.pumpAndSettle();
expect(vibrationCount, 1);
await gesture.moveTo(tester.getCenter(find.text('Three')));
await tester.pumpAndSettle();
expect(vibrationCount, 2);
await gesture.up();
expect(vibrationCount, 2);
},
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets(
'CupertinoActionSheet appearance changes correctly when actions or cancel button is focused',
(WidgetTester tester) async {
final focusNodeOne = FocusNode(debugLabel: 'CupertinoActionSheetAction One');
final focusNodeTwo = FocusNode(debugLabel: 'CupertinoActionSheetAction Two');
final focusNodeCancel = FocusNode(debugLabel: 'CupertinoActionSheetAction Cancel');
addTearDown(focusNodeOne.dispose);
addTearDown(focusNodeTwo.dispose);
addTearDown(focusNodeCancel.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('Title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNodeOne,
child: const Text('One'),
),
CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNodeTwo,
child: const Text('Two'),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNodeCancel,
child: const Text('Cancel'),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
final Finder decoratedBoxBetweenTraversalGroupAndButtonBackgroundFinder = find.ancestor(
of: find.byElementPredicate((Element element) {
return element.widget.runtimeType.toString() == '_ActionSheetButtonBackground';
}),
matching: find.descendant(
of: find.byType(CupertinoFocusHalo),
matching: find.byType(DecoratedBox),
),
);
final Finder actionsDecoratedBoxFinder = find.ancestor(
of: find.widgetWithText(CupertinoActionSheetAction, 'One'),
matching: decoratedBoxBetweenTraversalGroupAndButtonBackgroundFinder,
);
final Finder cancelDecoratedBoxFinder = find.ancestor(
of: find.widgetWithText(CupertinoActionSheetAction, 'Cancel'),
matching: decoratedBoxBetweenTraversalGroupAndButtonBackgroundFinder,
);
ShapeBorder findBorder(Finder decoratedBoxFinder) {
final box = tester.widget(decoratedBoxFinder) as DecoratedBox;
final decoration = box.decoration as ShapeDecoration;
return decoration.shape;
}
BorderSide getExpectedBorderSide({required bool hasFocus}) => hasFocus
? BorderSide(
color:
HSLColor.fromColor(
CupertinoColors.activeBlue.withOpacity(kCupertinoFocusColorOpacity),
)
.withLightness(kCupertinoFocusColorBrightness)
.withSaturation(kCupertinoFocusColorSaturation)
.toColor(),
width: 3.5,
)
: BorderSide.none;
ShapeBorder getExpectedActionHaloBorder({required bool hasFocus}) => RoundedRectangleBorder(
side: getExpectedBorderSide(hasFocus: hasFocus),
borderRadius: kCupertinoButtonSizeBorderRadius[CupertinoButtonSize.large]!.copyWith(
topLeft: Radius.zero,
topRight: Radius.zero,
),
);
ShapeBorder getExpectedCancelHaloBorder({required bool hasFocus}) => RoundedRectangleBorder(
side: getExpectedBorderSide(hasFocus: hasFocus),
borderRadius: kCupertinoButtonSizeBorderRadius[CupertinoButtonSize.large]!,
);
expect(actionsDecoratedBoxFinder, findsOneWidget);
expect(cancelDecoratedBoxFinder, findsOneWidget);
expect(findBorder(actionsDecoratedBoxFinder), getExpectedActionHaloBorder(hasFocus: false));
expect(findBorder(cancelDecoratedBoxFinder), getExpectedCancelHaloBorder(hasFocus: false));
focusNodeOne.requestFocus();
await tester.pumpAndSettle();
expect(findBorder(actionsDecoratedBoxFinder), getExpectedActionHaloBorder(hasFocus: true));
expect(findBorder(cancelDecoratedBoxFinder), getExpectedCancelHaloBorder(hasFocus: false));
focusNodeTwo.requestFocus();
await tester.pumpAndSettle();
expect(findBorder(actionsDecoratedBoxFinder), getExpectedActionHaloBorder(hasFocus: true));
expect(findBorder(cancelDecoratedBoxFinder), getExpectedCancelHaloBorder(hasFocus: false));
focusNodeCancel.requestFocus();
await tester.pumpAndSettle();
expect(findBorder(actionsDecoratedBoxFinder), getExpectedActionHaloBorder(hasFocus: false));
expect(findBorder(cancelDecoratedBoxFinder), getExpectedCancelHaloBorder(hasFocus: true));
focusNodeCancel.unfocus();
await tester.pumpAndSettle();
expect(findBorder(actionsDecoratedBoxFinder), getExpectedActionHaloBorder(hasFocus: false));
expect(findBorder(cancelDecoratedBoxFinder), getExpectedCancelHaloBorder(hasFocus: false));
},
);
testWidgets('CupertinoActionSheetActions in CupertinoActionSheet can be focused and unfocused', (
WidgetTester tester,
) async {
final focusNodeOne = FocusNode(debugLabel: 'CupertinoActionSheetAction One');
final focusNodeTwo = FocusNode(debugLabel: 'CupertinoActionSheetAction Two');
final focusNodeCancel = FocusNode(debugLabel: 'CupertinoActionSheetAction Cancel');
addTearDown(focusNodeOne.dispose);
addTearDown(focusNodeTwo.dispose);
addTearDown(focusNodeCancel.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('Title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNodeOne,
child: const Text('One'),
),
CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNodeTwo,
child: const Text('Two'),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNodeCancel,
child: const Text('Cancel'),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isFalse);
expect(focusNodeTwo.hasPrimaryFocus, isFalse);
expect(focusNodeCancel.hasPrimaryFocus, isFalse);
focusNodeOne.requestFocus();
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isTrue);
expect(focusNodeTwo.hasPrimaryFocus, isFalse);
expect(focusNodeCancel.hasPrimaryFocus, isFalse);
focusNodeTwo.requestFocus();
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isFalse);
expect(focusNodeTwo.hasPrimaryFocus, isTrue);
expect(focusNodeCancel.hasPrimaryFocus, isFalse);
focusNodeTwo.unfocus();
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isFalse);
expect(focusNodeTwo.hasPrimaryFocus, isFalse);
expect(focusNodeCancel.hasPrimaryFocus, isFalse);
focusNodeCancel.requestFocus();
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isFalse);
expect(focusNodeTwo.hasPrimaryFocus, isFalse);
expect(focusNodeCancel.hasPrimaryFocus, isTrue);
focusNodeCancel.unfocus();
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isFalse);
expect(focusNodeTwo.hasPrimaryFocus, isFalse);
expect(focusNodeCancel.hasPrimaryFocus, isFalse);
});
testWidgets(
'CupertinoActionSheetActions in CupertinoActionSheet can be traversed with keyboard',
(WidgetTester tester) async {
final focusNodeOne = FocusNode(debugLabel: 'CupertinoActionSheetAction One');
final focusNodeTwo = FocusNode(debugLabel: 'CupertinoActionSheetAction Two');
final focusNodeCancel = FocusNode(debugLabel: 'CupertinoActionSheetAction Cancel');
addTearDown(focusNodeOne.dispose);
addTearDown(focusNodeTwo.dispose);
addTearDown(focusNodeCancel.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('Title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNodeOne,
child: const Text('One'),
),
CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNodeTwo,
child: const Text('Two'),
),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNodeCancel,
child: const Text('Cancel'),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isFalse);
expect(focusNodeTwo.hasPrimaryFocus, isFalse);
expect(focusNodeCancel.hasPrimaryFocus, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isTrue);
expect(focusNodeTwo.hasPrimaryFocus, isFalse);
expect(focusNodeCancel.hasPrimaryFocus, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isFalse);
expect(focusNodeTwo.hasPrimaryFocus, isTrue);
expect(focusNodeCancel.hasPrimaryFocus, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
expect(focusNodeOne.hasPrimaryFocus, isFalse);
expect(focusNodeTwo.hasPrimaryFocus, isFalse);
expect(focusNodeCancel.hasPrimaryFocus, isTrue);
},
);
testWidgets(
'CupertinoActionSheetAction in CupertinoActionSheet actions can be selected with keyboard',
(WidgetTester tester) async {
var isOneSelected = false;
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('Title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(
onPressed: () {
isOneSelected = true;
},
child: const Text('One'),
),
CupertinoActionSheetAction(onPressed: () {}, child: const Text('Two')),
],
cancelButton: CupertinoActionSheetAction(onPressed: () {}, child: const Text('Cancel')),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.pumpAndSettle();
expect(isOneSelected, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(isOneSelected, isTrue);
},
);
testWidgets(
'CupertinoActionSheetAction as CupertinoActionSheet cancel button can be selected with keyboard',
(WidgetTester tester) async {
var isCancelSelected = false;
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('Title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(onPressed: () {}, child: const Text('One')),
CupertinoActionSheetAction(onPressed: () {}, child: const Text('Two')),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () {
isCancelSelected = true;
},
child: const Text('Cancel'),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
await tester.sendKeyUpEvent(LogicalKeyboardKey.shift);
await tester.pumpAndSettle();
expect(isCancelSelected, isFalse);
await tester.sendKeyEvent(LogicalKeyboardKey.space);
await tester.pumpAndSettle();
expect(isCancelSelected, isTrue);
},
);
testWidgets('CupertinoActionSheetAction has correct focused appearance in light theme', (
WidgetTester tester,
) async {
final focusNode = FocusNode(debugLabel: 'CupertinoActionSheetAction');
addTearDown(focusNode.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final Color defaultLightFocusColor = HSLColor.fromColor(
CupertinoColors.activeBlue.withOpacity(kCupertinoButtonTintedOpacityLight),
).toColor();
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('Title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(onPressed: () {}, child: const Text('One')),
CupertinoActionSheetAction(onPressed: () {}, child: const Text('Two')),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNode,
child: const Text('Cancel'),
),
),
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
final Finder decoratedBoxFinder = find.descendant(
of: find.byType(CupertinoActionSheetAction),
matching: find.byType(DecoratedBox),
);
expect(decoratedBoxFinder, findsNothing);
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(decoratedBoxFinder, findsOneWidget);
final decoration = tester.widget<DecoratedBox>(decoratedBoxFinder).decoration as BoxDecoration;
expect(decoration.color, defaultLightFocusColor);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(decoratedBoxFinder, findsNothing);
});
testWidgets('CupertinoActionSheetAction has correct focused appearance in dark theme', (
WidgetTester tester,
) async {
final focusNode = FocusNode(debugLabel: 'CupertinoActionSheetAction');
addTearDown(focusNode.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
final Color defaultDarkFocusColor = HSLColor.fromColor(
CupertinoColors.activeBlue.withOpacity(kCupertinoButtonTintedOpacityDark),
).toColor();
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('Title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(onPressed: () {}, child: const Text('One')),
CupertinoActionSheetAction(onPressed: () {}, child: const Text('Two')),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNode,
child: const Text('Cancel'),
),
),
brightness: Brightness.dark,
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
final Finder decoratedBoxFinder = find.descendant(
of: find.byType(CupertinoActionSheetAction),
matching: find.byType(DecoratedBox),
);
expect(decoratedBoxFinder, findsNothing);
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(decoratedBoxFinder, findsOneWidget);
final decoration = tester.widget<DecoratedBox>(decoratedBoxFinder).decoration as BoxDecoration;
expect(decoration.color, defaultDarkFocusColor);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(decoratedBoxFinder, findsNothing);
});
testWidgets('CupertinoActionSheetAction has correct focused appearance with custom focus color', (
WidgetTester tester,
) async {
final focusNode = FocusNode(debugLabel: 'CupertinoActionSheetAction');
addTearDown(focusNode.dispose);
tester.binding.focusManager.highlightStrategy = FocusHighlightStrategy.alwaysTraditional;
const focusColor = Color(0xffffaaaa);
final Color defaultDarkFocusColor = HSLColor.fromColor(
focusColor.withOpacity(kCupertinoButtonTintedOpacityDark),
).toColor();
await tester.pumpWidget(
createAppWithButtonThatLaunchesActionSheet(
CupertinoActionSheet(
title: const Text('Title'),
message: const Text('Message'),
actions: <Widget>[
CupertinoActionSheetAction(onPressed: () {}, child: const Text('One')),
CupertinoActionSheetAction(onPressed: () {}, child: const Text('Two')),
],
cancelButton: CupertinoActionSheetAction(
onPressed: () {},
focusNode: focusNode,
focusColor: focusColor,
child: const Text('Cancel'),
),
),
brightness: Brightness.dark,
),
);
await tester.tap(find.text('Go'));
await tester.pumpAndSettle();
final Finder decoratedBoxFinder = find.descendant(
of: find.byType(CupertinoActionSheetAction),
matching: find.byType(DecoratedBox),
);
expect(decoratedBoxFinder, findsNothing);
focusNode.requestFocus();
await tester.pumpAndSettle();
expect(decoratedBoxFinder, findsOneWidget);
final decoration = tester.widget<DecoratedBox>(decoratedBoxFinder).decoration as BoxDecoration;
expect(decoration.color, defaultDarkFocusColor);
focusNode.unfocus();
await tester.pumpAndSettle();
expect(decoratedBoxFinder, findsNothing);
});
}
RenderBox findScrollableActionsSectionRenderBox(WidgetTester tester) {
final RenderObject actionsSection = tester.renderObject(
find.byElementPredicate((Element element) {
return element.widget.runtimeType.toString() == '_ActionSheetActionSection';
}),
);
assert(actionsSection is RenderBox);
return actionsSection as RenderBox;
}
Widget createAppWithButtonThatLaunchesActionSheet(
Widget actionSheet, {
Brightness brightness = Brightness.light,
}) {
return CupertinoApp(
theme: CupertinoThemeData(brightness: brightness),
home: Center(
child: Builder(
builder: (BuildContext context) {
return CupertinoButton(
onPressed: () {
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return actionSheet;
},
);
},
child: const Text('Go'),
);
},
),
),
);
}
// Shows an app that has a button with text "Go", and clicking this button
// displays the `actionSheet` 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.actionSheet});
final CupertinoThemeData theme;
final Widget actionSheet;
@override
TestScaffoldAppState createState() => TestScaffoldAppState();
}
class TestScaffoldAppState extends State<TestScaffoldApp> {
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;
});
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) {
return widget.actionSheet;
},
);
},
child: const Text('Go'),
),
),
),
),
);
}
}
Widget boilerplate(Widget child) {
return Directionality(textDirection: TextDirection.ltr, child: child);
}
typedef MediaQueryTransformer = MediaQueryData Function(MediaQueryData);
class OverrideMediaQuery extends StatelessWidget {
const OverrideMediaQuery({super.key, required this.transformer, required this.child});
final MediaQueryTransformer transformer;
final Widget child;
@override
Widget build(BuildContext context) {
final MediaQueryData currentData = MediaQuery.of(context);
return MediaQuery(data: transformer(currentData), child: child);
}
}
// 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: 57),
child: Container(
alignment: AlignmentDirectional.center,
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 10.0),
child: child,
),
),
);
}
}