1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'package:flutter/foundation.dart'; |
6 | import 'package:flutter/material.dart'; |
7 | import 'package:flutter/services.dart'; |
8 | import 'package:flutter_test/flutter_test.dart'; |
9 | |
10 | import 'navigator_utils.dart'; |
11 | |
12 | void main() { |
13 | bool? lastFrameworkHandlesBack; |
14 | setUp(() async { |
15 | lastFrameworkHandlesBack = null; |
16 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( |
17 | SystemChannels.platform, |
18 | (MethodCall methodCall) async { |
19 | if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack' ) { |
20 | expect(methodCall.arguments, isA<bool>()); |
21 | lastFrameworkHandlesBack = methodCall.arguments as bool; |
22 | } |
23 | return; |
24 | }, |
25 | ); |
26 | await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( |
27 | 'flutter/lifecycle' , |
28 | const StringCodec().encodeMessage(AppLifecycleState.resumed.toString()), |
29 | (ByteData? data) {}, |
30 | ); |
31 | }); |
32 | |
33 | tearDown(() { |
34 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( |
35 | SystemChannels.platform, |
36 | null, |
37 | ); |
38 | }); |
39 | |
40 | testWidgets('toggling canPop on root route allows/prevents backs' , (WidgetTester tester) async { |
41 | bool canPop = false; |
42 | late StateSetter setState; |
43 | late BuildContext context; |
44 | await tester.pumpWidget( |
45 | MaterialApp( |
46 | initialRoute: '/' , |
47 | routes: <String, WidgetBuilder>{ |
48 | '/' : |
49 | (BuildContext buildContext) => Scaffold( |
50 | body: StatefulBuilder( |
51 | builder: (BuildContext buildContext, StateSetter stateSetter) { |
52 | context = buildContext; |
53 | setState = stateSetter; |
54 | return PopScope<Object?>( |
55 | canPop: canPop, |
56 | child: const Center( |
57 | child: Column( |
58 | mainAxisAlignment: MainAxisAlignment.center, |
59 | children: <Widget>[Text('Home/PopScope Page' )], |
60 | ), |
61 | ), |
62 | ); |
63 | }, |
64 | ), |
65 | ), |
66 | }, |
67 | ), |
68 | ); |
69 | |
70 | expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop); |
71 | |
72 | setState(() { |
73 | canPop = true; |
74 | }); |
75 | await tester.pump(); |
76 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
77 | expect(lastFrameworkHandlesBack, isFalse); |
78 | } |
79 | expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.bubble); |
80 | }, variant: TargetPlatformVariant.all()); |
81 | |
82 | testWidgets('pop scope can receive result' , (WidgetTester tester) async { |
83 | Object? receivedResult; |
84 | final Object poppedResult = Object(); |
85 | final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>(); |
86 | await tester.pumpWidget( |
87 | MaterialApp( |
88 | initialRoute: '/' , |
89 | navigatorKey: nav, |
90 | home: Scaffold( |
91 | body: PopScope<Object?>( |
92 | canPop: false, |
93 | onPopInvokedWithResult: (bool didPop, Object? result) { |
94 | receivedResult = result; |
95 | }, |
96 | child: const Center( |
97 | child: Column( |
98 | mainAxisAlignment: MainAxisAlignment.center, |
99 | children: <Widget>[Text('Home/PopScope Page' )], |
100 | ), |
101 | ), |
102 | ), |
103 | ), |
104 | ), |
105 | ); |
106 | |
107 | nav.currentState!.maybePop(poppedResult); |
108 | await tester.pumpAndSettle(); |
109 | expect(receivedResult, poppedResult); |
110 | }, variant: TargetPlatformVariant.all()); |
111 | |
112 | testWidgets( |
113 | 'pop scope can have Object? generic type while route has stricter generic type' , |
114 | (WidgetTester tester) async { |
115 | Object? receivedResult; |
116 | const int poppedResult = 13; |
117 | final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>(); |
118 | await tester.pumpWidget( |
119 | MaterialApp( |
120 | initialRoute: '/' , |
121 | navigatorKey: nav, |
122 | home: Scaffold( |
123 | body: PopScope<Object?>( |
124 | canPop: false, |
125 | onPopInvokedWithResult: (bool didPop, Object? result) { |
126 | receivedResult = result; |
127 | }, |
128 | child: const Center( |
129 | child: Column( |
130 | mainAxisAlignment: MainAxisAlignment.center, |
131 | children: <Widget>[Text('Home/PopScope Page' )], |
132 | ), |
133 | ), |
134 | ), |
135 | ), |
136 | ), |
137 | ); |
138 | |
139 | nav.currentState!.push( |
140 | MaterialPageRoute<int>( |
141 | builder: (BuildContext context) { |
142 | return Scaffold( |
143 | body: PopScope<Object?>( |
144 | canPop: false, |
145 | onPopInvokedWithResult: (bool didPop, Object? result) { |
146 | receivedResult = result; |
147 | }, |
148 | child: const Center(child: Text('new page' )), |
149 | ), |
150 | ); |
151 | }, |
152 | ), |
153 | ); |
154 | await tester.pumpAndSettle(); |
155 | expect(find.text('new page' ), findsOneWidget); |
156 | |
157 | nav.currentState!.maybePop(poppedResult); |
158 | await tester.pumpAndSettle(); |
159 | expect(receivedResult, poppedResult); |
160 | }, |
161 | variant: TargetPlatformVariant.all(), |
162 | ); |
163 | |
164 | testWidgets('toggling canPop on secondary route allows/prevents backs' , ( |
165 | WidgetTester tester, |
166 | ) async { |
167 | final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>(); |
168 | bool canPop = true; |
169 | late StateSetter setState; |
170 | late BuildContext homeContext; |
171 | late BuildContext oneContext; |
172 | late bool lastPopSuccess; |
173 | await tester.pumpWidget( |
174 | MaterialApp( |
175 | navigatorKey: nav, |
176 | initialRoute: '/' , |
177 | routes: <String, WidgetBuilder>{ |
178 | '/' : (BuildContext context) { |
179 | homeContext = context; |
180 | return Scaffold( |
181 | body: Center( |
182 | child: Column( |
183 | mainAxisAlignment: MainAxisAlignment.center, |
184 | children: <Widget>[ |
185 | const Text('Home Page' ), |
186 | TextButton( |
187 | onPressed: () { |
188 | Navigator.of(context).pushNamed('/one' ); |
189 | }, |
190 | child: const Text('Next' ), |
191 | ), |
192 | ], |
193 | ), |
194 | ), |
195 | ); |
196 | }, |
197 | '/one' : |
198 | (BuildContext context) => Scaffold( |
199 | body: StatefulBuilder( |
200 | builder: (BuildContext context, StateSetter stateSetter) { |
201 | oneContext = context; |
202 | setState = stateSetter; |
203 | return PopScope<Object?>( |
204 | canPop: canPop, |
205 | onPopInvokedWithResult: (bool didPop, Object? result) { |
206 | lastPopSuccess = didPop; |
207 | }, |
208 | child: const Center( |
209 | child: Column( |
210 | mainAxisAlignment: MainAxisAlignment.center, |
211 | children: <Widget>[Text('PopScope Page' )], |
212 | ), |
213 | ), |
214 | ); |
215 | }, |
216 | ), |
217 | ), |
218 | }, |
219 | ), |
220 | ); |
221 | |
222 | expect(find.text('Home Page' ), findsOneWidget); |
223 | expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); |
224 | |
225 | await tester.tap(find.text('Next' )); |
226 | await tester.pumpAndSettle(); |
227 | expect(find.text('PopScope Page' ), findsOneWidget); |
228 | expect(find.text('Home Page' ), findsNothing); |
229 | expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop); |
230 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
231 | expect(lastFrameworkHandlesBack, isTrue); |
232 | } |
233 | |
234 | // When canPop is true, can use pop to go back. |
235 | nav.currentState!.maybePop(); |
236 | await tester.pumpAndSettle(); |
237 | expect(lastPopSuccess, true); |
238 | expect(find.text('Home Page' ), findsOneWidget); |
239 | expect(find.text('PopScope Page' ), findsNothing); |
240 | expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); |
241 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
242 | expect(lastFrameworkHandlesBack, isFalse); |
243 | } |
244 | |
245 | await tester.tap(find.text('Next' )); |
246 | await tester.pumpAndSettle(); |
247 | expect(find.text('PopScope Page' ), findsOneWidget); |
248 | expect(find.text('Home Page' ), findsNothing); |
249 | expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop); |
250 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
251 | expect(lastFrameworkHandlesBack, isTrue); |
252 | } |
253 | |
254 | // When canPop is true, can use system back to go back. |
255 | await simulateSystemBack(); |
256 | await tester.pumpAndSettle(); |
257 | expect(lastPopSuccess, true); |
258 | expect(find.text('Home Page' ), findsOneWidget); |
259 | expect(find.text('PopScope Page' ), findsNothing); |
260 | expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); |
261 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
262 | expect(lastFrameworkHandlesBack, isFalse); |
263 | } |
264 | |
265 | await tester.tap(find.text('Next' )); |
266 | await tester.pumpAndSettle(); |
267 | expect(find.text('PopScope Page' ), findsOneWidget); |
268 | expect(find.text('Home Page' ), findsNothing); |
269 | expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop); |
270 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
271 | expect(lastFrameworkHandlesBack, isTrue); |
272 | } |
273 | |
274 | setState(() { |
275 | canPop = false; |
276 | }); |
277 | await tester.pump(); |
278 | |
279 | // When canPop is false, can't use pop to go back. |
280 | nav.currentState!.maybePop(); |
281 | await tester.pumpAndSettle(); |
282 | expect(lastPopSuccess, false); |
283 | expect(find.text('PopScope Page' ), findsOneWidget); |
284 | expect(find.text('Home Page' ), findsNothing); |
285 | expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.doNotPop); |
286 | |
287 | // When canPop is false, can't use system back to go back. |
288 | await simulateSystemBack(); |
289 | await tester.pumpAndSettle(); |
290 | expect(lastPopSuccess, false); |
291 | expect(find.text('PopScope Page' ), findsOneWidget); |
292 | expect(find.text('Home Page' ), findsNothing); |
293 | expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.doNotPop); |
294 | |
295 | // Toggle canPop back to true and back works again. |
296 | setState(() { |
297 | canPop = true; |
298 | }); |
299 | await tester.pump(); |
300 | |
301 | nav.currentState!.maybePop(); |
302 | await tester.pumpAndSettle(); |
303 | expect(lastPopSuccess, true); |
304 | expect(find.text('Home Page' ), findsOneWidget); |
305 | expect(find.text('PopScope Page' ), findsNothing); |
306 | expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); |
307 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
308 | expect(lastFrameworkHandlesBack, isFalse); |
309 | } |
310 | |
311 | await tester.tap(find.text('Next' )); |
312 | await tester.pumpAndSettle(); |
313 | expect(find.text('PopScope Page' ), findsOneWidget); |
314 | expect(find.text('Home Page' ), findsNothing); |
315 | expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop); |
316 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
317 | expect(lastFrameworkHandlesBack, isTrue); |
318 | } |
319 | |
320 | await simulateSystemBack(); |
321 | await tester.pumpAndSettle(); |
322 | expect(lastPopSuccess, true); |
323 | expect(find.text('Home Page' ), findsOneWidget); |
324 | expect(find.text('PopScope Page' ), findsNothing); |
325 | expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble); |
326 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
327 | expect(lastFrameworkHandlesBack, isFalse); |
328 | } |
329 | }, variant: TargetPlatformVariant.all()); |
330 | |
331 | testWidgets( |
332 | 'removing PopScope from the tree removes its effect on navigation' , |
333 | (WidgetTester tester) async { |
334 | bool usePopScope = true; |
335 | late StateSetter setState; |
336 | late BuildContext context; |
337 | await tester.pumpWidget( |
338 | MaterialApp( |
339 | initialRoute: '/' , |
340 | routes: <String, WidgetBuilder>{ |
341 | '/' : |
342 | (BuildContext buildContext) => Scaffold( |
343 | body: StatefulBuilder( |
344 | builder: (BuildContext buildContext, StateSetter stateSetter) { |
345 | context = buildContext; |
346 | setState = stateSetter; |
347 | const Widget child = Center( |
348 | child: Column( |
349 | mainAxisAlignment: MainAxisAlignment.center, |
350 | children: <Widget>[Text('Home/PopScope Page' )], |
351 | ), |
352 | ); |
353 | if (!usePopScope) { |
354 | return child; |
355 | } |
356 | return const PopScope<Object?>(canPop: false, child: child); |
357 | }, |
358 | ), |
359 | ), |
360 | }, |
361 | ), |
362 | ); |
363 | |
364 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
365 | expect(lastFrameworkHandlesBack, isTrue); |
366 | } |
367 | expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop); |
368 | |
369 | setState(() { |
370 | usePopScope = false; |
371 | }); |
372 | await tester.pump(); |
373 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
374 | expect(lastFrameworkHandlesBack, isFalse); |
375 | } |
376 | expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.bubble); |
377 | }, |
378 | variant: TargetPlatformVariant.all(), |
379 | ); |
380 | |
381 | testWidgets('identical PopScopes' , (WidgetTester tester) async { |
382 | bool usePopScope1 = true; |
383 | bool usePopScope2 = true; |
384 | late StateSetter setState; |
385 | late BuildContext context; |
386 | await tester.pumpWidget( |
387 | MaterialApp( |
388 | home: Scaffold( |
389 | body: StatefulBuilder( |
390 | builder: (BuildContext buildContext, StateSetter stateSetter) { |
391 | context = buildContext; |
392 | setState = stateSetter; |
393 | return Column( |
394 | children: <Widget>[ |
395 | if (usePopScope1) const PopScope<Object?>(canPop: false, child: Text('hello' )), |
396 | if (usePopScope2) const PopScope<Object?>(canPop: false, child: Text('hello' )), |
397 | ], |
398 | ); |
399 | }, |
400 | ), |
401 | ), |
402 | ), |
403 | ); |
404 | |
405 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
406 | expect(lastFrameworkHandlesBack, isTrue); |
407 | } |
408 | expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop); |
409 | |
410 | // Despite being in the widget tree twice, the ModalRoute has only ever |
411 | // registered one PopScopeInterface for it. Removing one makes it think that |
412 | // both have been removed. |
413 | setState(() { |
414 | usePopScope1 = false; |
415 | }); |
416 | await tester.pump(); |
417 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
418 | expect(lastFrameworkHandlesBack, isTrue); |
419 | } |
420 | expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop); |
421 | |
422 | setState(() { |
423 | usePopScope2 = false; |
424 | }); |
425 | await tester.pump(); |
426 | if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) { |
427 | expect(lastFrameworkHandlesBack, isFalse); |
428 | } |
429 | expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.bubble); |
430 | }, variant: TargetPlatformVariant.all()); |
431 | } |
432 | |