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