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 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
776typedef SimpleRouterDelegateBuilder =
777 Widget Function(BuildContext context, RouteInformation information);
778typedef SimpleNavigatorRouterDelegatePopPage<T> =
779 bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate);
780
781class SelectAllSpy extends Action<SelectAllTextIntent> {
782 bool invoked = false;
783 @override
784 void invoke(SelectAllTextIntent intent) {
785 invoked = true;
786 }
787}
788
789class CopySpy extends Action<CopySelectionTextIntent> {
790 bool invoked = false;
791 @override
792 void invoke(CopySelectionTextIntent intent) {
793 invoked = true;
794 }
795}
796
797class PasteSpy extends Action<PasteTextIntent> {
798 bool invoked = false;
799 @override
800 void invoke(PasteTextIntent intent) {
801 invoked = true;
802 }
803}
804
805class 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
819class 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

Provided by KDAB

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