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
5import 'package:flutter/foundation.dart';
6import 'package:flutter/material.dart';
7import 'package:flutter/services.dart';
8import 'package:flutter_test/flutter_test.dart';
9
10class TestIntent extends Intent {
11 const TestIntent();
12}
13
14class 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
25void 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
803typedef SimpleRouterDelegateBuilder = Widget Function(BuildContext context, RouteInformation information);
804typedef SimpleNavigatorRouterDelegatePopPage<T> = bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
805
806class SelectAllSpy extends Action<SelectAllTextIntent> {
807 bool invoked = false;
808 @override
809 void invoke(SelectAllTextIntent intent) {
810 invoked = true;
811 }
812}
813
814class CopySpy extends Action<CopySelectionTextIntent> {
815 bool invoked = false;
816 @override
817 void invoke(CopySelectionTextIntent intent) {
818 invoked = true;
819 }
820}
821
822class PasteSpy extends Action<PasteTextIntent> {
823 bool invoked = false;
824 @override
825 void invoke(PasteTextIntent intent) {
826 invoked = true;
827 }
828}
829
830class 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
844class 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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com