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