Skip to content

Commit 01f73f5

Browse files
committed
dialog: Use Cupertino-flavored alert dialogs on iOS
Fixes: zulip#996
1 parent 40165ef commit 01f73f5

File tree

3 files changed

+201
-49
lines changed

3 files changed

+201
-49
lines changed

lib/widgets/dialog.dart

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import 'package:flutter/cupertino.dart';
2+
import 'package:flutter/foundation.dart';
13
import 'package:flutter/material.dart';
24

35
import '../generated/l10n/zulip_localizations.dart';
46
import 'actions.dart';
57

6-
Widget _dialogActionText(String text) {
8+
Widget _materialDialogActionText(String text) {
79
return Text(
810
text,
911

@@ -17,6 +19,20 @@ Widget _dialogActionText(String text) {
1719
);
1820
}
1921

22+
/// A platform-appropriate action for [AlertDialog.adaptive]'s [actions] param.
23+
Widget _adaptiveAction({required VoidCallback onPressed, required String text}) {
24+
switch (defaultTargetPlatform) {
25+
case TargetPlatform.android:
26+
case TargetPlatform.fuchsia:
27+
case TargetPlatform.linux:
28+
case TargetPlatform.windows:
29+
return TextButton(onPressed: onPressed, child: _materialDialogActionText(text));
30+
case TargetPlatform.iOS:
31+
case TargetPlatform.macOS:
32+
return CupertinoDialogAction(onPressed: onPressed, child: Text(text));
33+
}
34+
}
35+
2036
/// Tracks the status of a dialog, in being still open or already closed.
2137
///
2238
/// See also:
@@ -46,17 +62,18 @@ DialogStatus showErrorDialog({
4662
final zulipLocalizations = ZulipLocalizations.of(context);
4763
final future = showDialog<void>(
4864
context: context,
49-
builder: (BuildContext context) => AlertDialog(
65+
builder: (BuildContext context) => AlertDialog.adaptive(
5066
title: Text(title),
5167
content: message != null ? SingleChildScrollView(child: Text(message)) : null,
5268
actions: [
5369
if (learnMoreButtonUrl != null)
54-
TextButton(
70+
_adaptiveAction(
5571
onPressed: () => PlatformActions.launchUrl(context, learnMoreButtonUrl),
56-
child: _dialogActionText(zulipLocalizations.errorDialogLearnMore)),
57-
TextButton(
72+
text: zulipLocalizations.errorDialogLearnMore,
73+
),
74+
_adaptiveAction(
5875
onPressed: () => Navigator.pop(context),
59-
child: _dialogActionText(zulipLocalizations.errorDialogContinue)),
76+
text: zulipLocalizations.errorDialogContinue),
6077
]));
6178
return DialogStatus(future);
6279
}
@@ -71,18 +88,18 @@ void showSuggestedActionDialog({
7188
final zulipLocalizations = ZulipLocalizations.of(context);
7289
showDialog<void>(
7390
context: context,
74-
builder: (BuildContext context) => AlertDialog(
91+
builder: (BuildContext context) => AlertDialog.adaptive(
7592
title: Text(title),
7693
content: SingleChildScrollView(child: Text(message)),
7794
actions: [
78-
TextButton(
95+
_adaptiveAction(
7996
onPressed: () => Navigator.pop(context),
80-
child: _dialogActionText(zulipLocalizations.dialogCancel)),
81-
TextButton(
97+
text: zulipLocalizations.dialogCancel),
98+
_adaptiveAction(
8299
onPressed: () {
83100
onActionButtonPress();
84101
Navigator.pop(context);
85102
},
86-
child: _dialogActionText(actionButtonText ?? zulipLocalizations.dialogContinue)),
103+
text: actionButtonText ?? zulipLocalizations.dialogContinue),
87104
]));
88105
}

test/widgets/dialog_checks.dart

Lines changed: 75 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
import 'package:checks/checks.dart';
2+
import 'package:flutter/cupertino.dart';
3+
import 'package:flutter/foundation.dart';
24
import 'package:flutter/material.dart';
35
import 'package:flutter_checks/flutter_checks.dart';
46
import 'package:flutter_test/flutter_test.dart';
7+
import 'package:url_launcher/url_launcher.dart';
58
import 'package:zulip/widgets/dialog.dart';
69

7-
/// In a widget test, check that showErrorDialog was called with the right text.
10+
import '../model/binding.dart';
11+
12+
/// In a widget test, check that [showErrorDialog] was called with the right text.
813
///
914
/// Checks for an error dialog matching an expected title
1015
/// and, optionally, matching an expected message. Fails if none is found.
@@ -14,27 +19,55 @@ import 'package:zulip/widgets/dialog.dart';
1419
Widget checkErrorDialog(WidgetTester tester, {
1520
required String expectedTitle,
1621
String? expectedMessage,
22+
Uri? expectedLearnMoreButtonUrl,
1723
}) {
18-
final dialog = tester.widget<AlertDialog>(find.byType(AlertDialog));
19-
tester.widget(find.descendant(matchRoot: true,
20-
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
21-
if (expectedMessage != null) {
22-
tester.widget(find.descendant(matchRoot: true,
23-
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
24-
}
24+
switch (defaultTargetPlatform) {
25+
case TargetPlatform.android:
26+
case TargetPlatform.fuchsia:
27+
case TargetPlatform.linux:
28+
case TargetPlatform.windows:
29+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
30+
tester.widget(find.descendant(matchRoot: true,
31+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
32+
if (expectedMessage != null) {
33+
tester.widget(find.descendant(matchRoot: true,
34+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
35+
}
36+
if (expectedLearnMoreButtonUrl != null) {
37+
check(testBinding.takeLaunchUrlCalls()).single.equals((
38+
url: expectedLearnMoreButtonUrl,
39+
mode: LaunchMode.inAppBrowserView));
40+
}
41+
42+
return tester.widget(find.descendant(of: find.byWidget(dialog),
43+
matching: find.widgetWithText(TextButton, 'OK')));
2544

26-
// TODO check "Learn more" button?
45+
case TargetPlatform.iOS:
46+
case TargetPlatform.macOS:
47+
final dialog = tester.widget<CupertinoAlertDialog>(find.byType(CupertinoAlertDialog));
48+
tester.widget(find.descendant(matchRoot: true,
49+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
50+
if (expectedMessage != null) {
51+
tester.widget(find.descendant(matchRoot: true,
52+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
53+
}
54+
if (expectedLearnMoreButtonUrl != null) {
55+
check(testBinding.takeLaunchUrlCalls()).single.equals((
56+
url: expectedLearnMoreButtonUrl,
57+
mode: LaunchMode.externalApplication));
58+
}
2759

28-
return tester.widget(
29-
find.descendant(of: find.byWidget(dialog),
30-
matching: find.widgetWithText(TextButton, 'OK')));
60+
return tester.widget(find.descendant(of: find.byWidget(dialog),
61+
matching: find.widgetWithText(CupertinoDialogAction, 'OK')));
62+
}
3163
}
3264

33-
// TODO(#996) update this to check for per-platform flavors of alert dialog
3465
/// Checks that there is no dialog.
3566
/// Fails if one is found.
3667
void checkNoDialog(WidgetTester tester) {
37-
check(find.byType(AlertDialog)).findsNothing();
68+
check(find.byType(Dialog)).findsNothing();
69+
check(find.bySubtype<AlertDialog>()).findsNothing();
70+
check(find.byType(CupertinoAlertDialog)).findsNothing();
3871
}
3972

4073
/// In a widget test, check that [showSuggestedActionDialog] was called
@@ -51,19 +84,35 @@ void checkNoDialog(WidgetTester tester) {
5184
required String expectedMessage,
5285
String? expectedActionButtonText,
5386
}) {
54-
final dialog = tester.widget<AlertDialog>(find.byType(AlertDialog));
55-
tester.widget(find.descendant(matchRoot: true,
56-
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
57-
tester.widget(find.descendant(matchRoot: true,
58-
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
87+
switch (defaultTargetPlatform) {
88+
case TargetPlatform.android:
89+
case TargetPlatform.fuchsia:
90+
case TargetPlatform.linux:
91+
case TargetPlatform.windows:
92+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
93+
tester.widget(find.descendant(matchRoot: true,
94+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
95+
tester.widget(find.descendant(matchRoot: true,
96+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
5997

60-
final actionButton = tester.widget(
61-
find.descendant(of: find.byWidget(dialog),
62-
matching: find.widgetWithText(TextButton, expectedActionButtonText ?? 'Continue')));
98+
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog),
99+
matching: find.widgetWithText(TextButton, expectedActionButtonText ?? 'Continue')));
100+
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog),
101+
matching: find.widgetWithText(TextButton, 'Cancel')));
102+
return (actionButton, cancelButton);
63103

64-
final cancelButton = tester.widget(
65-
find.descendant(of: find.byWidget(dialog),
66-
matching: find.widgetWithText(TextButton, 'Cancel')));
104+
case TargetPlatform.iOS:
105+
case TargetPlatform.macOS:
106+
final dialog = tester.widget<CupertinoAlertDialog>(find.byType(CupertinoAlertDialog));
107+
tester.widget(find.descendant(matchRoot: true,
108+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
109+
tester.widget(find.descendant(matchRoot: true,
110+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
67111

68-
return (actionButton, cancelButton);
112+
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog),
113+
matching: find.widgetWithText(CupertinoDialogAction, expectedActionButtonText ?? 'Continue')));
114+
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog),
115+
matching: find.widgetWithText(CupertinoDialogAction, 'Cancel')));
116+
return (actionButton, cancelButton);
117+
}
69118
}

test/widgets/dialog_test.dart

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,115 @@
11
import 'package:checks/checks.dart';
2-
import 'package:flutter/widgets.dart';
2+
import 'package:flutter/foundation.dart';
3+
import 'package:flutter/material.dart';
34
import 'package:flutter_test/flutter_test.dart';
4-
import 'package:url_launcher/url_launcher.dart';
55
import 'package:zulip/widgets/dialog.dart';
66

77
import '../model/binding.dart';
8+
import 'dialog_checks.dart';
89
import 'test_app.dart';
910

1011
void main() {
1112
TestZulipBinding.ensureInitialized();
1213

14+
late BuildContext context;
15+
16+
const title = "Dialog Title";
17+
const message = "Dialog message.";
18+
19+
Future<void> prepare(WidgetTester tester) async {
20+
addTearDown(testBinding.reset);
21+
22+
await tester.pumpWidget(const TestZulipApp(
23+
child: Scaffold(body: Placeholder())));
24+
await tester.pump();
25+
context = tester.element(find.byType(Placeholder));
26+
}
27+
1328
group('showErrorDialog', () {
14-
testWidgets('tap "Learn more" button', (tester) async {
15-
addTearDown(testBinding.reset);
16-
await tester.pumpWidget(TestZulipApp());
29+
testWidgets('show error dialog', (tester) async {
30+
await prepare(tester);
31+
32+
showErrorDialog(context: context, title: title, message: message);
33+
await tester.pump();
34+
checkErrorDialog(tester, expectedTitle: title, expectedMessage: message);
35+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
36+
37+
testWidgets('user closes error dialog', (tester) async {
38+
await prepare(tester);
39+
40+
showErrorDialog(context: context, title: title, message: message);
41+
await tester.pump();
42+
43+
final button = checkErrorDialog(tester, expectedTitle: title);
44+
await tester.tap(find.byWidget(button));
1745
await tester.pump();
18-
final element = tester.element(find.byType(Placeholder));
46+
checkNoDialog(tester);
47+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
48+
49+
testWidgets('tap "Learn more" button', (tester) async {
50+
await prepare(tester);
51+
52+
final learnMoreButtonUrl = Uri.parse('https://foo.example');
1953

20-
showErrorDialog(context: element, title: 'hello',
21-
learnMoreButtonUrl: Uri.parse('https://foo.example'));
54+
showErrorDialog(context: context, title: title, learnMoreButtonUrl: learnMoreButtonUrl);
2255
await tester.pump();
2356
await tester.tap(find.text('Learn more'));
24-
check(testBinding.takeLaunchUrlCalls()).single.equals((
25-
url: Uri.parse('https://foo.example'),
26-
mode: LaunchMode.inAppBrowserView));
27-
});
57+
58+
checkErrorDialog(tester, expectedTitle: title,
59+
expectedLearnMoreButtonUrl: learnMoreButtonUrl);
60+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
61+
});
62+
63+
group('showSuggestedActionDialog', () {
64+
const actionButtonText = "Action";
65+
66+
testWidgets('show suggested action dialog', (tester) async {
67+
await prepare(tester);
68+
69+
showSuggestedActionDialog(context: context, title: title, message: message,
70+
actionButtonText: actionButtonText, onActionButtonPress: () {});
71+
await tester.pump();
72+
73+
checkSuggestedActionDialog(tester, expectedTitle: title, expectedMessage: message,
74+
expectedActionButtonText: actionButtonText);
75+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
76+
77+
testWidgets('user presses action button', (tester) async {
78+
await prepare(tester);
79+
80+
bool wasPressed = false;
81+
void onActionButtonPress() {
82+
wasPressed = true;
83+
}
84+
showSuggestedActionDialog(context: context, title: title, message: message,
85+
actionButtonText: actionButtonText, onActionButtonPress: onActionButtonPress);
86+
await tester.pump();
87+
88+
final (actionButton, _) = checkSuggestedActionDialog(tester, expectedTitle: title,
89+
expectedMessage: message, expectedActionButtonText: actionButtonText);
90+
await tester.tap(find.byWidget(actionButton));
91+
await tester.pump();
92+
checkNoDialog(tester);
93+
check(wasPressed).isTrue();
94+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
95+
96+
testWidgets('user cancels', (tester) async {
97+
await prepare(tester);
98+
99+
bool wasPressed = false;
100+
void onActionButtonPress() {
101+
wasPressed = true;
102+
}
103+
showSuggestedActionDialog(context: context, title: title, message: message,
104+
actionButtonText: actionButtonText, onActionButtonPress: onActionButtonPress);
105+
await tester.pump();
106+
107+
final (_, cancelButton) = checkSuggestedActionDialog(tester, expectedTitle: title,
108+
expectedMessage: message, expectedActionButtonText: actionButtonText);
109+
await tester.tap(find.byWidget(cancelButton));
110+
await tester.pump();
111+
checkNoDialog(tester);
112+
check(wasPressed).isFalse();
113+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
28114
});
29115
}

0 commit comments

Comments
 (0)