// Copyright 2015 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/material.dart'; import 'observer_tester.dart'; import 'semantics_tester.dart'; class FirstWidget extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( onTap: () { Navigator.pushNamed(context, '/second'); }, child: Container( color: const Color(0xFFFFFF00), child: const Text('X'), ), ); } } class SecondWidget extends StatefulWidget { @override SecondWidgetState createState() => SecondWidgetState(); } class SecondWidgetState extends State<SecondWidget> { @override Widget build(BuildContext context) { return GestureDetector( onTap: () => Navigator.pop(context), child: Container( color: const Color(0xFFFF00FF), child: const Text('Y'), ), ); } } typedef ExceptionCallback = void Function(dynamic exception); class ThirdWidget extends StatelessWidget { const ThirdWidget({ this.targetKey, this.onException }); final Key targetKey; final ExceptionCallback onException; @override Widget build(BuildContext context) { return GestureDetector( key: targetKey, onTap: () { try { Navigator.of(context); } catch (e) { onException(e); } }, behavior: HitTestBehavior.opaque, ); } } class OnTapPage extends StatelessWidget { const OnTapPage({ Key key, this.id, this.onTap }) : super(key: key); final String id; final VoidCallback onTap; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Page $id')), body: GestureDetector( onTap: onTap, behavior: HitTestBehavior.opaque, child: Container( child: Center( child: Text(id, style: Theme.of(context).textTheme.display2), ), ), ), ); } } void main() { testWidgets('Can navigator navigate to and from a stateful widget', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => FirstWidget(), // X '/second': (BuildContext context) => SecondWidget(), // Y }; await tester.pumpWidget(MaterialApp(routes: routes)); expect(find.text('X'), findsOneWidget); expect(find.text('Y', skipOffstage: false), findsNothing); await tester.tap(find.text('X')); await tester.pump(); expect(find.text('X'), findsOneWidget); expect(find.text('Y', skipOffstage: false), isOffstage); await tester.pump(const Duration(milliseconds: 10)); expect(find.text('X'), findsOneWidget); expect(find.text('Y'), findsOneWidget); await tester.pump(const Duration(milliseconds: 10)); expect(find.text('X'), findsOneWidget); expect(find.text('Y'), findsOneWidget); await tester.pump(const Duration(milliseconds: 10)); expect(find.text('X'), findsOneWidget); expect(find.text('Y'), findsOneWidget); await tester.pump(const Duration(seconds: 1)); expect(find.text('X'), findsNothing); expect(find.text('X', skipOffstage: false), findsOneWidget); expect(find.text('Y'), findsOneWidget); await tester.tap(find.text('Y')); expect(find.text('X'), findsNothing); expect(find.text('Y'), findsOneWidget); await tester.pump(); await tester.pump(); expect(find.text('X'), findsOneWidget); expect(find.text('Y'), findsOneWidget); await tester.pump(const Duration(milliseconds: 10)); expect(find.text('X'), findsOneWidget); expect(find.text('Y'), findsOneWidget); await tester.pump(const Duration(seconds: 1)); expect(find.text('X'), findsOneWidget); expect(find.text('Y', skipOffstage: false), findsNothing); }); testWidgets('Navigator.of fails gracefully when not found in context', (WidgetTester tester) async { const Key targetKey = Key('foo'); dynamic exception; final Widget widget = ThirdWidget( targetKey: targetKey, onException: (dynamic e) { exception = e; }, ); await tester.pumpWidget(widget); await tester.tap(find.byKey(targetKey)); expect(exception, isInstanceOf<FlutterError>()); expect('$exception', startsWith('Navigator operation requested with a context')); }); testWidgets('Navigator.of rootNavigator finds root Navigator', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Column( children: <Widget>[ const SizedBox( height: 300.0, child: Text('Root page'), ), SizedBox( height: 300.0, child: Navigator( onGenerateRoute: (RouteSettings settings) { if (settings.isInitialRoute) { return MaterialPageRoute<void>( builder: (BuildContext context) { return RaisedButton( child: const Text('Next'), onPressed: () { Navigator.of(context).push( MaterialPageRoute<void>( builder: (BuildContext context) { return RaisedButton( child: const Text('Inner page'), onPressed: () { Navigator.of(context, rootNavigator: true).push( MaterialPageRoute<void>( builder: (BuildContext context) { return const Text('Dialog'); } ), ); }, ); } ), ); }, ); }, ); } return null; }, ), ), ], ), ), )); await tester.tap(find.text('Next')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); // Both elements are on screen. expect(tester.getTopLeft(find.text('Root page')).dy, 0.0); expect(tester.getTopLeft(find.text('Inner page')).dy, greaterThan(300.0)); await tester.tap(find.text('Inner page')); await tester.pump(); await tester.pump(const Duration(milliseconds: 300)); // Dialog is pushed to the whole page and is at the top of the screen, not // inside the inner page. expect(tester.getTopLeft(find.text('Dialog')).dy, 0.0); }); testWidgets('Gestures between push and build are ignored', (WidgetTester tester) async { final List<String> log = <String>[]; final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) { return Row( children: <Widget>[ GestureDetector( onTap: () { log.add('left'); Navigator.pushNamed(context, '/second'); }, child: const Text('left'), ), GestureDetector( onTap: () { log.add('right'); }, child: const Text('right'), ), ], ); }, '/second': (BuildContext context) => Container(), }; await tester.pumpWidget(MaterialApp(routes: routes)); expect(log, isEmpty); await tester.tap(find.text('left')); expect(log, equals(<String>['left'])); await tester.tap(find.text('right')); expect(log, equals(<String>['left'])); }); // This test doesn't work because the testing framework uses a fake version of // the pointer event dispatch loop. // // TODO(abarth): Test more of the real code and enable this test. // See https://github.com/flutter/flutter/issues/4771. // // testWidgets('Pending gestures are rejected', (WidgetTester tester) async { // List<String> log = <String>[]; // final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ // '/': (BuildContext context) { // return new Row( // children: <Widget>[ // new GestureDetector( // onTap: () { // log.add('left'); // Navigator.pushNamed(context, '/second'); // }, // child: new Text('left') // ), // new GestureDetector( // onTap: () { log.add('right'); }, // child: new Text('right') // ), // ] // ); // }, // '/second': (BuildContext context) => new Container(), // }; // await tester.pumpWidget(new MaterialApp(routes: routes)); // TestGesture gesture = await tester.startGesture(tester.getCenter(find.text('right')), pointer: 23); // expect(log, isEmpty); // await tester.tap(find.text('left')); // expect(log, equals(<String>['left'])); // await gesture.up(); // expect(log, equals(<String>['left'])); // }); testWidgets('popAndPushNamed', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.popAndPushNamed(context, '/B'); }), '/B': (BuildContext context) => OnTapPage(id: 'B', onTap: () { Navigator.pop(context); }), }; await tester.pumpWidget(MaterialApp(routes: routes)); expect(find.text('/'), findsOneWidget); expect(find.text('A', skipOffstage: false), findsNothing); expect(find.text('B', skipOffstage: false), findsNothing); await tester.tap(find.text('/')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsNothing); expect(find.text('A'), findsOneWidget); expect(find.text('B'), findsNothing); await tester.tap(find.text('A')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsNothing); expect(find.text('A'), findsNothing); expect(find.text('B'), findsOneWidget); }); testWidgets('Push and pop should trigger the observers', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pop(context); }), }; bool isPushed = false; bool isPopped = false; final TestObserver observer = TestObserver() ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) { // Pushes the initial route. expect(route is PageRoute && route.settings.name == '/', isTrue); expect(previousRoute, isNull); isPushed = true; } ..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) { isPopped = true; }; await tester.pumpWidget(MaterialApp( routes: routes, navigatorObservers: <NavigatorObserver>[observer], )); expect(find.text('/'), findsOneWidget); expect(find.text('A'), findsNothing); expect(isPushed, isTrue); expect(isPopped, isFalse); isPushed = false; isPopped = false; observer.onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) { expect(route is PageRoute && route.settings.name == '/A', isTrue); expect(previousRoute is PageRoute && previousRoute.settings.name == '/', isTrue); isPushed = true; }; await tester.tap(find.text('/')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsNothing); expect(find.text('A'), findsOneWidget); expect(isPushed, isTrue); expect(isPopped, isFalse); isPushed = false; isPopped = false; observer.onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) { expect(route is PageRoute && route.settings.name == '/A', isTrue); expect(previousRoute is PageRoute && previousRoute.settings.name == '/', isTrue); isPopped = true; }; await tester.tap(find.text('A')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsOneWidget); expect(find.text('A'), findsNothing); expect(isPushed, isFalse); expect(isPopped, isTrue); }); testWidgets('Add and remove an observer should work', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pop(context); }), }; bool isPushed = false; bool isPopped = false; final TestObserver observer1 = TestObserver(); final TestObserver observer2 = TestObserver() ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) { isPushed = true; } ..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) { isPopped = true; }; await tester.pumpWidget(MaterialApp( routes: routes, navigatorObservers: <NavigatorObserver>[observer1], )); expect(isPushed, isFalse); expect(isPopped, isFalse); await tester.pumpWidget(MaterialApp( routes: routes, navigatorObservers: <NavigatorObserver>[observer1, observer2], )); await tester.tap(find.text('/')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(isPushed, isTrue); expect(isPopped, isFalse); isPushed = false; isPopped = false; await tester.pumpWidget(MaterialApp( routes: routes, navigatorObservers: <NavigatorObserver>[observer1], )); await tester.tap(find.text('A')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(isPushed, isFalse); expect(isPopped, isFalse); }); testWidgets('replaceNamed', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushReplacementNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pushReplacementNamed(context, '/B'); }), '/B': (BuildContext context) => const OnTapPage(id: 'B'), }; await tester.pumpWidget(MaterialApp(routes: routes)); await tester.tap(find.text('/')); // replaceNamed('/A') await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsNothing); expect(find.text('A'), findsOneWidget); await tester.tap(find.text('A')); // replaceNamed('/B') await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsNothing); expect(find.text('A'), findsNothing); expect(find.text('B'), findsOneWidget); }); testWidgets('replaceNamed returned value', (WidgetTester tester) async { Future<String> value; final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { value = Navigator.pushReplacementNamed(context, '/B', result: 'B'); }), '/B': (BuildContext context) => OnTapPage(id: 'B', onTap: () { Navigator.pop(context, 'B'); }), }; await tester.pumpWidget(MaterialApp( onGenerateRoute: (RouteSettings settings) { return PageRouteBuilder<String>( settings: settings, pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { return routes[settings.name](context); }, ); } )); expect(find.text('/'), findsOneWidget); expect(find.text('A', skipOffstage: false), findsNothing); expect(find.text('B', skipOffstage: false), findsNothing); await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsNothing); expect(find.text('A'), findsOneWidget); expect(find.text('B'), findsNothing); await tester.tap(find.text('A')); // replaceNamed('/B'), stack becomes /, /B await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsNothing); expect(find.text('A'), findsNothing); expect(find.text('B'), findsOneWidget); await tester.tap(find.text('B')); // pop, stack becomes / await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsOneWidget); expect(find.text('A'), findsNothing); expect(find.text('B'), findsNothing); final String replaceNamedValue = await value; // replaceNamed result was 'B' expect(replaceNamedValue, 'B'); }); testWidgets('removeRoute', (WidgetTester tester) async { final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{ '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pushNamed(context, '/B'); }), '/B': (BuildContext context) => const OnTapPage(id: 'B'), }; final Map<String, Route<String>> routes = <String, Route<String>>{}; Route<String> removedRoute; Route<String> previousRoute; final TestObserver observer = TestObserver() ..onRemoved = (Route<dynamic> route, Route<dynamic> previous) { removedRoute = route; previousRoute = previous; }; await tester.pumpWidget(MaterialApp( navigatorObservers: <NavigatorObserver>[observer], onGenerateRoute: (RouteSettings settings) { routes[settings.name] = PageRouteBuilder<String>( settings: settings, pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { return pageBuilders[settings.name](context); }, ); return routes[settings.name]; }, )); expect(find.text('/'), findsOneWidget); expect(find.text('A'), findsNothing); expect(find.text('B'), findsNothing); await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('A'), findsOneWidget); expect(find.text('B'), findsNothing); await tester.tap(find.text('A')); // pushNamed('/B'), stack becomes /, /A, /B await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('A'), findsNothing); expect(find.text('B'), findsOneWidget); // Verify that the navigator's stack is ordered as expected. expect(routes['/'].isActive, true); expect(routes['/A'].isActive, true); expect(routes['/B'].isActive, true); expect(routes['/'].isFirst, true); expect(routes['/B'].isCurrent, true); final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator)); navigator.removeRoute(routes['/B']); // stack becomes /, /A await tester.pump(); expect(find.text('/'), findsNothing); expect(find.text('A'), findsOneWidget); expect(find.text('B'), findsNothing); // Verify that the navigator's stack no longer includes /B expect(routes['/'].isActive, true); expect(routes['/A'].isActive, true); expect(routes['/B'].isActive, false); expect(routes['/'].isFirst, true); expect(routes['/A'].isCurrent, true); expect(removedRoute, routes['/B']); expect(previousRoute, routes['/A']); navigator.removeRoute(routes['/A']); // stack becomes just / await tester.pump(); expect(find.text('/'), findsOneWidget); expect(find.text('A'), findsNothing); expect(find.text('B'), findsNothing); // Verify that the navigator's stack no longer includes /A expect(routes['/'].isActive, true); expect(routes['/A'].isActive, false); expect(routes['/B'].isActive, false); expect(routes['/'].isFirst, true); expect(routes['/'].isCurrent, true); expect(removedRoute, routes['/A']); expect(previousRoute, routes['/']); }); testWidgets('remove a route whose value is awaited', (WidgetTester tester) async { Future<String> pageValue; final Map<String, WidgetBuilder> pageBuilders = <String, WidgetBuilder>{ '/': (BuildContext context) => OnTapPage(id: '/', onTap: () { pageValue = Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pop(context, 'A'); }), }; final Map<String, Route<String>> routes = <String, Route<String>>{}; await tester.pumpWidget(MaterialApp( onGenerateRoute: (RouteSettings settings) { routes[settings.name] = PageRouteBuilder<String>( settings: settings, pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) { return pageBuilders[settings.name](context); }, ); return routes[settings.name]; } )); await tester.tap(find.text('/')); // pushNamed('/A'), stack becomes /, /A await tester.pumpAndSettle(); pageValue.then((String value) { assert(false); }); final NavigatorState navigator = tester.state<NavigatorState>(find.byType(Navigator)); navigator.removeRoute(routes['/A']); // stack becomes /, pageValue will not complete }); testWidgets('replacing route can be observed', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); final List<String> log = <String>[]; final TestObserver observer = TestObserver() ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) { log.add('pushed ${route.settings.name} (previous is ${previousRoute == null ? "<none>" : previousRoute.settings.name})'); } ..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) { log.add('popped ${route.settings.name} (previous is ${previousRoute == null ? "<none>" : previousRoute.settings.name})'); } ..onRemoved = (Route<dynamic> route, Route<dynamic> previousRoute) { log.add('removed ${route.settings.name} (previous is ${previousRoute == null ? "<none>" : previousRoute.settings.name})'); } ..onReplaced = (Route<dynamic> newRoute, Route<dynamic> oldRoute) { log.add('replaced ${oldRoute.settings.name} with ${newRoute.settings.name}'); }; Route<void> routeB; await tester.pumpWidget(MaterialApp( navigatorKey: key, navigatorObservers: <NavigatorObserver>[observer], home: FlatButton( child: const Text('A'), onPressed: () { key.currentState.push<void>(routeB = MaterialPageRoute<void>( settings: const RouteSettings(name: 'B'), builder: (BuildContext context) { return FlatButton( child: const Text('B'), onPressed: () { key.currentState.push<void>(MaterialPageRoute<int>( settings: const RouteSettings(name: 'C'), builder: (BuildContext context) { return FlatButton( child: const Text('C'), onPressed: () { key.currentState.replace( oldRoute: routeB, newRoute: MaterialPageRoute<int>( settings: const RouteSettings(name: 'D'), builder: (BuildContext context) { return const Text('D'); }, ), ); }, ); }, )); }, ); }, )); }, ), )); expect(log, <String>['pushed / (previous is <none>)']); await tester.tap(find.text('A')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(log, <String>['pushed / (previous is <none>)', 'pushed B (previous is /)']); await tester.tap(find.text('B')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(log, <String>['pushed / (previous is <none>)', 'pushed B (previous is /)', 'pushed C (previous is B)']); await tester.tap(find.text('C')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(log, <String>['pushed / (previous is <none>)', 'pushed B (previous is /)', 'pushed C (previous is B)', 'replaced B with D']); }); testWidgets('didStartUserGesture observable', (WidgetTester tester) async { final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pop(context); }), }; Route<dynamic> observedRoute; Route<dynamic> observedPreviousRoute; final TestObserver observer = TestObserver() ..onStartUserGesture = (Route<dynamic> route, Route<dynamic> previousRoute) { observedRoute = route; observedPreviousRoute = previousRoute; }; await tester.pumpWidget(MaterialApp( routes: routes, navigatorObservers: <NavigatorObserver>[observer], )); await tester.tap(find.text('/')); await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(find.text('/'), findsNothing); expect(find.text('A'), findsOneWidget); tester.state<NavigatorState>(find.byType(Navigator)).didStartUserGesture(); expect(observedRoute.settings.name, '/A'); expect(observedPreviousRoute.settings.name, '/'); }); testWidgets('ModalRoute.of sets up a route to rebuild if its state changes', (WidgetTester tester) async { final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>(); final List<String> log = <String>[]; Route<void> routeB; await tester.pumpWidget(MaterialApp( navigatorKey: key, home: FlatButton( child: const Text('A'), onPressed: () { key.currentState.push<void>(routeB = MaterialPageRoute<void>( settings: const RouteSettings(name: 'B'), builder: (BuildContext context) { log.add('building B'); return FlatButton( child: const Text('B'), onPressed: () { key.currentState.push<void>(MaterialPageRoute<int>( settings: const RouteSettings(name: 'C'), builder: (BuildContext context) { log.add('building C'); log.add('found ${ModalRoute.of(context).settings.name}'); return FlatButton( child: const Text('C'), onPressed: () { key.currentState.replace( oldRoute: routeB, newRoute: MaterialPageRoute<int>( settings: const RouteSettings(name: 'D'), builder: (BuildContext context) { log.add('building D'); return const Text('D'); }, ), ); }, ); }, )); }, ); }, )); }, ), )); expect(log, <String>[]); await tester.tap(find.text('A')); await tester.pumpAndSettle(const Duration(milliseconds: 10)); expect(log, <String>['building B']); await tester.tap(find.text('B')); await tester.pumpAndSettle(const Duration(milliseconds: 10)); expect(log, <String>['building B', 'building C', 'found C']); await tester.tap(find.text('C')); await tester.pumpAndSettle(const Duration(milliseconds: 10)); expect(log, <String>['building B', 'building C', 'found C', 'building D']); key.currentState.pop<void>(); await tester.pumpAndSettle(const Duration(milliseconds: 10)); expect(log, <String>['building B', 'building C', 'found C', 'building D', 'building C', 'found C']); }); testWidgets('route semantics', (WidgetTester tester) async { final SemanticsTester semantics = SemanticsTester(tester); final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ '/': (BuildContext context) => OnTapPage(id: '1', onTap: () { Navigator.pushNamed(context, '/A'); }), '/A': (BuildContext context) => OnTapPage(id: '2', onTap: () { Navigator.pushNamed(context, '/B/C'); }), '/B/C': (BuildContext context) => const OnTapPage(id: '3'), }; await tester.pumpWidget(MaterialApp(routes: routes)); expect(semantics, includesNodeWith( flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], )); expect(semantics, includesNodeWith( label: 'Page 1', flags: <SemanticsFlag>[ SemanticsFlag.namesRoute, SemanticsFlag.isHeader, ], )); await tester.tap(find.text('1')); // pushNamed('/A') await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(semantics, includesNodeWith( flags: <SemanticsFlag>[SemanticsFlag.scopesRoute], )); expect(semantics, includesNodeWith( label: 'Page 2', flags: <SemanticsFlag>[ SemanticsFlag.namesRoute, SemanticsFlag.isHeader, ], )); await tester.tap(find.text('2')); // pushNamed('/B/C') await tester.pump(); await tester.pump(const Duration(seconds: 1)); expect(semantics, includesNodeWith( flags: <SemanticsFlag>[ SemanticsFlag.scopesRoute, ], )); expect(semantics, includesNodeWith( label: 'Page 3', flags: <SemanticsFlag>[ SemanticsFlag.namesRoute, SemanticsFlag.isHeader, ], )); semantics.dispose(); }); testWidgets('arguments for named routes on Navigator', (WidgetTester tester) async { GlobalKey currentRouteKey; final List<Object> arguments = <Object>[]; await tester.pumpWidget(MaterialApp( onGenerateRoute: (RouteSettings settings) { arguments.add(settings.arguments); return MaterialPageRoute<void>( settings: settings, builder: (BuildContext context) => Center(key: currentRouteKey = GlobalKey(), child: Text(settings.name)), ); }, )); expect(find.text('/'), findsOneWidget); expect(arguments.single, isNull); arguments.clear(); Navigator.pushNamed( currentRouteKey.currentContext, '/A', arguments: 'pushNamed', ); await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('/A'), findsOneWidget); expect(arguments.single, 'pushNamed'); arguments.clear(); Navigator.popAndPushNamed( currentRouteKey.currentContext, '/B', arguments: 'popAndPushNamed', ); await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('/A'), findsNothing); expect(find.text('/B'), findsOneWidget); expect(arguments.single, 'popAndPushNamed'); arguments.clear(); Navigator.pushNamedAndRemoveUntil( currentRouteKey.currentContext, '/C', (Route<dynamic> route) => route.isFirst, arguments: 'pushNamedAndRemoveUntil', ); await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('/A'), findsNothing); expect(find.text('/B'), findsNothing); expect(find.text('/C'), findsOneWidget); expect(arguments.single, 'pushNamedAndRemoveUntil'); arguments.clear(); Navigator.pushReplacementNamed( currentRouteKey.currentContext, '/D', arguments: 'pushReplacementNamed', ); await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('/A'), findsNothing); expect(find.text('/B'), findsNothing); expect(find.text('/C'), findsNothing); expect(find.text('/D'), findsOneWidget); expect(arguments.single, 'pushReplacementNamed'); arguments.clear(); }); testWidgets('arguments for named routes on NavigatorState', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final List<Object> arguments = <Object>[]; await tester.pumpWidget(MaterialApp( navigatorKey: navigatorKey, onGenerateRoute: (RouteSettings settings) { arguments.add(settings.arguments); return MaterialPageRoute<void>( settings: settings, builder: (BuildContext context) => Center(child: Text(settings.name)), ); }, )); expect(find.text('/'), findsOneWidget); expect(arguments.single, isNull); arguments.clear(); navigatorKey.currentState.pushNamed( '/A', arguments:'pushNamed', ); await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('/A'), findsOneWidget); expect(arguments.single, 'pushNamed'); arguments.clear(); navigatorKey.currentState.popAndPushNamed( '/B', arguments: 'popAndPushNamed', ); await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('/A'), findsNothing); expect(find.text('/B'), findsOneWidget); expect(arguments.single, 'popAndPushNamed'); arguments.clear(); navigatorKey.currentState.pushNamedAndRemoveUntil( '/C', (Route<dynamic> route) => route.isFirst, arguments: 'pushNamedAndRemoveUntil', ); await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('/A'), findsNothing); expect(find.text('/B'), findsNothing); expect(find.text('/C'), findsOneWidget); expect(arguments.single, 'pushNamedAndRemoveUntil'); arguments.clear(); navigatorKey.currentState.pushReplacementNamed( '/D', arguments: 'pushReplacementNamed', ); await tester.pumpAndSettle(); expect(find.text('/'), findsNothing); expect(find.text('/A'), findsNothing); expect(find.text('/B'), findsNothing); expect(find.text('/C'), findsNothing); expect(find.text('/D'), findsOneWidget); expect(arguments.single, 'pushReplacementNamed'); arguments.clear(); }); testWidgets('Initial route can have gaps', (WidgetTester tester) async { final GlobalKey<NavigatorState> keyNav = GlobalKey<NavigatorState>(); const Key keyRoot = Key('Root'); const Key keyA = Key('A'); const Key keyABC = Key('ABC'); await tester.pumpWidget( MaterialApp( navigatorKey: keyNav, initialRoute: '/A/B/C', routes: <String, WidgetBuilder>{ '/': (BuildContext context) => Container(key: keyRoot), '/A': (BuildContext context) => Container(key: keyA), // The route /A/B is intentionally left out. '/A/B/C': (BuildContext context) => Container(key: keyABC), }, ), ); // The initial route /A/B/C should've been pushed successfully. expect(find.byKey(keyRoot, skipOffstage: false), findsOneWidget); expect(find.byKey(keyA, skipOffstage: false), findsOneWidget); expect(find.byKey(keyABC), findsOneWidget); keyNav.currentState.pop(); await tester.pumpAndSettle(); expect(find.byKey(keyRoot, skipOffstage: false), findsOneWidget); expect(find.byKey(keyA), findsOneWidget); expect(find.byKey(keyABC, skipOffstage: false), findsNothing); }); testWidgets('The full initial route has to be matched', (WidgetTester tester) async { final GlobalKey<NavigatorState> keyNav = GlobalKey<NavigatorState>(); const Key keyRoot = Key('Root'); const Key keyA = Key('A'); const Key keyAB = Key('AB'); await tester.pumpWidget( MaterialApp( navigatorKey: keyNav, initialRoute: '/A/B/C', routes: <String, WidgetBuilder>{ '/': (BuildContext context) => Container(key: keyRoot), '/A': (BuildContext context) => Container(key: keyA), '/A/B': (BuildContext context) => Container(key: keyAB), // The route /A/B/C is intentionally left out. }, ), ); final dynamic exception = tester.takeException(); expect(exception is String, isTrue); expect(exception.startsWith('Could not navigate to initial route.'), isTrue); // Only the root route should've been pushed. expect(find.byKey(keyRoot), findsOneWidget); expect(find.byKey(keyA), findsNothing); expect(find.byKey(keyAB), findsNothing); }); group('error control test', () { testWidgets('onUnknownRoute null and onGenerateRoute returns null', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget(Navigator( key: navigatorKey, onGenerateRoute: (_) => null, )); final dynamic exception = tester.takeException(); expect(exception, isNotNull); expect(exception, isFlutterError); final FlutterError error = exception; expect(error, isNotNull); expect(error.diagnostics.last, isInstanceOf<DiagnosticsProperty<NavigatorState>>()); expect( error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' If a Navigator has no onUnknownRoute, then its onGenerateRoute\n' ' must never return null.\n' ' When trying to build the route "/", onGenerateRoute returned\n' ' null, but there was no onUnknownRoute callback specified.\n' ' The Navigator was:\n' ' NavigatorState#4d6bf(lifecycle state: created)\n', ), ); }); testWidgets('onUnknownRoute null and onGenerateRoute returns null', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget(Navigator( key: navigatorKey, onGenerateRoute: (_) => null, onUnknownRoute: (_) => null, )); final dynamic exception = tester.takeException(); expect(exception, isNotNull); expect(exception, isFlutterError); final FlutterError error = exception; expect(error, isNotNull); expect(error.diagnostics.last, isInstanceOf<DiagnosticsProperty<NavigatorState>>()); expect( error.toStringDeep(), equalsIgnoringHashCodes( 'FlutterError\n' ' A Navigator\'s onUnknownRoute returned null.\n' ' When trying to build the route "/", both onGenerateRoute and\n' ' onUnknownRoute returned null. The onUnknownRoute callback should\n' ' never return null.\n' ' The Navigator was:\n' ' NavigatorState#38036(lifecycle state: created)\n', ), ); }); }); testWidgets('OverlayEntry of topmost initial route is marked as opaque', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/38038. final Key root = UniqueKey(); final Key intermediate = UniqueKey(); final GlobalKey topmost = GlobalKey(); await tester.pumpWidget( MaterialApp( initialRoute: '/A/B', routes: <String, WidgetBuilder>{ '/': (BuildContext context) => Container(key: root), '/A': (BuildContext context) => Container(key: intermediate), '/A/B': (BuildContext context) => Container(key: topmost), }, ), ); expect(ModalRoute.of(topmost.currentContext).overlayEntries.first.opaque, isTrue); expect(find.byKey(root), findsNothing); // hidden by opaque Route expect(find.byKey(intermediate), findsNothing); // hidden by opaque Route expect(find.byKey(topmost), findsOneWidget); }); testWidgets('OverlayEntry of topmost route is set to opaque after Push', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/38038. final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( navigatorKey: navigator, initialRoute: '/', onGenerateRoute: (RouteSettings settings) { return NoAnimationPageRoute( pageBuilder: (_) => Container(key: ValueKey<String>(settings.name)), ); }, ), ); expect(find.byKey(const ValueKey<String>('/')), findsOneWidget); navigator.currentState.pushNamed('/A'); await tester.pump(); final BuildContext topMostContext = tester.element(find.byKey(const ValueKey<String>('/A'))); expect(ModalRoute.of(topMostContext).overlayEntries.first.opaque, isTrue); expect(find.byKey(const ValueKey<String>('/')), findsNothing); // hidden by /A expect(find.byKey(const ValueKey<String>('/A')), findsOneWidget); }); testWidgets('OverlayEntry of topmost route is set to opaque after Replace', (WidgetTester tester) async { // Regression test for https://github.com/flutter/flutter/issues/38038. final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( navigatorKey: navigator, initialRoute: '/A/B', onGenerateRoute: (RouteSettings settings) { return NoAnimationPageRoute( pageBuilder: (_) => Container(key: ValueKey<String>(settings.name)), ); }, ), ); expect(find.byKey(const ValueKey<String>('/')), findsNothing); expect(find.byKey(const ValueKey<String>('/A')), findsNothing); expect(find.byKey(const ValueKey<String>('/A/B')), findsOneWidget); final Route<dynamic> oldRoute = ModalRoute.of( tester.element(find.byKey(const ValueKey<String>('/A'), skipOffstage: false)), ); final Route<void> newRoute = NoAnimationPageRoute( pageBuilder: (_) => Container(key: const ValueKey<String>('/C')), ); navigator.currentState.replace<void>(oldRoute: oldRoute, newRoute: newRoute); await tester.pump(); expect(newRoute.overlayEntries.first.opaque, isTrue); expect(find.byKey(const ValueKey<String>('/')), findsNothing); // hidden by /A/B expect(find.byKey(const ValueKey<String>('/A')), findsNothing); // replaced expect(find.byKey(const ValueKey<String>('/C')), findsNothing); // hidden by /A/B expect(find.byKey(const ValueKey<String>('/A/B')), findsOneWidget); navigator.currentState.pop(); await tester.pumpAndSettle(); expect(find.byKey(const ValueKey<String>('/')), findsNothing); // hidden by /C expect(find.byKey(const ValueKey<String>('/A')), findsNothing); // replaced expect(find.byKey(const ValueKey<String>('/A/B')), findsNothing); // popped expect(find.byKey(const ValueKey<String>('/C')), findsOneWidget); }); } class NoAnimationPageRoute extends PageRouteBuilder<void> { NoAnimationPageRoute({WidgetBuilder pageBuilder}) : super(pageBuilder: (BuildContext context, __, ___) { return pageBuilder(context); }); @override AnimationController createAnimationController() { return super.createAnimationController()..value = 1.0; } }