| 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 | ( |
| 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 | |
| 785 | typedef SimpleRouterDelegateBuilder = |
| 786 | Widget Function(BuildContext context, RouteInformation information); |
| 787 | typedef SimpleNavigatorRouterDelegatePopPage<T> = |
| 788 | bool Function(Route<T> route, T result, SimpleNavigatorRouterDelegate delegate); |
| 789 | |
| 790 | class SelectAllSpy extends Action<SelectAllTextIntent> { |
| 791 | bool invoked = false; |
| 792 | @override |
| 793 | void invoke(SelectAllTextIntent intent) { |
| 794 | invoked = true; |
| 795 | } |
| 796 | } |
| 797 | |
| 798 | class CopySpy extends Action<CopySelectionTextIntent> { |
| 799 | bool invoked = false; |
| 800 | @override |
| 801 | void invoke(CopySelectionTextIntent intent) { |
| 802 | invoked = true; |
| 803 | } |
| 804 | } |
| 805 | |
| 806 | class PasteSpy extends Action<PasteTextIntent> { |
| 807 | bool invoked = false; |
| 808 | @override |
| 809 | void invoke(PasteTextIntent intent) { |
| 810 | invoked = true; |
| 811 | } |
| 812 | } |
| 813 | |
| 814 | class 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 | |
| 828 | class 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 | |