1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
bool willPopValue = false;
class SamplePage extends StatefulWidget {
@override
SamplePageState createState() => SamplePageState();
}
class SamplePageState extends State<SamplePage> {
ModalRoute<void> _route;
Future<bool> _callback() async => willPopValue;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_route?.removeScopedWillPopCallback(_callback);
_route = ModalRoute.of(context);
_route?.addScopedWillPopCallback(_callback);
}
@override
void dispose() {
super.dispose();
_route?.removeScopedWillPopCallback(_callback);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample Page')),
);
}
}
int willPopCount = 0;
class SampleForm extends StatelessWidget {
const SampleForm({ Key key, this.callback }) : super(key: key);
final WillPopCallback callback;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Sample Form')),
body: SizedBox.expand(
child: Form(
onWillPop: () {
willPopCount += 1;
return callback();
},
child: const TextField(),
),
),
);
}
}
// Expose the protected hasScopedWillPopCallback getter
class TestPageRoute<T> extends MaterialPageRoute<T> {
TestPageRoute({ WidgetBuilder builder })
: super(builder: builder, maintainState: true);
bool get hasCallback => super.hasScopedWillPopCallback;
}
void main() {
testWidgets('ModalRoute scopedWillPopupCallback can inhibit back button', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: FlatButton(
child: const Text('X'),
onPressed: () {
showDialog<void>(
context: context,
builder: (BuildContext context) => SamplePage(),
);
},
),
);
},
),
),
),
);
expect(find.byTooltip('Back'), findsNothing);
expect(find.text('Sample Page'), findsNothing);
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Page'), findsOneWidget);
willPopValue = false;
await tester.tap(find.byTooltip('Back'));
await tester.pump();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Page'), findsOneWidget);
// Use didPopRoute() to simulate the system back button. Check that
// didPopRoute() indicates that the notification was handled.
final dynamic widgetsAppState = tester.state(find.byType(WidgetsApp));
expect(await widgetsAppState.didPopRoute(), isTrue);
expect(find.text('Sample Page'), findsOneWidget);
willPopValue = true;
await tester.tap(find.byTooltip('Back'));
await tester.pump();
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Page'), findsNothing);
});
testWidgets('Form.willPop can inhibit back button', (WidgetTester tester) async {
Widget buildFrame() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: FlatButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return SampleForm(
callback: () => Future<bool>.value(willPopValue),
);
},
));
},
),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Form'), findsOneWidget);
willPopValue = false;
willPopCount = 0;
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Complete the willPop() Future.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsOneWidget);
expect(willPopCount, 1);
willPopValue = true;
willPopCount = 0;
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Complete the willPop() Future.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsNothing);
expect(willPopCount, 1);
});
testWidgets('Form.willPop callbacks do not accumulate', (WidgetTester tester) async {
Future<bool> showYesNoAlert(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
actions: <Widget> [
FlatButton(
child: const Text('YES'),
onPressed: () { Navigator.of(context).pop(true); },
),
FlatButton(
child: const Text('NO'),
onPressed: () { Navigator.of(context).pop(false); },
),
],
);
},
);
}
Widget buildFrame() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: FlatButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) {
return SampleForm(
callback: () => showYesNoAlert(context),
);
}
));
},
),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Form'), findsOneWidget);
// Press the Scaffold's back button. This causes the willPop callback
// to run, which shows the YES/NO Alert Dialog. Veto the back operation
// by pressing the Alert's NO button.
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Call willPop which will show an Alert.
await tester.tap(find.text('NO'));
await tester.pump(); // Start the dismiss animation.
await tester.pump(); // Resolve the willPop callback.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsOneWidget);
// Do it again.
// Each time the Alert is shown and dismissed the FormState's
// didChangeDependencies() method runs. We're making sure that the
// didChangeDependencies() method doesn't add an extra willPop callback.
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Call willPop which will show an Alert.
await tester.tap(find.text('NO'));
await tester.pump(); // Start the dismiss animation.
await tester.pump(); // Resolve the willPop callback.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsOneWidget);
// This time really dismiss the SampleForm by pressing the Alert's
// YES button.
await tester.tap(find.byTooltip('Back'));
await tester.pump(); // Start the pop "back" operation.
await tester.pump(); // Call willPop which will show an Alert.
await tester.tap(find.text('YES'));
await tester.pump(); // Start the dismiss animation.
await tester.pump(); // Resolve the willPop callback.
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
expect(find.text('Sample Form'), findsNothing);
});
testWidgets('Route.scopedWillPop callbacks do not accumulate', (WidgetTester tester) async {
StateSetter contentsSetState; // call this to rebuild the route's SampleForm contents
bool contentsEmpty = false; // when true, don't include the SampleForm in the route
final TestPageRoute<void> route = TestPageRoute<void>(
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
contentsSetState = setState;
return contentsEmpty ? Container() : SampleForm(key: UniqueKey());
}
);
},
);
Widget buildFrame() {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Home')),
body: Builder(
builder: (BuildContext context) {
return Center(
child: FlatButton(
child: const Text('X'),
onPressed: () {
Navigator.of(context).push(route);
},
),
);
},
),
),
);
}
await tester.pumpWidget(buildFrame());
await tester.tap(find.text('X'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
expect(find.text('Sample Form'), findsOneWidget);
expect(route.hasCallback, isTrue);
// Rebuild the route's SampleForm child an additional 3x for good measure.
contentsSetState(() { });
await tester.pump();
contentsSetState(() { });
await tester.pump();
contentsSetState(() { });
await tester.pump();
// Now build the route's contents without the sample form.
contentsEmpty = true;
contentsSetState(() { });
await tester.pump();
expect(route.hasCallback, isFalse);
});
}