Skip to content

Commit 049a972

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

File tree

3 files changed

+199
-49
lines changed

3 files changed

+199
-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: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,113 @@
11
import 'package:checks/checks.dart';
2-
import 'package:flutter/widgets.dart';
2+
import 'package:flutter/material.dart';
33
import 'package:flutter_test/flutter_test.dart';
4-
import 'package:url_launcher/url_launcher.dart';
54
import 'package:zulip/widgets/dialog.dart';
65

76
import '../model/binding.dart';
7+
import 'dialog_checks.dart';
88
import 'test_app.dart';
99

1010
void main() {
1111
TestZulipBinding.ensureInitialized();
1212

13+
late BuildContext context;
14+
15+
const title = "Dialog Title";
16+
const message = "Dialog message.";
17+
18+
Future<void> prepare(WidgetTester tester) async {
19+
addTearDown(testBinding.reset);
20+
21+
await tester.pumpWidget(const TestZulipApp(
22+
child: Scaffold(body: Placeholder())));
23+
await tester.pump();
24+
context = tester.element(find.byType(Placeholder));
25+
}
26+
1327
group('showErrorDialog', () {
14-
testWidgets('tap "Learn more" button', (tester) async {
15-
addTearDown(testBinding.reset);
16-
await tester.pumpWidget(TestZulipApp());
28+
testWidgets('show error dialog', (tester) async {
29+
await prepare(tester);
30+
31+
showErrorDialog(context: context, title: title, message: message);
1732
await tester.pump();
18-
final element = tester.element(find.byType(Placeholder));
33+
checkErrorDialog(tester, expectedTitle: title, expectedMessage: message);
34+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
1935

20-
showErrorDialog(context: element, title: 'hello',
21-
learnMoreButtonUrl: Uri.parse('https://foo.example'));
36+
testWidgets('user closes error dialog', (tester) async {
37+
await prepare(tester);
38+
39+
showErrorDialog(context: context, title: title, message: message);
40+
await tester.pump();
41+
42+
final button = checkErrorDialog(tester, expectedTitle: title);
43+
await tester.tap(find.byWidget(button));
44+
await tester.pump();
45+
checkNoDialog(tester);
46+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
47+
48+
testWidgets('tap "Learn more" button', (tester) async {
49+
await prepare(tester);
50+
51+
final learnMoreButtonUrl = Uri.parse('https://foo.example');
52+
showErrorDialog(context: context, title: title, learnMoreButtonUrl: learnMoreButtonUrl);
2253
await tester.pump();
2354
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-
});
55+
56+
checkErrorDialog(tester, expectedTitle: title,
57+
expectedLearnMoreButtonUrl: learnMoreButtonUrl);
58+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
59+
});
60+
61+
group('showSuggestedActionDialog', () {
62+
const actionButtonText = "Action";
63+
64+
testWidgets('show suggested action dialog', (tester) async {
65+
await prepare(tester);
66+
67+
showSuggestedActionDialog(context: context, title: title, message: message,
68+
actionButtonText: actionButtonText, onActionButtonPress: () {});
69+
await tester.pump();
70+
71+
checkSuggestedActionDialog(tester, expectedTitle: title, expectedMessage: message,
72+
expectedActionButtonText: actionButtonText);
73+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
74+
75+
testWidgets('user presses action button', (tester) async {
76+
await prepare(tester);
77+
78+
bool wasPressed = false;
79+
void onActionButtonPress() {
80+
wasPressed = true;
81+
}
82+
showSuggestedActionDialog(context: context, title: title, message: message,
83+
actionButtonText: actionButtonText, onActionButtonPress: onActionButtonPress);
84+
await tester.pump();
85+
86+
final (actionButton, _) = checkSuggestedActionDialog(tester, expectedTitle: title,
87+
expectedMessage: message, expectedActionButtonText: actionButtonText);
88+
await tester.tap(find.byWidget(actionButton));
89+
await tester.pump();
90+
checkNoDialog(tester);
91+
check(wasPressed).isTrue();
92+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
93+
94+
testWidgets('user cancels', (tester) async {
95+
await prepare(tester);
96+
97+
bool wasPressed = false;
98+
void onActionButtonPress() {
99+
wasPressed = true;
100+
}
101+
showSuggestedActionDialog(context: context, title: title, message: message,
102+
actionButtonText: actionButtonText, onActionButtonPress: onActionButtonPress);
103+
await tester.pump();
104+
105+
final (_, cancelButton) = checkSuggestedActionDialog(tester, expectedTitle: title,
106+
expectedMessage: message, expectedActionButtonText: actionButtonText);
107+
await tester.tap(find.byWidget(cancelButton));
108+
await tester.pump();
109+
checkNoDialog(tester);
110+
check(wasPressed).isFalse();
111+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
28112
});
29113
}

0 commit comments

Comments
 (0)