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 | class TestIntent extends Intent { |
11 | const TestIntent(); |
12 | } |
13 | |
14 | class TestAction extends Action<Intent> { |
15 | TestAction(); |
16 | |
17 | int calls = 0; |
18 | |
19 | @override |
20 | void invoke(Intent intent) { |
21 | calls += 1; |
22 | } |
23 | } |
24 | |
25 | void main() { |
26 | testWidgets('WidgetsApp with builder only', (WidgetTester tester) async { |
27 | final GlobalKey key = GlobalKey(); |
28 | await tester.pumpWidget( |
29 | WidgetsApp( |
30 | key: key, |
31 | builder: (BuildContext context, Widget? child) { |
32 | return const Placeholder(); |
33 | }, |
34 | color: const Color(0xFF123456), |
35 | ), |
36 | ); |
37 | expect(find.byKey(key), findsOneWidget); |
38 | }); |
39 | |
40 | testWidgets('WidgetsApp default key bindings', (WidgetTester tester) async { |
41 | bool? checked = false; |
42 | final GlobalKey key = GlobalKey(); |
43 | await tester.pumpWidget( |
44 | WidgetsApp( |
45 | key: key, |
46 | builder: (BuildContext context, Widget? child) { |
47 | return Material( |
48 | child: Checkbox( |
49 | value: checked, |
50 | autofocus: true, |
51 | onChanged: (bool? value) { |
52 | checked = value; |
53 | }, |
54 | ), |
55 | ); |
56 | }, |
57 | color: const Color(0xFF123456), |
58 | ), |
59 | ); |
60 | await tester.pump(); // Wait for focus to take effect. |
61 | await tester.sendKeyEvent(LogicalKeyboardKey.space); |
62 | await tester.pumpAndSettle(); |
63 | // Default key mapping worked. |
64 | expect(checked, isTrue); |
65 | }); |
66 | |
67 | testWidgets('WidgetsApp can override default key bindings', (WidgetTester tester) async { |
68 | final TestAction action = TestAction(); |
69 | bool? checked = false; |
70 | final GlobalKey key = GlobalKey(); |
71 | await tester.pumpWidget( |
72 | WidgetsApp( |
73 | key: key, |
74 | actions: <Type, Action<Intent>>{ |
75 | TestIntent: action, |
76 | }, |
77 | shortcuts: const <ShortcutActivator, Intent> { |
78 | SingleActivator(LogicalKeyboardKey.space): TestIntent(), |
79 | }, |
80 | builder: (BuildContext context, Widget? child) { |
81 | return Material( |
82 | child: Checkbox( |
83 | value: checked, |
84 | autofocus: true, |
85 | onChanged: (bool? value) { |
86 | checked = value; |
87 | }, |
88 | ), |
89 | ); |
90 | }, |
91 | color: const Color(0xFF123456), |
92 | ), |
93 | ); |
94 | await tester.pump(); |
95 | |
96 | await tester.sendKeyEvent(LogicalKeyboardKey.space); |
97 | await tester.pumpAndSettle(); |
98 | // Default key mapping was not invoked. |
99 | expect(checked, isFalse); |
100 | // Overridden mapping was invoked. |
101 | expect(action.calls, equals(1)); |
102 | }); |
103 | |
104 | testWidgets('WidgetsApp default activation key mappings work', (WidgetTester tester) async { |
105 | bool? checked = false; |
106 | |
107 | await tester.pumpWidget( |
108 | WidgetsApp( |
109 | builder: (BuildContext context, Widget? child) { |
110 | return Material( |
111 | child: Checkbox( |
112 | value: checked, |
113 | autofocus: true, |
114 | onChanged: (bool? value) { |
115 | checked = value; |
116 | }, |
117 | ), |
118 | ); |
119 | }, |
120 | color: const Color(0xFF123456), |
121 | ), |
122 | ); |
123 | await tester.pump(); |
124 | |
125 | // Test three default buttons for the activation action. |
126 | await tester.sendKeyEvent(LogicalKeyboardKey.space); |
127 | await tester.pumpAndSettle(); |
128 | expect(checked, isTrue); |
129 | |
130 | // Only space is used as an activation key on web. |
131 | if (kIsWeb) { |
132 | return; |
133 | } |
134 | |
135 | checked = false; |
136 | await tester.sendKeyEvent(LogicalKeyboardKey.enter); |
137 | await tester.pumpAndSettle(); |
138 | expect(checked, isTrue); |
139 | |
140 | checked = false; |
141 | await tester.sendKeyEvent(LogicalKeyboardKey.numpadEnter); |
142 | await tester.pumpAndSettle(); |
143 | expect(checked, isTrue); |
144 | |
145 | checked = false; |
146 | await tester.sendKeyEvent(LogicalKeyboardKey.gameButtonA); |
147 | await tester.pumpAndSettle(); |
148 | expect(checked, isTrue); |
149 | |
150 | checked = false; |
151 | await tester.sendKeyEvent(LogicalKeyboardKey.select); |
152 | await tester.pumpAndSettle(); |
153 | expect(checked, isTrue); |
154 | }, variant: KeySimulatorTransitModeVariant.all()); |
155 | |
156 | testWidgets('Title is not created if title is not passed and kIsweb', (WidgetTester tester) async { |
157 | await tester.pumpWidget( |
158 | WidgetsApp( |
159 | color: const Color(0xFF123456), |
160 | builder: (BuildContext context, Widget? child) => Container(), |
161 | ), |
162 | ); |
163 | |
164 | expect(find.byType(Title), kIsWeb ? findsNothing : findsOneWidget); |
165 | }); |
166 | |
167 | group('error control test', () { |
168 | Future<void> expectFlutterError({ |
169 | required GlobalKey<NavigatorState> key, |
170 | required Widget widget, |
171 | required WidgetTester tester, |
172 | required String errorMessage, |
173 | }) async { |
174 | await tester.pumpWidget(widget); |
175 | late FlutterError error; |
176 | try { |
177 | key.currentState!.pushNamed('/path'); |
178 | } on FlutterError catch (e) { |
179 | error = e; |
180 | } finally { |
181 | expect(error, isNotNull); |
182 | expect(error, isFlutterError); |
183 | expect(error.toStringDeep(), errorMessage); |
184 | } |
185 | } |
186 | |
187 | testWidgets('push unknown route when onUnknownRoute is null', (WidgetTester tester) async { |
188 | final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); |
189 | expectFlutterError( |
190 | key: key, |
191 | tester: tester, |
192 | widget: MaterialApp( |
193 | navigatorKey: key, |
194 | home: Container(), |
195 | onGenerateRoute: (_) => null, |
196 | ), |
197 | errorMessage: |
198 | 'FlutterError\n' |
199 | ' Could not find a generator for route RouteSettings("/path", null)\n' |
200 | ' in the _WidgetsAppState.\n' |
201 | ' Make sure your root app widget has provided a way to generate\n' |
202 | ' this route.\n' |
203 | ' Generators for routes are searched for in the following order:\n' |
204 | ' 1. For the "/" route, the "home" property, if non-null, is used.\n' |
205 | ' 2. Otherwise, the "routes" table is used, if it has an entry for\n' |
206 | ' the route.\n' |
207 | ' 3. Otherwise, onGenerateRoute is called. It should return a\n' |
208 | ' non-null value for any valid route not handled by "home" and\n' |
209 | ' "routes".\n' |
210 | ' 4. Finally if all else fails onUnknownRoute is called.\n' |
211 | ' Unfortunately, onUnknownRoute was not set.\n', |
212 | ); |
213 | }); |
214 | |
215 | testWidgets('push unknown route when onUnknownRoute returns null', (WidgetTester tester) async { |
216 | final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); |
217 | expectFlutterError( |
218 | key: key, |
219 | tester: tester, |
220 | widget: MaterialApp( |
221 | navigatorKey: key, |
222 | home: Container(), |
223 | onGenerateRoute: (_) => null, |
224 | onUnknownRoute: (_) => null, |
225 | ), |
226 | errorMessage: |
227 | 'FlutterError\n' |
228 | ' The onUnknownRoute callback returned null.\n' |
229 | ' When the _WidgetsAppState requested the route\n' |
230 | ' RouteSettings("/path", null) from its onUnknownRoute callback,\n' |
231 | ' the callback returned null. Such callbacks must never return\n' |
232 | ' null.\n', |
233 | ); |
234 | }); |
235 | }); |
236 | |
237 | testWidgets('WidgetsApp can customize initial routes', (WidgetTester tester) async { |
238 | final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); |
239 | await tester.pumpWidget( |
240 | WidgetsApp( |
241 | navigatorKey: navigatorKey, |
242 | onGenerateInitialRoutes: (String initialRoute) { |
243 | expect(initialRoute, '/abc'); |
244 | return <Route<void>>[ |
245 | PageRouteBuilder<void>( |
246 | pageBuilder: ( |
247 | BuildContext context, |
248 | Animation<double> animation, |
249 | Animation<double> secondaryAnimation, |
250 | ) { |
251 | return const Text('non-regular page one'); |
252 | }, |
253 | ), |
254 | PageRouteBuilder<void>( |
255 | pageBuilder: ( |
256 | BuildContext context, |
257 | Animation<double> animation, |
258 | Animation<double> secondaryAnimation, |
259 | ) { |
260 | return const Text('non-regular page two'); |
261 | }, |
262 | ), |
263 | ]; |
264 | }, |
265 | initialRoute: '/abc', |
266 | onGenerateRoute: (RouteSettings settings) { |
267 | return PageRouteBuilder<void>( |
268 | pageBuilder: ( |
269 | BuildContext context, |
270 | Animation<double> animation, |
271 | Animation<double> secondaryAnimation, |
272 | ) { |
273 | return const Text('regular page'); |
274 | }, |
275 | ); |
276 | }, |
277 | color: const Color(0xFF123456), |
278 | ), |
279 | ); |
280 | expect(find.text('non-regular page two'), findsOneWidget); |
281 | expect(find.text('non-regular page one'), findsNothing); |
282 | expect(find.text('regular page'), findsNothing); |
283 | navigatorKey.currentState!.pop(); |
284 | await tester.pumpAndSettle(); |
285 | expect(find.text('non-regular page two'), findsNothing); |
286 | expect(find.text('non-regular page one'), findsOneWidget); |
287 | expect(find.text('regular page'), findsNothing); |
288 | }); |
289 | |
290 | testWidgets('WidgetsApp.router works', (WidgetTester tester) async { |
291 | final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( |
292 | initialRouteInformation: RouteInformation( |
293 | uri: Uri.parse('initial'), |
294 | ), |
295 | ); |
296 | addTearDown(provider.dispose); |
297 | final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( |
298 | builder: (BuildContext context, RouteInformation information) { |
299 | return Text(information.uri.toString()); |
300 | }, |
301 | onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { |
302 | delegate.routeInformation = RouteInformation( |
303 | uri: Uri.parse('popped'), |
304 | ); |
305 | return route.didPop(result); |
306 | }, |
307 | ); |
308 | addTearDown(delegate.dispose); |
309 | await tester.pumpWidget(WidgetsApp.router( |
310 | routeInformationProvider: provider, |
311 | routeInformationParser: SimpleRouteInformationParser(), |
312 | routerDelegate: delegate, |
313 | color: const Color(0xFF123456), |
314 | )); |
315 | expect(find.text('initial'), findsOneWidget); |
316 | |
317 | // Simulate android back button intent. |
318 | final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); |
319 | await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); |
320 | await tester.pumpAndSettle(); |
321 | expect(find.text('popped'), findsOneWidget); |
322 | }); |
323 | |
324 | testWidgets('WidgetsApp.router route information parser is optional', (WidgetTester tester) async { |
325 | final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( |
326 | builder: (BuildContext context, RouteInformation information) { |
327 | return Text(information.uri.toString()); |
328 | }, |
329 | onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { |
330 | delegate.routeInformation = RouteInformation( |
331 | uri: Uri.parse('popped'), |
332 | ); |
333 | return route.didPop(result); |
334 | }, |
335 | ); |
336 | addTearDown(delegate.dispose); |
337 | delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); |
338 | await tester.pumpWidget(WidgetsApp.router( |
339 | routerDelegate: delegate, |
340 | color: const Color(0xFF123456), |
341 | )); |
342 | expect(find.text('initial'), findsOneWidget); |
343 | |
344 | // Simulate android back button intent. |
345 | final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); |
346 | await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); |
347 | await tester.pumpAndSettle(); |
348 | expect(find.text('popped'), findsOneWidget); |
349 | }); |
350 | |
351 | testWidgets('WidgetsApp.router throw if route information provider is provided but no route information parser', (WidgetTester tester) async { |
352 | final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( |
353 | builder: (BuildContext context, RouteInformation information) { |
354 | return Text(information.uri.toString()); |
355 | }, |
356 | onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { |
357 | delegate.routeInformation = RouteInformation( |
358 | uri: Uri.parse('popped'), |
359 | ); |
360 | return route.didPop(result); |
361 | }, |
362 | ); |
363 | addTearDown(delegate.dispose); |
364 | delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); |
365 | final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( |
366 | initialRouteInformation: RouteInformation( |
367 | uri: Uri.parse('initial'), |
368 | ), |
369 | ); |
370 | addTearDown(provider.dispose); |
371 | await expectLater(() async { |
372 | await tester.pumpWidget(WidgetsApp.router( |
373 | routeInformationProvider: provider, |
374 | routerDelegate: delegate, |
375 | color: const Color(0xFF123456), |
376 | )); |
377 | }, throwsAssertionError); |
378 | }); |
379 | |
380 | testWidgets('WidgetsApp.router throw if route configuration is provided along with other delegate', (WidgetTester tester) async { |
381 | final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( |
382 | builder: (BuildContext context, RouteInformation information) { |
383 | return Text(information.uri.toString()); |
384 | }, |
385 | onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { |
386 | delegate.routeInformation = RouteInformation( |
387 | uri: Uri.parse('popped'), |
388 | ); |
389 | return route.didPop(result); |
390 | }, |
391 | ); |
392 | addTearDown(delegate.dispose); |
393 | delegate.routeInformation = RouteInformation(uri: Uri.parse('initial')); |
394 | final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>(routerDelegate: delegate); |
395 | await expectLater(() async { |
396 | await tester.pumpWidget(WidgetsApp.router( |
397 | routerDelegate: delegate, |
398 | routerConfig: routerConfig, |
399 | color: const Color(0xFF123456), |
400 | )); |
401 | }, throwsAssertionError); |
402 | }); |
403 | |
404 | testWidgets('WidgetsApp.router router config works', (WidgetTester tester) async { |
405 | final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( |
406 | builder: (BuildContext context, RouteInformation information) { |
407 | return Text(information.uri.toString()); |
408 | }, |
409 | onPopPage: (Route<void> route, void result, SimpleNavigatorRouterDelegate delegate) { |
410 | delegate.routeInformation = RouteInformation( |
411 | uri: Uri.parse('popped'), |
412 | ); |
413 | return route.didPop(result); |
414 | }, |
415 | ); |
416 | addTearDown(delegate.dispose); |
417 | final PlatformRouteInformationProvider provider = PlatformRouteInformationProvider( |
418 | initialRouteInformation: RouteInformation( |
419 | uri: Uri.parse('initial'), |
420 | ), |
421 | ); |
422 | addTearDown(provider.dispose); |
423 | final RouterConfig<RouteInformation> routerConfig = RouterConfig<RouteInformation>( |
424 | routeInformationProvider: provider, |
425 | routeInformationParser: SimpleRouteInformationParser(), |
426 | routerDelegate: delegate, |
427 | backButtonDispatcher: RootBackButtonDispatcher() |
428 | ); |
429 | await tester.pumpWidget(WidgetsApp.router( |
430 | routerConfig: routerConfig, |
431 | color: const Color(0xFF123456), |
432 | )); |
433 | expect(find.text('initial'), findsOneWidget); |
434 | |
435 | // Simulate android back button intent. |
436 | final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('popRoute')); |
437 | await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { }); |
438 | await tester.pumpAndSettle(); |
439 | expect(find.text('popped'), findsOneWidget); |
440 | }); |
441 | |
442 | testWidgets('WidgetsApp.router has correct default', (WidgetTester tester) async { |
443 | final SimpleNavigatorRouterDelegate delegate = SimpleNavigatorRouterDelegate( |
444 | builder: (BuildContext context, RouteInformation information) { |
445 | return Text(information.uri.toString()); |
446 | }, |
447 | onPopPage: (Route<Object?> route, Object? result, SimpleNavigatorRouterDelegate delegate) => true, |
448 | ); |
449 | addTearDown(delegate.dispose); |
450 | await tester.pumpWidget(WidgetsApp.router( |
451 | routeInformationParser: SimpleRouteInformationParser(), |
452 | routerDelegate: delegate, |
453 | color: const Color(0xFF123456), |
454 | )); |
455 | expect(find.text('/'), findsOneWidget); |
456 | }); |
457 | |
458 | testWidgets('WidgetsApp has correct default ScrollBehavior', (WidgetTester tester) async { |
459 | late BuildContext capturedContext; |
460 | await tester.pumpWidget( |
461 | WidgetsApp( |
462 | builder: (BuildContext context, Widget? child) { |
463 | capturedContext = context; |
464 | return const Placeholder(); |
465 | }, |
466 | color: const Color(0xFF123456), |
467 | ), |
468 | ); |
469 | expect(ScrollConfiguration.of(capturedContext).runtimeType, ScrollBehavior); |
470 | }); |
471 | |
472 | test('basicLocaleListResolution', () { |
473 | // Matches exactly for language code. |
474 | expect( |
475 | basicLocaleListResolution( |
476 | <Locale>[ |
477 | const Locale('zh'), |
478 | const Locale('un'), |
479 | const Locale('en'), |
480 | ], |
481 | <Locale>[ |
482 | const Locale('en'), |
483 | ], |
484 | ), |
485 | const Locale('en'), |
486 | ); |
487 | |
488 | // Matches exactly for language code and country code. |
489 | expect( |
490 | basicLocaleListResolution( |
491 | <Locale>[ |
492 | const Locale('en'), |
493 | const Locale('en', 'US'), |
494 | ], |
495 | <Locale>[ |
496 | const Locale('en', 'US'), |
497 | ], |
498 | ), |
499 | const Locale('en', 'US'), |
500 | ); |
501 | |
502 | // Matches language+script over language+country |
503 | expect( |
504 | basicLocaleListResolution( |
505 | <Locale>[ |
506 | const Locale.fromSubtags( |
507 | languageCode: 'zh', |
508 | scriptCode: 'Hant', |
509 | countryCode: 'HK', |
510 | ), |
511 | ], |
512 | <Locale>[ |
513 | const Locale.fromSubtags( |
514 | languageCode: 'zh', |
515 | countryCode: 'HK', |
516 | ), |
517 | const Locale.fromSubtags( |
518 | languageCode: 'zh', |
519 | scriptCode: 'Hant', |
520 | ), |
521 | ], |
522 | ), |
523 | const Locale.fromSubtags( |
524 | languageCode: 'zh', |
525 | scriptCode: 'Hant', |
526 | ), |
527 | ); |
528 | |
529 | // Matches exactly for language code, script code and country code. |
530 | expect( |
531 | basicLocaleListResolution( |
532 | <Locale>[ |
533 | const Locale.fromSubtags( |
534 | languageCode: 'zh', |
535 | ), |
536 | const Locale.fromSubtags( |
537 | languageCode: 'zh', |
538 | scriptCode: 'Hant', |
539 | countryCode: 'TW', |
540 | ), |
541 | ], |
542 | <Locale>[ |
543 | const Locale.fromSubtags( |
544 | languageCode: 'zh', |
545 | scriptCode: 'Hant', |
546 | countryCode: 'TW', |
547 | ), |
548 | ], |
549 | ), |
550 | const Locale.fromSubtags( |
551 | languageCode: 'zh', |
552 | scriptCode: 'Hant', |
553 | countryCode: 'TW', |
554 | ), |
555 | ); |
556 | |
557 | // Selects for country code if the language code is not found in the |
558 | // preferred locales list. |
559 | expect( |
560 | basicLocaleListResolution( |
561 | <Locale>[ |
562 | const Locale.fromSubtags( |
563 | languageCode: 'en', |
564 | ), |
565 | const Locale.fromSubtags( |
566 | languageCode: 'ar', |
567 | countryCode: 'tn', |
568 | ), |
569 | ], |
570 | <Locale>[ |
571 | const Locale.fromSubtags( |
572 | languageCode: 'fr', |
573 | countryCode: 'tn', |
574 | ), |
575 | ], |
576 | ), |
577 | const Locale.fromSubtags( |
578 | languageCode: 'fr', |
579 | countryCode: 'tn', |
580 | ), |
581 | ); |
582 | |
583 | // Selects first (default) locale when no match at all is found. |
584 | expect( |
585 | basicLocaleListResolution( |
586 | <Locale>[ |
587 | const Locale('tn'), |
588 | ], |
589 | <Locale>[ |
590 | const Locale('zh'), |
591 | const Locale('un'), |
592 | const Locale('en'), |
593 | ], |
594 | ), |
595 | const Locale('zh'), |
596 | ); |
597 | }); |
598 | |
599 | testWidgets("WidgetsApp reports an exception if the selected locale isn't supported", (WidgetTester tester) async { |
600 | late final List<Locale>? localesArg; |
601 | late final Iterable<Locale> supportedLocalesArg; |
602 | await tester.pumpWidget( |
603 | MaterialApp( // This uses a MaterialApp because it introduces some actual localizations. |
604 | localeListResolutionCallback: (List<Locale>? locales, Iterable<Locale> supportedLocales) { |
605 | localesArg = locales; |
606 | supportedLocalesArg = supportedLocales; |
607 | return const Locale('C_UTF-8'); |
608 | }, |
609 | builder: (BuildContext context, Widget? child) => const Placeholder(), |
610 | color: const Color(0xFF000000), |
611 | ), |
612 | ); |
613 | if (!kIsWeb) { |
614 | // On web, `flutter test` does not guarantee a particular locale, but |
615 | // when using `flutter_tester`, we guarantee that it's en-US, zh-CN. |
616 | // https://github.com/flutter/flutter/issues/93290 |
617 | expect(localesArg, const <Locale>[Locale('en', 'US'), Locale( 'zh', 'CN')]); |
618 | } |
619 | expect(supportedLocalesArg, const <Locale>[Locale('en', 'US')]); |
620 | expect(tester.takeException(), "Warning: This application's locale, C_UTF-8, is not supported by all of its localization delegates."); |
621 | }); |
622 | |
623 | testWidgets("WidgetsApp doesn't have dependency on MediaQuery", (WidgetTester tester) async { |
624 | int routeBuildCount = 0; |
625 | |
626 | final Widget widget = WidgetsApp( |
627 | color: const Color.fromARGB(255, 255, 255, 255), |
628 | onGenerateRoute: (_) { |
629 | return PageRouteBuilder<void>(pageBuilder: (_, __, ___) { |
630 | routeBuildCount++; |
631 | return const Placeholder(); |
632 | }); |
633 | }, |
634 | ); |
635 | |
636 | await tester.pumpWidget( |
637 | MediaQuery(data: const MediaQueryData(textScaler: TextScaler.linear(10)), child: widget), |
638 | ); |
639 | |
640 | expect(routeBuildCount, equals(1)); |
641 | |
642 | await tester.pumpWidget( |
643 | MediaQuery(data: const MediaQueryData(textScaler: TextScaler.linear(20)), child: widget), |
644 | ); |
645 | |
646 | expect(routeBuildCount, equals(1)); |
647 | }); |
648 | |
649 | testWidgets('WidgetsApp provides meta based shortcuts for iOS and macOS', (WidgetTester tester) async { |
650 | final FocusNode focusNode = FocusNode(); |
651 | addTearDown(focusNode.dispose); |
652 | |
653 | final SelectAllSpy selectAllSpy = SelectAllSpy(); |
654 | final CopySpy copySpy = CopySpy(); |
655 | final PasteSpy pasteSpy = PasteSpy(); |
656 | final Map<Type, Action<Intent>> actions = <Type, Action<Intent>>{ |
657 | // Copy Paste |
658 | SelectAllTextIntent: selectAllSpy, |
659 | CopySelectionTextIntent: copySpy, |
660 | PasteTextIntent: pasteSpy, |
661 | }; |
662 | await tester.pumpWidget( |
663 | WidgetsApp( |
664 | builder: (BuildContext context, Widget? child) { |
665 | return Actions( |
666 | actions: actions, |
667 | child: Focus( |
668 | focusNode: focusNode, |
669 | child: const Placeholder(), |
670 | ), |
671 | ); |
672 | }, |
673 | color: const Color(0xFF123456), |
674 | ), |
675 | ); |
676 | focusNode.requestFocus(); |
677 | await tester.pump(); |
678 | expect(selectAllSpy.invoked, isFalse); |
679 | expect(copySpy.invoked, isFalse); |
680 | expect(pasteSpy.invoked, isFalse); |
681 | |
682 | // Select all. |
683 | await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft); |
684 | await tester.sendKeyDownEvent(LogicalKeyboardKey.keyA); |
685 | await tester.sendKeyUpEvent(LogicalKeyboardKey.keyA); |
686 | await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft); |
687 | await tester.pump(); |
688 | |
689 | expect(selectAllSpy.invoked, isTrue); |
690 | expect(copySpy.invoked, isFalse); |
691 | expect(pasteSpy.invoked, isFalse); |
692 | |
693 | // Copy. |
694 | await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft); |
695 | await tester.sendKeyDownEvent(LogicalKeyboardKey.keyC); |
696 | await tester.sendKeyUpEvent(LogicalKeyboardKey.keyC); |
697 | await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft); |
698 | await tester.pump(); |
699 | |
700 | expect(selectAllSpy.invoked, isTrue); |
701 | expect(copySpy.invoked, isTrue); |
702 | expect(pasteSpy.invoked, isFalse); |
703 | |
704 | // Paste. |
705 | await tester.sendKeyDownEvent(LogicalKeyboardKey.metaLeft); |
706 | await tester.sendKeyDownEvent(LogicalKeyboardKey.keyV); |
707 | await tester.sendKeyUpEvent(LogicalKeyboardKey.keyV); |
708 | await tester.sendKeyUpEvent(LogicalKeyboardKey.metaLeft); |
709 | await tester.pump(); |
710 | |
711 | expect(selectAllSpy.invoked, isTrue); |
712 | expect(copySpy.invoked, isTrue); |
713 | expect(pasteSpy.invoked, isTrue); |
714 | }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); |
715 | |
716 | group('Android Predictive Back', () { |
717 | Future<void> setAppLifeCycleState(AppLifecycleState state) async { |
718 | final ByteData? message = const StringCodec().encodeMessage(state.toString()); |
719 | await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger |
720 | .handlePlatformMessage('flutter/lifecycle', message, (ByteData? data) {}); |
721 | } |
722 | |
723 | final List<bool> frameworkHandlesBacks = <bool>[]; |
724 | setUp(() async { |
725 | frameworkHandlesBacks.clear(); |
726 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger |
727 | .setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { |
728 | if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') { |
729 | expect(methodCall.arguments, isA<bool>()); |
730 | frameworkHandlesBacks.add(methodCall.arguments as bool); |
731 | } |
732 | return; |
733 | }); |
734 | }); |
735 | |
736 | tearDown(() async { |
737 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger |
738 | .setMockMethodCallHandler(SystemChannels.platform, null); |
739 | await setAppLifeCycleState(AppLifecycleState.resumed); |
740 | }); |
741 | |
742 | testWidgets('WidgetsApp calls setFrameworkHandlesBack only when app is ready', (WidgetTester tester) async { |
743 | // Start in the `resumed` state, where setFrameworkHandlesBack should be |
744 | // called like normal. |
745 | await setAppLifeCycleState(AppLifecycleState.resumed); |
746 | |
747 | late BuildContext currentContext; |
748 | await tester.pumpWidget( |
749 | WidgetsApp( |
750 | color: const Color(0xFF123456), |
751 | builder: (BuildContext context, Widget? child) { |
752 | currentContext = context; |
753 | return const Placeholder(); |
754 | }, |
755 | ), |
756 | ); |
757 | |
758 | expect(frameworkHandlesBacks, isEmpty); |
759 | |
760 | const NavigationNotification(canHandlePop: true).dispatch(currentContext); |
761 | await tester.pumpAndSettle(); |
762 | expect(frameworkHandlesBacks, isNotEmpty); |
763 | expect(frameworkHandlesBacks.last, isTrue); |
764 | |
765 | const NavigationNotification(canHandlePop: false).dispatch(currentContext); |
766 | await tester.pumpAndSettle(); |
767 | expect(frameworkHandlesBacks.last, isFalse); |
768 | |
769 | // Set the app state to inactive, where setFrameworkHandlesBack is still |
770 | // called. This could happen when responding to a tap on a notification |
771 | // when the app is not active and immediately navigating, for example. |
772 | // See https://github.com/flutter/flutter/pull/154313. |
773 | await setAppLifeCycleState(AppLifecycleState.inactive); |
774 | |
775 | final int inactiveStartCallsLength = frameworkHandlesBacks.length; |
776 | const NavigationNotification(canHandlePop: true).dispatch(currentContext); |
777 | await tester.pumpAndSettle(); |
778 | expect(frameworkHandlesBacks, hasLength(inactiveStartCallsLength + 1)); |
779 | |
780 | const NavigationNotification(canHandlePop: false).dispatch(currentContext); |
781 | await tester.pumpAndSettle(); |
782 | expect(frameworkHandlesBacks, hasLength(inactiveStartCallsLength + 2)); |
783 | |
784 | // Set the app state to detached, where setFrameworkHandlesBack shouldn't |
785 | // be called. |
786 | await setAppLifeCycleState(AppLifecycleState.detached); |
787 | |
788 | final int finalCallsLength = frameworkHandlesBacks.length; |
789 | const NavigationNotification(canHandlePop: true).dispatch(currentContext); |
790 | await tester.pumpAndSettle(); |
791 | expect(frameworkHandlesBacks, hasLength(finalCallsLength)); |
792 | |
793 | const NavigationNotification(canHandlePop: false).dispatch(currentContext); |
794 | await tester.pumpAndSettle(); |
795 | expect(frameworkHandlesBacks, hasLength(finalCallsLength)); |
796 | }, |
797 | skip: kIsWeb, // [intended] predictive back is only native Android. |
798 | variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }) |
799 | ); |
800 | }); |
801 | } |
802 | |
803 | typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext context, RouteInformation information); |
804 | typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate); |
805 | |
806 | class SelectAllSpy extends Action<SelectAllTextIntent> { |
807 | bool invoked = false; |
808 | @override |
809 | void invoke(SelectAllTextIntent intent) { |
810 | invoked = true; |
811 | } |
812 | } |
813 | |
814 | class CopySpy extends Action<CopySelectionTextIntent> { |
815 | bool invoked = false; |
816 | @override |
817 | void invoke(CopySelectionTextIntent intent) { |
818 | invoked = true; |
819 | } |
820 | } |
821 | |
822 | class PasteSpy extends Action<PasteTextIntent> { |
823 | bool invoked = false; |
824 | @override |
825 | void invoke(PasteTextIntent intent) { |
826 | invoked = true; |
827 | } |
828 | } |
829 | |
830 | class SimpleRouteInformationParser extends RouteInformationParser<RouteInformation> { |
831 | SimpleRouteInformationParser(); |
832 | |
833 | @override |
834 | Future<RouteInformation> parseRouteInformation(RouteInformation information) { |
835 | return SynchronousFuture<RouteInformation>(information); |
836 | } |
837 | |
838 | @override |
839 | RouteInformation restoreRouteInformation(RouteInformation configuration) { |
840 | return configuration; |
841 | } |
842 | } |
843 | |
844 | class SimpleNavigatorRouterDelegate extends RouterDelegate<RouteInformation> with PopNavigatorRouterDelegateMixin<RouteInformation>, ChangeNotifier { |
845 | SimpleNavigatorRouterDelegate({ |
846 | required this.builder, |
847 | required this.onPopPage, |
848 | }); |
849 | |
850 | @override |
851 | GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); |
852 | |
853 | RouteInformation get routeInformation => _routeInformation; |
854 | late RouteInformation _routeInformation; |
855 | set routeInformation(RouteInformation newValue) { |
856 | _routeInformation = newValue; |
857 | notifyListeners(); |
858 | } |
859 | |
860 | final SimpleRouterDelegateBuilder builder; |
861 | final SimpleNavigatorRouterDelegatePopPage<void> onPopPage; |
862 | |
863 | @override |
864 | Future<void> setNewRoutePath(RouteInformation configuration) { |
865 | _routeInformation = configuration; |
866 | return SynchronousFuture<void>(null); |
867 | } |
868 | |
869 | bool _handlePopPage(Route<void> route, void data) { |
870 | return onPopPage(route, data, this); |
871 | } |
872 | |
873 | @override |
874 | Widget build(BuildContext context) { |
875 | return Navigator( |
876 | key: navigatorKey, |
877 | onPopPage: _handlePopPage, |
878 | pages: <Page<void>>[ |
879 | // We need at least two pages for the pop to propagate through. |
880 | // Otherwise, the navigator will bubble the pop to the system navigator. |
881 | const MaterialPage<void>( |
882 | child: Text('base'), |
883 | ), |
884 | MaterialPage<void>( |
885 | key: ValueKey<String>(routeInformation.uri.toString()), |
886 | child: builder(context, routeInformation), |
887 | ), |
888 | ], |
889 | ); |
890 | } |
891 | } |
892 |
Definitions
- TestIntent
- TestIntent
- TestAction
- TestAction
- invoke
- main
- expectFlutterError
- setAppLifeCycleState
- SelectAllSpy
- invoke
- CopySpy
- invoke
- PasteSpy
- invoke
- SimpleRouteInformationParser
- SimpleRouteInformationParser
- parseRouteInformation
- restoreRouteInformation
- SimpleNavigatorRouterDelegate
- SimpleNavigatorRouterDelegate
- routeInformation
- routeInformation
- setNewRoutePath
- _handlePopPage
Learn more about Flutter for embedded and desktop on industrialflutter.com