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 | void main() { |
11 | final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
12 | |
13 | Finder findPredictiveBackPageTransition() { |
14 | return find.descendant( |
15 | of: find.byType(MaterialApp), |
16 | matching: find.byWidgetPredicate( |
17 | (Widget w) => ' ${w.runtimeType}' == '_PredictiveBackPageTransition' , |
18 | ), |
19 | ); |
20 | } |
21 | |
22 | Finder findFallbackPageTransition() { |
23 | return find.descendant( |
24 | of: find.byType(MaterialApp), |
25 | matching: find.byWidgetPredicate((Widget w) => ' ${w.runtimeType}' == '_ZoomPageTransition' ), |
26 | ); |
27 | } |
28 | |
29 | testWidgets( |
30 | 'PredictiveBackPageTransitionsBuilder supports predictive back on Android' , |
31 | (WidgetTester tester) async { |
32 | final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
33 | '/' : |
34 | (BuildContext context) => Material( |
35 | child: TextButton( |
36 | child: const Text('push' ), |
37 | onPressed: () { |
38 | Navigator.of(context).pushNamed('/b' ); |
39 | }, |
40 | ), |
41 | ), |
42 | '/b' : (BuildContext context) => const Text('page b' ), |
43 | }; |
44 | |
45 | await tester.pumpWidget( |
46 | MaterialApp( |
47 | theme: ThemeData( |
48 | pageTransitionsTheme: PageTransitionsTheme( |
49 | builders: <TargetPlatform, PageTransitionsBuilder>{ |
50 | for (final TargetPlatform platform in TargetPlatform.values) |
51 | platform: const PredictiveBackPageTransitionsBuilder(), |
52 | }, |
53 | ), |
54 | ), |
55 | routes: routes, |
56 | ), |
57 | ); |
58 | |
59 | expect(find.text('push' ), findsOneWidget); |
60 | expect(find.text('page b' ), findsNothing); |
61 | expect(findPredictiveBackPageTransition(), findsNothing); |
62 | expect(findFallbackPageTransition(), findsOneWidget); |
63 | |
64 | await tester.tap(find.text('push' )); |
65 | await tester.pumpAndSettle(); |
66 | |
67 | expect(find.text('push' ), findsNothing); |
68 | expect(find.text('page b' ), findsOneWidget); |
69 | expect(findPredictiveBackPageTransition(), findsNothing); |
70 | expect(findFallbackPageTransition(), findsOneWidget); |
71 | |
72 | // Only Android supports backGesture channel methods. Other platforms will |
73 | // do nothing. |
74 | if (defaultTargetPlatform != TargetPlatform.android) { |
75 | return; |
76 | } |
77 | |
78 | // Start a system pop gesture, which will switch to using |
79 | // _PredictiveBackPageTransition for the page transition. |
80 | final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( |
81 | const MethodCall('startBackGesture' , <String, dynamic>{ |
82 | 'touchOffset' : <double>[5.0, 300.0], |
83 | 'progress' : 0.0, |
84 | 'swipeEdge' : 0, // left |
85 | }), |
86 | ); |
87 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
88 | 'flutter/backgesture' , |
89 | startMessage, |
90 | (ByteData? _) {}, |
91 | ); |
92 | await tester.pump(); |
93 | |
94 | expect(findPredictiveBackPageTransition(), findsOneWidget); |
95 | expect(findFallbackPageTransition(), findsNothing); |
96 | final Offset startPageBOffset = tester.getTopLeft(find.text('page b' )); |
97 | expect(startPageBOffset.dx, 0.0); |
98 | |
99 | // Drag the system back gesture far enough to commit. |
100 | final ByteData updateMessage = const StandardMethodCodec().encodeMethodCall( |
101 | const MethodCall('updateBackGestureProgress' , <String, dynamic>{ |
102 | 'x' : 100.0, |
103 | 'y' : 300.0, |
104 | 'progress' : 0.35, |
105 | 'swipeEdge' : 0, // left |
106 | }), |
107 | ); |
108 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
109 | 'flutter/backgesture' , |
110 | updateMessage, |
111 | (ByteData? _) {}, |
112 | ); |
113 | await tester.pumpAndSettle(); |
114 | |
115 | expect(findPredictiveBackPageTransition(), findsNWidgets(2)); |
116 | expect(findFallbackPageTransition(), findsNothing); |
117 | |
118 | final Offset updatePageBOffset = tester.getTopLeft(find.text('page b' )); |
119 | expect(updatePageBOffset.dx, greaterThan(startPageBOffset.dx)); |
120 | |
121 | // Commit the system back gesture. |
122 | final ByteData commitMessage = const StandardMethodCodec().encodeMethodCall( |
123 | const MethodCall('commitBackGesture' ), |
124 | ); |
125 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
126 | 'flutter/backgesture' , |
127 | commitMessage, |
128 | (ByteData? _) {}, |
129 | ); |
130 | await tester.pumpAndSettle(); |
131 | |
132 | expect(findPredictiveBackPageTransition(), findsNothing); |
133 | expect(findFallbackPageTransition(), findsOneWidget); |
134 | expect(find.text('push' ), findsOneWidget); |
135 | expect(find.text('page b' ), findsNothing); |
136 | }, |
137 | variant: TargetPlatformVariant.all(), |
138 | ); |
139 | |
140 | testWidgets( |
141 | 'PredictiveBackPageTransitionsBuilder supports canceling a predictive back gesture' , |
142 | (WidgetTester tester) async { |
143 | final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
144 | '/' : |
145 | (BuildContext context) => Material( |
146 | child: TextButton( |
147 | child: const Text('push' ), |
148 | onPressed: () { |
149 | Navigator.of(context).pushNamed('/b' ); |
150 | }, |
151 | ), |
152 | ), |
153 | '/b' : (BuildContext context) => const Text('page b' ), |
154 | }; |
155 | |
156 | await tester.pumpWidget( |
157 | MaterialApp( |
158 | theme: ThemeData( |
159 | pageTransitionsTheme: PageTransitionsTheme( |
160 | builders: <TargetPlatform, PageTransitionsBuilder>{ |
161 | for (final TargetPlatform platform in TargetPlatform.values) |
162 | platform: const PredictiveBackPageTransitionsBuilder(), |
163 | }, |
164 | ), |
165 | ), |
166 | routes: routes, |
167 | ), |
168 | ); |
169 | |
170 | expect(find.text('push' ), findsOneWidget); |
171 | expect(find.text('page b' ), findsNothing); |
172 | expect(findPredictiveBackPageTransition(), findsNothing); |
173 | expect(findFallbackPageTransition(), findsOneWidget); |
174 | |
175 | await tester.tap(find.text('push' )); |
176 | await tester.pumpAndSettle(); |
177 | |
178 | expect(find.text('push' ), findsNothing); |
179 | expect(find.text('page b' ), findsOneWidget); |
180 | expect(findPredictiveBackPageTransition(), findsNothing); |
181 | expect(findFallbackPageTransition(), findsOneWidget); |
182 | |
183 | // Only Android supports backGesture channel methods. Other platforms will |
184 | // do nothing. |
185 | if (defaultTargetPlatform != TargetPlatform.android) { |
186 | return; |
187 | } |
188 | |
189 | // Start a system pop gesture, which will switch to using |
190 | // _PredictiveBackPageTransition for the page transition. |
191 | final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( |
192 | const MethodCall('startBackGesture' , <String, dynamic>{ |
193 | 'touchOffset' : <double>[5.0, 300.0], |
194 | 'progress' : 0.0, |
195 | 'swipeEdge' : 0, // left |
196 | }), |
197 | ); |
198 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
199 | 'flutter/backgesture' , |
200 | startMessage, |
201 | (ByteData? _) {}, |
202 | ); |
203 | await tester.pump(); |
204 | |
205 | expect(findPredictiveBackPageTransition(), findsOneWidget); |
206 | expect(findFallbackPageTransition(), findsNothing); |
207 | final Offset startPageBOffset = tester.getTopLeft(find.text('page b' )); |
208 | expect(startPageBOffset.dx, 0.0); |
209 | |
210 | // Drag the system back gesture. |
211 | final ByteData updateMessage = const StandardMethodCodec().encodeMethodCall( |
212 | const MethodCall('updateBackGestureProgress' , <String, dynamic>{ |
213 | 'touchOffset' : <double>[100.0, 300.0], |
214 | 'progress' : 0.35, |
215 | 'swipeEdge' : 0, // left |
216 | }), |
217 | ); |
218 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
219 | 'flutter/backgesture' , |
220 | updateMessage, |
221 | (ByteData? _) {}, |
222 | ); |
223 | await tester.pumpAndSettle(); |
224 | |
225 | expect(findPredictiveBackPageTransition(), findsNWidgets(2)); |
226 | expect(findFallbackPageTransition(), findsNothing); |
227 | |
228 | final Offset updatePageBOffset = tester.getTopLeft(find.text('page b' )); |
229 | expect(updatePageBOffset.dx, greaterThan(startPageBOffset.dx)); |
230 | |
231 | // Cancel the system back gesture. |
232 | final ByteData commitMessage = const StandardMethodCodec().encodeMethodCall( |
233 | const MethodCall('cancelBackGesture' ), |
234 | ); |
235 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
236 | 'flutter/backgesture' , |
237 | commitMessage, |
238 | (ByteData? _) {}, |
239 | ); |
240 | await tester.pumpAndSettle(); |
241 | |
242 | expect(find.text('push' ), findsNothing); |
243 | expect(find.text('page b' ), findsOneWidget); |
244 | expect(findPredictiveBackPageTransition(), findsNothing); |
245 | expect(findFallbackPageTransition(), findsOneWidget); |
246 | }, |
247 | variant: TargetPlatformVariant.all(), |
248 | ); |
249 | |
250 | testWidgets( |
251 | 'if multiple PredictiveBackPageTransitionBuilder observers, only one gets called for a given back gesture' , |
252 | (WidgetTester tester) async { |
253 | bool includingNestedNavigator = false; |
254 | late StateSetter setState; |
255 | final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ |
256 | '/' : |
257 | (BuildContext context) => Material( |
258 | child: TextButton( |
259 | child: const Text('push' ), |
260 | onPressed: () { |
261 | Navigator.of(context).pushNamed('/b' ); |
262 | }, |
263 | ), |
264 | ), |
265 | '/b' : |
266 | (BuildContext context) => Column( |
267 | crossAxisAlignment: CrossAxisAlignment.start, |
268 | children: <Widget>[ |
269 | const Text('page b' ), |
270 | StatefulBuilder( |
271 | builder: (BuildContext context, StateSetter localSetState) { |
272 | setState = localSetState; |
273 | if (!includingNestedNavigator) { |
274 | return const SizedBox.shrink(); |
275 | } |
276 | return Navigator( |
277 | initialRoute: 'b/nested' , |
278 | onGenerateRoute: (RouteSettings settings) { |
279 | WidgetBuilder builder; |
280 | switch (settings.name) { |
281 | case 'b/nested' : |
282 | builder = |
283 | (BuildContext context) => Material( |
284 | child: Theme( |
285 | data: ThemeData( |
286 | pageTransitionsTheme: PageTransitionsTheme( |
287 | builders: <TargetPlatform, PageTransitionsBuilder>{ |
288 | for (final TargetPlatform platform |
289 | in TargetPlatform.values) |
290 | platform: const PredictiveBackPageTransitionsBuilder(), |
291 | }, |
292 | ), |
293 | ), |
294 | child: const Column( |
295 | children: <Widget>[Text('Nested route inside of page b' )], |
296 | ), |
297 | ), |
298 | ); |
299 | default: |
300 | throw Exception('Invalid route: ${settings.name}' ); |
301 | } |
302 | return MaterialPageRoute<void>(builder: builder, settings: settings); |
303 | }, |
304 | ); |
305 | }, |
306 | ), |
307 | ], |
308 | ), |
309 | }; |
310 | |
311 | await tester.pumpWidget( |
312 | MaterialApp( |
313 | theme: ThemeData( |
314 | pageTransitionsTheme: PageTransitionsTheme( |
315 | builders: <TargetPlatform, PageTransitionsBuilder>{ |
316 | for (final TargetPlatform platform in TargetPlatform.values) |
317 | platform: const PredictiveBackPageTransitionsBuilder(), |
318 | }, |
319 | ), |
320 | ), |
321 | routes: routes, |
322 | ), |
323 | ); |
324 | |
325 | expect(find.text('push' ), findsOneWidget); |
326 | expect(find.text('page b' ), findsNothing); |
327 | expect(find.text('Nested route inside of page b' ), findsNothing); |
328 | expect(findPredictiveBackPageTransition(), findsNothing); |
329 | expect(findFallbackPageTransition(), findsOneWidget); |
330 | |
331 | await tester.tap(find.text('push' )); |
332 | await tester.pumpAndSettle(); |
333 | |
334 | expect(find.text('push' ), findsNothing); |
335 | expect(find.text('page b' ), findsOneWidget); |
336 | expect(find.text('Nested route inside of page b' ), findsNothing); |
337 | expect(findPredictiveBackPageTransition(), findsNothing); |
338 | expect(findFallbackPageTransition(), findsOneWidget); |
339 | |
340 | // Only Android supports backGesture channel methods. Other platforms will |
341 | // do nothing. |
342 | if (defaultTargetPlatform != TargetPlatform.android) { |
343 | return; |
344 | } |
345 | |
346 | // Start a system pop gesture, which will switch to using |
347 | // _PredictiveBackPageTransition for the page transition. |
348 | final ByteData startMessage = const StandardMethodCodec().encodeMethodCall( |
349 | const MethodCall('startBackGesture' , <String, dynamic>{ |
350 | 'touchOffset' : <double>[5.0, 300.0], |
351 | 'progress' : 0.0, |
352 | 'swipeEdge' : 0, // left |
353 | }), |
354 | ); |
355 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
356 | 'flutter/backgesture' , |
357 | startMessage, |
358 | (ByteData? _) {}, |
359 | ); |
360 | await tester.pump(); |
361 | |
362 | expect(findPredictiveBackPageTransition(), findsOneWidget); |
363 | expect(findFallbackPageTransition(), findsNothing); |
364 | final Offset startPageBOffset = tester.getTopLeft(find.text('page b' )); |
365 | expect(startPageBOffset.dx, 0.0); |
366 | |
367 | // Drag the system back gesture. |
368 | final ByteData updateMessage = const StandardMethodCodec().encodeMethodCall( |
369 | const MethodCall('updateBackGestureProgress' , <String, dynamic>{ |
370 | 'touchOffset' : <double>[100.0, 300.0], |
371 | 'progress' : 0.3, |
372 | 'swipeEdge' : 0, // left |
373 | }), |
374 | ); |
375 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
376 | 'flutter/backgesture' , |
377 | updateMessage, |
378 | (ByteData? _) {}, |
379 | ); |
380 | await tester.pumpAndSettle(); |
381 | |
382 | expect(findPredictiveBackPageTransition(), findsNWidgets(2)); |
383 | expect(findFallbackPageTransition(), findsNothing); |
384 | |
385 | final Offset updatePageBOffset = tester.getTopLeft(find.text('page b' )); |
386 | expect(updatePageBOffset.dx, greaterThan(startPageBOffset.dx)); |
387 | |
388 | // In the middle of the system back gesture here, add a nested Navigator |
389 | // that includes a new predictive back gesture observer. |
390 | setState(() { |
391 | includingNestedNavigator = true; |
392 | }); |
393 | await tester.pumpAndSettle(); |
394 | expect(find.text('push' ), findsOneWidget); |
395 | expect(find.text('page b' ), findsOneWidget); |
396 | expect(find.text('Nested route inside of page b' ), findsOneWidget); |
397 | |
398 | // Send another drag gesture, and ensure that the original observer still |
399 | // gets it. |
400 | final ByteData updateMessage2 = const StandardMethodCodec().encodeMethodCall( |
401 | const MethodCall('updateBackGestureProgress' , <String, dynamic>{ |
402 | 'touchOffset' : <double>[110.0, 300.0], |
403 | 'progress' : 0.35, |
404 | 'swipeEdge' : 0, // left |
405 | }), |
406 | ); |
407 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
408 | 'flutter/backgesture' , |
409 | updateMessage2, |
410 | (ByteData? _) {}, |
411 | ); |
412 | await tester.pumpAndSettle(); |
413 | |
414 | expect(findPredictiveBackPageTransition(), findsNWidgets(2)); |
415 | // Despite using a PredictiveBackPageTransitions, the new route has not |
416 | // received a start event, so it is still using the fallback transition. |
417 | expect(findFallbackPageTransition(), findsOneWidget); |
418 | |
419 | final Offset update2PageBOffset = tester.getTopLeft(find.text('page b' )); |
420 | expect(update2PageBOffset.dx, greaterThan(updatePageBOffset.dx)); |
421 | |
422 | // Commit the system back gesture, and the original observer is able to |
423 | // handle the back without interference. |
424 | final ByteData commitMessage = const StandardMethodCodec().encodeMethodCall( |
425 | const MethodCall('commitBackGesture' ), |
426 | ); |
427 | await binding.defaultBinaryMessenger.handlePlatformMessage( |
428 | 'flutter/backgesture' , |
429 | commitMessage, |
430 | (ByteData? _) {}, |
431 | ); |
432 | await tester.pumpAndSettle(); |
433 | |
434 | expect(findPredictiveBackPageTransition(), findsNothing); |
435 | expect(findFallbackPageTransition(), findsOneWidget); |
436 | expect(find.text('push' ), findsOneWidget); |
437 | expect(find.text('page b' ), findsNothing); |
438 | expect(find.text('Nested route inside of page b' ), findsNothing); |
439 | }, |
440 | variant: TargetPlatformVariant.all(), |
441 | ); |
442 | } |
443 | |