feat: add few required properties to test widgets app (#182805)
This PR add new properties like initialRoute, builder, shortcuts,
actions and restorationScopeId.
part of: https://github.com/flutter/flutter/issues/177415
## Pre-launch Checklist
- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
diff --git a/packages/flutter/test/widgets/widgets_app_tester.dart b/packages/flutter/test/widgets/widgets_app_tester.dart
index 8de927a..ae78eb7 100644
--- a/packages/flutter/test/widgets/widgets_app_tester.dart
+++ b/packages/flutter/test/widgets/widgets_app_tester.dart
@@ -56,9 +56,14 @@
super.key,
this.navigatorKey,
this.home,
+ this.initialRoute,
this.routes = const <String, WidgetBuilder>{},
this.color = const Color(0xFFFFFFFF),
this.pageRouteBuilder = _defaultPageRouteBuilder,
+ this.builder,
+ this.shortcuts,
+ this.actions,
+ this.restorationScopeId,
});
/// A key to use when building the [Navigator].
@@ -91,6 +96,15 @@
/// * [WidgetsApp.home], the equivalent property in [WidgetsApp].
final Widget? home;
+ /// The name of the first route to show when the app launches.
+ ///
+ /// Defaults to [Navigator.defaultRouteName] (typically `/`).
+ ///
+ /// See also:
+ ///
+ /// * [WidgetsApp.initialRoute], the equivalent property in [WidgetsApp].
+ final String? initialRoute;
+
/// The application's top-level routing table.
///
/// Maps route names to widget builders. When navigating to a named route,
@@ -148,6 +162,44 @@
/// * [WidgetsApp.pageRouteBuilder], the equivalent property in [WidgetsApp].
final PageRouteFactory pageRouteBuilder;
+ /// A builder for inserting widgets above the [Navigator].
+ ///
+ /// See also:
+ ///
+ /// * [WidgetsApp.builder], the equivalent property in [WidgetsApp].
+ final TransitionBuilder? builder;
+
+ /// The application's keyboard shortcut map.
+ ///
+ /// In tests, this allows registering custom keyboard shortcuts to verify
+ /// that key combinations trigger the expected [Intent]s.
+ ///
+ /// When null, [WidgetsApp.defaultShortcuts] are used.
+ ///
+ /// See also:
+ ///
+ /// * [WidgetsApp.shortcuts], the equivalent property in [WidgetsApp].
+ final Map<ShortcutActivator, Intent>? shortcuts;
+
+ /// The application's action map.
+ ///
+ /// In tests, this allows registering custom [Action]s that respond to
+ /// [Intent]s dispatched by [Shortcuts] or programmatic invocation.
+ ///
+ /// When null, [WidgetsApp.defaultActions] are used.
+ ///
+ /// See also:
+ ///
+ /// * [WidgetsApp.actions], the equivalent property in [WidgetsApp].
+ final Map<Type, Action<Intent>>? actions;
+
+ /// The identifier to use for state restoration of the app's [Navigator].
+ ///
+ /// See also:
+ ///
+ /// * [WidgetsApp.restorationScopeId], the equivalent property in [WidgetsApp].
+ final String? restorationScopeId;
+
static PageRoute<T> _defaultPageRouteBuilder<T>(RouteSettings settings, WidgetBuilder builder) {
return PageRouteBuilder<T>(
settings: settings,
@@ -173,8 +225,13 @@
color: color,
navigatorKey: navigatorKey,
home: home,
+ initialRoute: initialRoute,
routes: routes,
pageRouteBuilder: pageRouteBuilder,
+ builder: builder,
+ shortcuts: shortcuts,
+ actions: actions,
+ restorationScopeId: restorationScopeId,
);
}
}
diff --git a/packages/flutter/test/widgets/widgets_app_tester_test.dart b/packages/flutter/test/widgets/widgets_app_tester_test.dart
index dc828bb..f700960 100644
--- a/packages/flutter/test/widgets/widgets_app_tester_test.dart
+++ b/packages/flutter/test/widgets/widgets_app_tester_test.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -398,6 +399,157 @@
expect(find.text('Page 2 Content'), findsOneWidget);
});
+ testWidgets('initialRoute navigates to the specified route on launch', (
+ WidgetTester tester,
+ ) async {
+ await tester.pumpWidget(
+ TestWidgetsApp(
+ initialRoute: '/details',
+ routes: <String, WidgetBuilder>{
+ '/': (BuildContext context) => const Text('Home Page'),
+ '/details': (BuildContext context) => const Text('Details Page'),
+ },
+ ),
+ );
+
+ await tester.pumpAndSettle();
+
+ expect(find.text('Details Page'), findsOneWidget);
+ });
+
+ testWidgets('initialRoute defaults to null', (WidgetTester tester) async {
+ await tester.pumpWidget(const TestWidgetsApp(home: Placeholder()));
+
+ final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp));
+ expect(widgetsApp.initialRoute, isNull);
+ });
+
+ testWidgets('builder wraps the navigator content', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ TestWidgetsApp(
+ builder: (BuildContext context, Widget? child) {
+ return Column(
+ children: <Widget>[
+ const Text('Header from builder'),
+ Expanded(child: child ?? const SizedBox.shrink()),
+ ],
+ );
+ },
+ home: const Text('Content'),
+ ),
+ );
+
+ expect(find.text('Header from builder'), findsOneWidget);
+ expect(find.text('Content'), findsOneWidget);
+ });
+
+ testWidgets('builder defaults to null', (WidgetTester tester) async {
+ await tester.pumpWidget(const TestWidgetsApp(home: Placeholder()));
+
+ final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp));
+ expect(widgetsApp.builder, isNull);
+ });
+
+ testWidgets('builder receives the navigator as child', (WidgetTester tester) async {
+ Widget? receivedChild;
+
+ await tester.pumpWidget(
+ TestWidgetsApp(
+ builder: (BuildContext context, Widget? child) {
+ receivedChild = child;
+ return child ?? const SizedBox.shrink();
+ },
+ home: const Text('Home'),
+ ),
+ );
+
+ expect(receivedChild, isNotNull);
+ });
+
+ testWidgets('shortcuts defaults to null', (WidgetTester tester) async {
+ await tester.pumpWidget(const TestWidgetsApp(home: Placeholder()));
+
+ final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp));
+ expect(widgetsApp.shortcuts, isNull);
+ });
+
+ testWidgets('custom shortcuts are passed to WidgetsApp', (WidgetTester tester) async {
+ final customShortcuts = <ShortcutActivator, Intent>{
+ const SingleActivator(LogicalKeyboardKey.keyX, control: true): VoidCallbackIntent(() {}),
+ };
+
+ await tester.pumpWidget(
+ TestWidgetsApp(home: const Placeholder(), shortcuts: customShortcuts),
+ );
+
+ final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp));
+ expect(widgetsApp.shortcuts, customShortcuts);
+ });
+
+ testWidgets('custom shortcuts respond to key events', (WidgetTester tester) async {
+ var shortcutTriggered = false;
+
+ await tester.pumpWidget(
+ TestWidgetsApp(
+ shortcuts: <ShortcutActivator, Intent>{
+ const SingleActivator(LogicalKeyboardKey.keyK, control: true): VoidCallbackIntent(() {
+ shortcutTriggered = true;
+ }),
+ },
+ actions: <Type, Action<Intent>>{VoidCallbackIntent: VoidCallbackAction()},
+ home: const Focus(autofocus: true, child: Placeholder()),
+ ),
+ );
+
+ await tester.pump();
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
+ await tester.sendKeyDownEvent(LogicalKeyboardKey.keyK);
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.keyK);
+ await tester.sendKeyUpEvent(LogicalKeyboardKey.control);
+
+ expect(shortcutTriggered, isTrue);
+ });
+
+ testWidgets('actions defaults to null', (WidgetTester tester) async {
+ await tester.pumpWidget(const TestWidgetsApp(home: Placeholder()));
+
+ final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp));
+ expect(widgetsApp.actions, isNull);
+ });
+
+ testWidgets('custom actions are passed to WidgetsApp', (WidgetTester tester) async {
+ final customActions = <Type, Action<Intent>>{VoidCallbackIntent: VoidCallbackAction()};
+
+ await tester.pumpWidget(TestWidgetsApp(home: const Placeholder(), actions: customActions));
+
+ final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp));
+ expect(widgetsApp.actions, customActions);
+ });
+
+ testWidgets('restorationScopeId is passed to WidgetsApp', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const TestWidgetsApp(home: Placeholder(), restorationScopeId: 'test-app'),
+ );
+
+ final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp));
+ expect(widgetsApp.restorationScopeId, 'test-app');
+ });
+
+ testWidgets('restorationScopeId defaults to null', (WidgetTester tester) async {
+ await tester.pumpWidget(const TestWidgetsApp(home: Placeholder()));
+
+ final WidgetsApp widgetsApp = tester.widget(find.byType(WidgetsApp));
+ expect(widgetsApp.restorationScopeId, isNull);
+ });
+
+ testWidgets('restorationScopeId inserts RootRestorationScope', (WidgetTester tester) async {
+ await tester.pumpWidget(
+ const TestWidgetsApp(home: Placeholder(), restorationScopeId: 'test-scope'),
+ );
+
+ expect(find.byType(RootRestorationScope), findsOneWidget);
+ });
+
testWidgets('navigatorKey provides access to NavigatorState', (WidgetTester tester) async {
final navigatorKey = GlobalKey<NavigatorState>();