Skip to content

Commit 787a891

Browse files
committed
dialog: Use Cupertino-flavored alert dialogs on iOS
Fixes: zulip#996
1 parent 0417c87 commit 787a891

File tree

3 files changed

+187
-33
lines changed

3 files changed

+187
-33
lines changed

lib/widgets/dialog.dart

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
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

5-
Widget _dialogActionText(String text) {
7+
Widget _materialDialogActionText(String text) {
68
return Text(
79
text,
810

@@ -16,6 +18,20 @@ Widget _dialogActionText(String text) {
1618
);
1719
}
1820

21+
/// A platform-appropriate action for [AlertDialog.adaptive]'s [actions] param.
22+
Widget _adaptiveAction({required VoidCallback onPressed, required String text}) {
23+
switch (defaultTargetPlatform) {
24+
case TargetPlatform.android:
25+
case TargetPlatform.fuchsia:
26+
case TargetPlatform.linux:
27+
case TargetPlatform.windows:
28+
return TextButton(onPressed: onPressed, child: _materialDialogActionText(text));
29+
case TargetPlatform.iOS:
30+
case TargetPlatform.macOS:
31+
return CupertinoDialogAction(onPressed: onPressed, child: Text(text));
32+
}
33+
}
34+
1935
/// Tracks the status of a dialog, in being still open or already closed.
2036
///
2137
/// See also:
@@ -43,13 +59,13 @@ DialogStatus showErrorDialog({
4359
final zulipLocalizations = ZulipLocalizations.of(context);
4460
final future = showDialog<void>(
4561
context: context,
46-
builder: (BuildContext context) => AlertDialog(
62+
builder: (BuildContext context) => AlertDialog.adaptive(
4763
title: Text(title),
4864
content: message != null ? SingleChildScrollView(child: Text(message)) : null,
4965
actions: [
50-
TextButton(
66+
_adaptiveAction(
5167
onPressed: () => Navigator.pop(context),
52-
child: _dialogActionText(zulipLocalizations.errorDialogContinue)),
68+
text: zulipLocalizations.errorDialogContinue),
5369
]));
5470
return DialogStatus(future);
5571
}
@@ -64,18 +80,18 @@ void showSuggestedActionDialog({
6480
final zulipLocalizations = ZulipLocalizations.of(context);
6581
showDialog<void>(
6682
context: context,
67-
builder: (BuildContext context) => AlertDialog(
83+
builder: (BuildContext context) => AlertDialog.adaptive(
6884
title: Text(title),
6985
content: SingleChildScrollView(child: Text(message)),
7086
actions: [
71-
TextButton(
87+
_adaptiveAction(
7288
onPressed: () => Navigator.pop(context),
73-
child: _dialogActionText(zulipLocalizations.dialogCancel)),
74-
TextButton(
89+
text: zulipLocalizations.dialogCancel),
90+
_adaptiveAction(
7591
onPressed: () {
7692
onActionButtonPress();
7793
Navigator.pop(context);
7894
},
79-
child: _dialogActionText(actionButtonText ?? zulipLocalizations.dialogContinue)),
95+
text: actionButtonText ?? zulipLocalizations.dialogContinue),
8096
]));
8197
}

test/widgets/dialog_checks.dart

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
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';
57
import 'package:zulip/widgets/dialog.dart';
68

7-
/// In a widget test, check that showErrorDialog was called with the right text.
9+
/// In a widget test, check that [showErrorDialog] was called with the right text.
810
///
911
/// Checks for an error dialog matching an expected title
1012
/// and, optionally, matching an expected message. Fails if none is found.
@@ -15,22 +17,44 @@ Widget checkErrorDialog(WidgetTester tester, {
1517
required String expectedTitle,
1618
String? expectedMessage,
1719
}) {
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-
}
20+
switch (defaultTargetPlatform) {
21+
case TargetPlatform.android:
22+
case TargetPlatform.fuchsia:
23+
case TargetPlatform.linux:
24+
case TargetPlatform.windows:
25+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
26+
tester.widget(find.descendant(matchRoot: true,
27+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
28+
if (expectedMessage != null) {
29+
tester.widget(find.descendant(matchRoot: true,
30+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
31+
}
32+
33+
return tester.widget(find.descendant(of: find.byWidget(dialog),
34+
matching: find.widgetWithText(TextButton, 'OK')));
35+
36+
case TargetPlatform.iOS:
37+
case TargetPlatform.macOS:
38+
final dialog = tester.widget<CupertinoAlertDialog>(find.byType(CupertinoAlertDialog));
39+
tester.widget(find.descendant(matchRoot: true,
40+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
41+
if (expectedMessage != null) {
42+
tester.widget(find.descendant(matchRoot: true,
43+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
44+
}
2545

26-
return tester.widget(
27-
find.descendant(of: find.byWidget(dialog),
28-
matching: find.widgetWithText(TextButton, 'OK')));
46+
return tester.widget(find.descendant(of: find.byWidget(dialog),
47+
matching: find.widgetWithText(CupertinoDialogAction, 'OK')));
48+
}
2949
}
3050

31-
// TODO(#996) update this to check for per-platform flavors of alert dialog
51+
/// Checks that there is no error dialog.
52+
/// Fails if one is found.
3253
void checkNoErrorDialog(WidgetTester tester) {
54+
check(find.byType(Dialog)).findsNothing();
3355
check(find.byType(AlertDialog)).findsNothing();
56+
check(find.bySubtype<AlertDialog>()).findsNothing();
57+
check(find.byType(CupertinoAlertDialog)).findsNothing();
3458
}
3559

3660
/// In a widget test, check that [showSuggestedActionDialog] was called
@@ -47,19 +71,35 @@ void checkNoErrorDialog(WidgetTester tester) {
4771
required String expectedMessage,
4872
String? expectedActionButtonText,
4973
}) {
50-
final dialog = tester.widget<AlertDialog>(find.byType(AlertDialog));
51-
tester.widget(find.descendant(matchRoot: true,
52-
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
53-
tester.widget(find.descendant(matchRoot: true,
54-
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
74+
switch (defaultTargetPlatform) {
75+
case TargetPlatform.android:
76+
case TargetPlatform.fuchsia:
77+
case TargetPlatform.linux:
78+
case TargetPlatform.windows:
79+
final dialog = tester.widget<AlertDialog>(find.bySubtype<AlertDialog>());
80+
tester.widget(find.descendant(matchRoot: true,
81+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
82+
tester.widget(find.descendant(matchRoot: true,
83+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
5584

56-
final actionButton = tester.widget(
57-
find.descendant(of: find.byWidget(dialog),
58-
matching: find.widgetWithText(TextButton, expectedActionButtonText ?? 'Continue')));
85+
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog),
86+
matching: find.widgetWithText(TextButton, expectedActionButtonText ?? 'Continue')));
87+
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog),
88+
matching: find.widgetWithText(TextButton, 'Cancel')));
89+
return (actionButton, cancelButton);
5990

60-
final cancelButton = tester.widget(
61-
find.descendant(of: find.byWidget(dialog),
62-
matching: find.widgetWithText(TextButton, 'Cancel')));
91+
case TargetPlatform.iOS:
92+
case TargetPlatform.macOS:
93+
final dialog = tester.widget<CupertinoAlertDialog>(find.byType(CupertinoAlertDialog));
94+
tester.widget(find.descendant(matchRoot: true,
95+
of: find.byWidget(dialog.title!), matching: find.text(expectedTitle)));
96+
tester.widget(find.descendant(matchRoot: true,
97+
of: find.byWidget(dialog.content!), matching: find.text(expectedMessage)));
6398

64-
return (actionButton, cancelButton);
99+
final actionButton = tester.widget(find.descendant(of: find.byWidget(dialog),
100+
matching: find.widgetWithText(CupertinoDialogAction, expectedActionButtonText ?? 'Continue')));
101+
final cancelButton = tester.widget(find.descendant(of: find.byWidget(dialog),
102+
matching: find.widgetWithText(CupertinoDialogAction, 'Cancel')));
103+
return (actionButton, cancelButton);
104+
}
65105
}

test/widgets/dialog_test.dart

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'package:checks/checks.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:flutter_test/flutter_test.dart';
4+
import 'package:zulip/widgets/dialog.dart';
5+
6+
import '../model/binding.dart';
7+
import 'dialog_checks.dart';
8+
import 'test_app.dart';
9+
10+
void main() {
11+
TestZulipBinding.ensureInitialized();
12+
13+
late BuildContext context;
14+
15+
const String title = "Dialog Title";
16+
const String 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+
27+
group('showErrorDialog', () {
28+
testWidgets('show error dialog', (tester) async {
29+
await prepare(tester);
30+
31+
showErrorDialog(context: context, title: title, message: message);
32+
await tester.pump();
33+
checkErrorDialog(tester, expectedTitle: title, expectedMessage: message);
34+
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+
Widget button = checkErrorDialog(tester, expectedTitle: title);
44+
await tester.tap(find.byWidget(button));
45+
await tester.pump();
46+
checkNoDialog(tester);
47+
});
48+
});
49+
50+
group('showSuggestedActionDialog', () {
51+
const String actionButtonText = "Action";
52+
53+
testWidgets('show suggested action dialog', (tester) async {
54+
await prepare(tester);
55+
56+
showSuggestedActionDialog(context: context, title: title, message: message,
57+
actionButtonText: actionButtonText, onActionButtonPress: () {});
58+
await tester.pump();
59+
60+
checkSuggestedActionDialog(tester, expectedTitle: title, expectedMessage: message,
61+
expectedActionButtonText: actionButtonText);
62+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
63+
64+
testWidgets('user presses action button', (tester) async {
65+
await prepare(tester);
66+
67+
late String actionText;
68+
const String expectedActionText = "Action performed!";
69+
void onActionButtonPress() {
70+
actionText = expectedActionText;
71+
}
72+
showSuggestedActionDialog(context: context, title: title, message: message,
73+
actionButtonText: actionButtonText, onActionButtonPress: onActionButtonPress);
74+
await tester.pump();
75+
76+
final (Widget actionButton, _) = checkSuggestedActionDialog(tester, expectedTitle: title, expectedMessage: message,
77+
expectedActionButtonText: actionButtonText);
78+
await tester.tap(find.byWidget(actionButton));
79+
await tester.pump();
80+
checkNoDialog(tester);
81+
check(actionText).equals(expectedActionText);
82+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
83+
84+
testWidgets('user cancels', (tester) async {
85+
await prepare(tester);
86+
87+
showSuggestedActionDialog(context: context, title: title, message: message,
88+
actionButtonText: actionButtonText, onActionButtonPress: () {});
89+
await tester.pump();
90+
91+
final (_, Widget cancelButton) = checkSuggestedActionDialog(tester, expectedTitle: title, expectedMessage: message,
92+
expectedActionButtonText: actionButtonText);
93+
await tester.tap(find.byWidget(cancelButton));
94+
await tester.pump();
95+
checkNoDialog(tester);
96+
}, variant: const TargetPlatformVariant({TargetPlatform.android, TargetPlatform.iOS}));
97+
});
98+
}

0 commit comments

Comments
 (0)