// Copyright 2014 The Flutter 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:collection'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; final List<String> results = <String>[]; Set<TestRoute> routes = HashSet<TestRoute>(); class TestRoute extends Route<String> with LocalHistoryRoute<String> { TestRoute(this.name); final String name; @override List<OverlayEntry> get overlayEntries => _entries; final List<OverlayEntry> _entries = <OverlayEntry>[]; void log(String s) { results.add('$name: $s'); } @override void install() { log('install'); final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) => Container(), opaque: true, ); _entries.add(entry); routes.add(this); super.install(); } @override TickerFuture didPush() { log('didPush'); return super.didPush(); } @override void didAdd() { log('didAdd'); super.didAdd(); } @override void didReplace(Route<dynamic> oldRoute) { expect(oldRoute, isA<TestRoute>()); final TestRoute castRoute = oldRoute as TestRoute; log('didReplace ${castRoute.name}'); super.didReplace(castRoute); } @override bool didPop(String result) { log('didPop $result'); bool returnValue; if (returnValue = super.didPop(result)) navigator.finalizeRoute(this); return returnValue; } @override void didPopNext(Route<dynamic> nextRoute) { expect(nextRoute, isA<TestRoute>()); final TestRoute castRoute = nextRoute as TestRoute; log('didPopNext ${castRoute.name}'); super.didPopNext(castRoute); } @override void didChangeNext(Route<dynamic> nextRoute) { expect(nextRoute, anyOf(isNull, isA<TestRoute>())); final TestRoute castRoute = nextRoute as TestRoute; log('didChangeNext ${castRoute?.name}'); super.didChangeNext(castRoute); } @override void dispose() { log('dispose'); _entries.clear(); routes.remove(this); super.dispose(); } } Future<void> runNavigatorTest( WidgetTester tester, NavigatorState host, VoidCallback test, List<String> expectations, ) async { expect(host, isNotNull); test(); expect(results, equals(expectations)); results.clear(); await tester.pump(); } void main() { testWidgets('Route settings', (WidgetTester tester) async { const RouteSettings settings = RouteSettings(name: 'A'); expect(settings, hasOneLineDescription); final RouteSettings settings2 = settings.copyWith(name: 'B'); expect(settings2.name, 'B'); }); testWidgets('Route settings arguments', (WidgetTester tester) async { const RouteSettings settings = RouteSettings(name: 'A'); expect(settings.arguments, isNull); final Object arguments = Object(); final RouteSettings settings2 = RouteSettings(name: 'A', arguments: arguments); expect(settings2.arguments, same(arguments)); final RouteSettings settings3 = settings2.copyWith(); expect(settings3.arguments, equals(arguments)); final Object arguments2 = Object(); final RouteSettings settings4 = settings2.copyWith(arguments: arguments2); expect(settings4.arguments, same(arguments2)); expect(settings4.arguments, isNot(same(arguments))); }); testWidgets('Route management - push, replace, pop sequence', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Navigator( key: navigatorKey, onGenerateRoute: (_) => TestRoute('initial'), ), ), ); final NavigatorState host = navigatorKey.currentState; await runNavigatorTest( tester, host, () { }, <String>[ 'initial: install', 'initial: didAdd', 'initial: didChangeNext null', ], ); TestRoute second; await runNavigatorTest( tester, host, () { host.push(second = TestRoute('second')); }, <String>[ // stack is: initial, second 'second: install', 'second: didPush', 'second: didChangeNext null', 'initial: didChangeNext second', ], ); await runNavigatorTest( tester, host, () { host.push(TestRoute('third')); }, <String>[ // stack is: initial, second, third 'third: install', 'third: didPush', 'third: didChangeNext null', 'second: didChangeNext third', ], ); await runNavigatorTest( tester, host, () { host.replace(oldRoute: second, newRoute: TestRoute('two')); }, <String>[ // stack is: initial, two, third 'two: install', 'two: didReplace second', 'two: didChangeNext third', 'initial: didChangeNext two', 'second: dispose', ], ); await runNavigatorTest( tester, host, () { host.pop('hello'); }, <String>[ // stack is: initial, two 'third: didPop hello', 'two: didPopNext third', 'third: dispose', ], ); await runNavigatorTest( tester, host, () { host.pop('good bye'); }, <String>[ // stack is: initial 'two: didPop good bye', 'initial: didPopNext two', 'two: dispose', ], ); await tester.pumpWidget(Container()); expect(results, equals(<String>['initial: dispose'])); expect(routes.isEmpty, isTrue); results.clear(); }); testWidgets('Route management - push, remove, pop', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Navigator( key: navigatorKey, onGenerateRoute: (_) => TestRoute('first'), ), ), ); final NavigatorState host = navigatorKey.currentState; await runNavigatorTest( tester, host, () { }, <String>[ 'first: install', 'first: didAdd', 'first: didChangeNext null', ], ); TestRoute second; await runNavigatorTest( tester, host, () { host.push(second = TestRoute('second')); }, <String>[ 'second: install', 'second: didPush', 'second: didChangeNext null', 'first: didChangeNext second', ], ); await runNavigatorTest( tester, host, () { host.push(TestRoute('third')); }, <String>[ 'third: install', 'third: didPush', 'third: didChangeNext null', 'second: didChangeNext third', ], ); await runNavigatorTest( tester, host, () { host.removeRouteBelow(second); }, <String>[ 'first: dispose', ], ); await runNavigatorTest( tester, host, () { host.pop('good bye'); }, <String>[ 'third: didPop good bye', 'second: didPopNext third', 'third: dispose', ], ); await runNavigatorTest( tester, host, () { host.push(TestRoute('three')); }, <String>[ 'three: install', 'three: didPush', 'three: didChangeNext null', 'second: didChangeNext three', ], ); TestRoute four; await runNavigatorTest( tester, host, () { host.push(four = TestRoute('four')); }, <String>[ 'four: install', 'four: didPush', 'four: didChangeNext null', 'three: didChangeNext four', ], ); await runNavigatorTest( tester, host, () { host.removeRouteBelow(four); }, <String>[ 'second: didChangeNext four', 'three: dispose', ], ); await runNavigatorTest( tester, host, () { host.pop('the end'); }, <String>[ 'four: didPop the end', 'second: didPopNext four', 'four: dispose', ], ); await tester.pumpWidget(Container()); expect(results, equals(<String>['second: dispose'])); expect(routes.isEmpty, isTrue); results.clear(); }); testWidgets('Route management - push, replace, popUntil', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Navigator( key: navigatorKey, onGenerateRoute: (_) => TestRoute('A'), ), ), ); final NavigatorState host = navigatorKey.currentState; await runNavigatorTest( tester, host, () { }, <String>[ 'A: install', 'A: didAdd', 'A: didChangeNext null', ], ); await runNavigatorTest( tester, host, () { host.push(TestRoute('B')); }, <String>[ 'B: install', 'B: didPush', 'B: didChangeNext null', 'A: didChangeNext B', ], ); TestRoute routeC; await runNavigatorTest( tester, host, () { host.push(routeC = TestRoute('C')); }, <String>[ 'C: install', 'C: didPush', 'C: didChangeNext null', 'B: didChangeNext C', ], ); expect(routeC.isActive, isTrue); TestRoute routeB; await runNavigatorTest( tester, host, () { host.replaceRouteBelow(anchorRoute: routeC, newRoute: routeB = TestRoute('b')); }, <String>[ 'b: install', 'b: didReplace B', 'b: didChangeNext C', 'A: didChangeNext b', 'B: dispose', ], ); await runNavigatorTest( tester, host, () { host.popUntil((Route<dynamic> route) => route == routeB); }, <String>[ 'C: didPop null', 'b: didPopNext C', 'C: dispose', ], ); await tester.pumpWidget(Container()); expect(results, equals(<String>['A: dispose', 'b: dispose'])); expect(routes.isEmpty, isTrue); results.clear(); }); testWidgets('Route localHistory - popUntil', (WidgetTester tester) async { final TestRoute routeA = TestRoute('A'); routeA.addLocalHistoryEntry(LocalHistoryEntry( onRemove: () { routeA.log('onRemove 0'); } )); routeA.addLocalHistoryEntry(LocalHistoryEntry( onRemove: () { routeA.log('onRemove 1'); } )); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: Navigator( key: navigatorKey, onGenerateRoute: (_) => routeA, ), ), ); final NavigatorState host = navigatorKey.currentState; await runNavigatorTest( tester, host, () { host.popUntil((Route<dynamic> route) => !route.willHandlePopInternally); }, <String>[ 'A: install', 'A: didAdd', 'A: didChangeNext null', 'A: didPop null', 'A: onRemove 1', 'A: didPop null', 'A: onRemove 0', ], ); await runNavigatorTest( tester, host, () { host.popUntil((Route<dynamic> route) => !route.willHandlePopInternally); }, <String>[ ], ); }); group('PageRouteObserver', () { test('calls correct listeners', () { final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>(); final RouteAware pageRouteAware1 = MockRouteAware(); final MockPageRoute route1 = MockPageRoute(); observer.subscribe(pageRouteAware1, route1); verify(pageRouteAware1.didPush()).called(1); final RouteAware pageRouteAware2 = MockRouteAware(); final MockPageRoute route2 = MockPageRoute(); observer.didPush(route2, route1); verify(pageRouteAware1.didPushNext()).called(1); observer.subscribe(pageRouteAware2, route2); verify(pageRouteAware2.didPush()).called(1); observer.didPop(route2, route1); verify(pageRouteAware2.didPop()).called(1); verify(pageRouteAware1.didPopNext()).called(1); }); test('does not call listeners for non-PageRoute', () { final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>(); final RouteAware pageRouteAware = MockRouteAware(); final MockPageRoute pageRoute = MockPageRoute(); final MockRoute route = MockRoute(); observer.subscribe(pageRouteAware, pageRoute); verify(pageRouteAware.didPush()); observer.didPush(route, pageRoute); observer.didPop(route, pageRoute); verifyNoMoreInteractions(pageRouteAware); }); test('does not call listeners when already subscribed', () { final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>(); final RouteAware pageRouteAware = MockRouteAware(); final MockPageRoute pageRoute = MockPageRoute(); observer.subscribe(pageRouteAware, pageRoute); observer.subscribe(pageRouteAware, pageRoute); verify(pageRouteAware.didPush()).called(1); }); test('does not call listeners when unsubscribed', () { final RouteObserver<PageRoute<dynamic>> observer = RouteObserver<PageRoute<dynamic>>(); final RouteAware pageRouteAware = MockRouteAware(); final MockPageRoute pageRoute = MockPageRoute(); final MockPageRoute nextPageRoute = MockPageRoute(); observer.subscribe(pageRouteAware, pageRoute); observer.subscribe(pageRouteAware, nextPageRoute); verify(pageRouteAware.didPush()).called(2); observer.unsubscribe(pageRouteAware); observer.didPush(nextPageRoute, pageRoute); observer.didPop(nextPageRoute, pageRoute); verifyNoMoreInteractions(pageRouteAware); }); }); testWidgets('Can autofocus a TextField nested in a Focus in a route.', (WidgetTester tester) async { final TextEditingController controller = TextEditingController(); final FocusNode focusNode = FocusNode(debugLabel: 'Test Node'); await tester.pumpWidget( Material( child: MaterialApp( onGenerateRoute: (RouteSettings settings) { return PageRouteBuilder<void>( settings: settings, pageBuilder: (BuildContext context, Animation<double> input, Animation<double> out) { return Focus( child: TextField( autofocus: true, focusNode: focusNode, controller: controller, ), ); }, ); }, ), ), ); await tester.pump(); expect(focusNode.hasPrimaryFocus, isTrue); }); group('TransitionRoute', () { testWidgets('secondary animation is kDismissed when next route finishes pop', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( navigatorKey: navigator, home: const Text('home'), ) ); // Push page one, its secondary animation is kAlwaysDismissedAnimation. ProxyAnimation secondaryAnimationProxyPageOne; ProxyAnimation animationPageOne; navigator.currentState.push( PageRouteBuilder<void>( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation; animationPageOne = animation as ProxyAnimation; return const Text('Page One'); }, ), ); await tester.pump(); await tester.pumpAndSettle(); final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent as ProxyAnimation; expect(animationPageOne.value, 1.0); expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); // Push page two, the secondary animation of page one is the primary // animation of page two. ProxyAnimation secondaryAnimationProxyPageTwo; ProxyAnimation animationPageTwo; navigator.currentState.push( PageRouteBuilder<void>( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { secondaryAnimationProxyPageTwo = secondaryAnimation as ProxyAnimation; animationPageTwo = animation as ProxyAnimation; return const Text('Page Two'); }, ), ); await tester.pump(); await tester.pumpAndSettle(); final ProxyAnimation secondaryAnimationPageTwo = secondaryAnimationProxyPageTwo.parent as ProxyAnimation; expect(animationPageTwo.value, 1.0); expect(secondaryAnimationPageTwo.parent, kAlwaysDismissedAnimation); expect(secondaryAnimationPageOne.parent, animationPageTwo.parent); // Pop page two, the secondary animation of page one becomes // kAlwaysDismissedAnimation. navigator.currentState.pop(); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(secondaryAnimationPageOne.parent, animationPageTwo.parent); await tester.pumpAndSettle(); expect(animationPageTwo.value, 0.0); expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); }); testWidgets('secondary animation is kDismissed when next route is removed', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( navigatorKey: navigator, home: const Text('home'), ) ); // Push page one, its secondary animation is kAlwaysDismissedAnimation. ProxyAnimation secondaryAnimationProxyPageOne; ProxyAnimation animationPageOne; navigator.currentState.push( PageRouteBuilder<void>( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation; animationPageOne = animation as ProxyAnimation; return const Text('Page One'); }, ), ); await tester.pump(); await tester.pumpAndSettle(); final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent as ProxyAnimation; expect(animationPageOne.value, 1.0); expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); // Push page two, the secondary animation of page one is the primary // animation of page two. ProxyAnimation secondaryAnimationProxyPageTwo; ProxyAnimation animationPageTwo; Route<void> secondRoute; navigator.currentState.push( secondRoute = PageRouteBuilder<void>( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { secondaryAnimationProxyPageTwo = secondaryAnimation as ProxyAnimation; animationPageTwo = animation as ProxyAnimation; return const Text('Page Two'); }, ), ); await tester.pump(); await tester.pumpAndSettle(); final ProxyAnimation secondaryAnimationPageTwo = secondaryAnimationProxyPageTwo.parent as ProxyAnimation; expect(animationPageTwo.value, 1.0); expect(secondaryAnimationPageTwo.parent, kAlwaysDismissedAnimation); expect(secondaryAnimationPageOne.parent, animationPageTwo.parent); // Remove the second route, the secondary animation of page one is // kAlwaysDismissedAnimation again. navigator.currentState.removeRoute(secondRoute); await tester.pump(); expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); }); testWidgets('secondary animation is kDismissed after train hopping finishes and pop', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( navigatorKey: navigator, home: const Text('home'), ) ); // Push page one, its secondary animation is kAlwaysDismissedAnimation. ProxyAnimation secondaryAnimationProxyPageOne; ProxyAnimation animationPageOne; navigator.currentState.push( PageRouteBuilder<void>( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation; animationPageOne = animation as ProxyAnimation; return const Text('Page One'); }, ), ); await tester.pump(); await tester.pumpAndSettle(); final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent as ProxyAnimation; expect(animationPageOne.value, 1.0); expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); // Push page two, the secondary animation of page one is the primary // animation of page two. ProxyAnimation animationPageTwo; navigator.currentState.push( PageRouteBuilder<void>( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { animationPageTwo = animation as ProxyAnimation; return const Text('Page Two'); }, ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(secondaryAnimationPageOne.parent, animationPageTwo.parent); // Replace with a different route while push is ongoing to trigger // TrainHopping. ProxyAnimation animationPageThree; navigator.currentState.pushReplacement( TestPageRouteBuilder( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { animationPageThree = animation as ProxyAnimation; return const Text('Page Three'); }, ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 1)); expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>()); final TrainHoppingAnimation trainHopper = secondaryAnimationPageOne.parent as TrainHoppingAnimation; expect(trainHopper.currentTrain, animationPageTwo.parent); await tester.pump(const Duration(milliseconds: 100)); expect(secondaryAnimationPageOne.parent, isNot(isA<TrainHoppingAnimation>())); expect(secondaryAnimationPageOne.parent, animationPageThree.parent); expect(trainHopper.currentTrain, isNull); // Has been disposed. await tester.pumpAndSettle(); expect(secondaryAnimationPageOne.parent, animationPageThree.parent); // Pop page three. navigator.currentState.pop(); await tester.pump(); await tester.pumpAndSettle(); expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); }); testWidgets('secondary animation is kDismissed when train hopping is interrupted', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); await tester.pumpWidget( MaterialApp( navigatorKey: navigator, home: const Text('home'), ) ); // Push page one, its secondary animation is kAlwaysDismissedAnimation. ProxyAnimation secondaryAnimationProxyPageOne; ProxyAnimation animationPageOne; navigator.currentState.push( PageRouteBuilder<void>( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { secondaryAnimationProxyPageOne = secondaryAnimation as ProxyAnimation; animationPageOne = animation as ProxyAnimation; return const Text('Page One'); }, ), ); await tester.pump(); await tester.pumpAndSettle(); final ProxyAnimation secondaryAnimationPageOne = secondaryAnimationProxyPageOne.parent as ProxyAnimation; expect(animationPageOne.value, 1.0); expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); // Push page two, the secondary animation of page one is the primary // animation of page two. ProxyAnimation animationPageTwo; navigator.currentState.push( PageRouteBuilder<void>( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { animationPageTwo = animation as ProxyAnimation; return const Text('Page Two'); }, ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(secondaryAnimationPageOne.parent, animationPageTwo.parent); // Replace with a different route while push is ongoing to trigger // TrainHopping. navigator.currentState.pushReplacement( TestPageRouteBuilder( pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { return const Text('Page Three'); }, ), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 10)); expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>()); final TrainHoppingAnimation trainHopper = secondaryAnimationPageOne.parent as TrainHoppingAnimation; expect(trainHopper.currentTrain, animationPageTwo.parent); // Pop page three while replacement push is ongoing. navigator.currentState.pop(); await tester.pump(); expect(secondaryAnimationPageOne.parent, isA<TrainHoppingAnimation>()); final TrainHoppingAnimation trainHopper2 = secondaryAnimationPageOne.parent as TrainHoppingAnimation; expect(trainHopper2.currentTrain, animationPageTwo.parent); expect(trainHopper.currentTrain, isNull); // Has been disposed. await tester.pumpAndSettle(); expect(secondaryAnimationPageOne.parent, kAlwaysDismissedAnimation); expect(trainHopper2.currentTrain, isNull); // Has been disposed. }); testWidgets('secondary animation is triggered when pop initial route', (WidgetTester tester) async { final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>(); Animation<double> secondaryAnimationOfRouteOne; Animation<double> primaryAnimationOfRouteTwo; await tester.pumpWidget( MaterialApp( navigatorKey: navigator, onGenerateRoute: (RouteSettings settings) { return PageRouteBuilder<void>( settings: settings, pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) { if (settings.name == '/') secondaryAnimationOfRouteOne = secondaryAnimation; else primaryAnimationOfRouteTwo = animation; return const Text('Page'); }, ); }, initialRoute: '/a', ) ); // The secondary animation of the bottom route should be chained with the // primary animation of top most route. expect(secondaryAnimationOfRouteOne.value, 1.0); expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value); // Pops the top most route and verifies two routes are still chained. navigator.currentState.pop(); await tester.pump(); await tester.pump(const Duration(milliseconds: 30)); expect(secondaryAnimationOfRouteOne.value, 0.9); expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value); await tester.pumpAndSettle(); expect(secondaryAnimationOfRouteOne.value, 0.0); expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value); }); testWidgets('showGeneralDialog uses root navigator by default', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); await tester.pumpWidget(MaterialApp( navigatorObservers: <NavigatorObserver>[rootObserver], home: Navigator( observers: <NavigatorObserver>[nestedObserver], onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<dynamic>( builder: (BuildContext context) { return RaisedButton( onPressed: () { showGeneralDialog<void>( context: context, barrierDismissible: false, transitionDuration: Duration.zero, pageBuilder: (BuildContext innerContext, _, __) { return const SizedBox(); }, ); }, child: const Text('Show Dialog'), ); }, ); }, ), )); // Open the dialog. await tester.tap(find.byType(RaisedButton)); expect(rootObserver.dialogCount, 1); expect(nestedObserver.dialogCount, 0); }); testWidgets('showGeneralDialog uses nested navigator if useRootNavigator is false', (WidgetTester tester) async { final DialogObserver rootObserver = DialogObserver(); final DialogObserver nestedObserver = DialogObserver(); await tester.pumpWidget(MaterialApp( navigatorObservers: <NavigatorObserver>[rootObserver], home: Navigator( observers: <NavigatorObserver>[nestedObserver], onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<dynamic>( builder: (BuildContext context) { return RaisedButton( onPressed: () { showGeneralDialog<void>( useRootNavigator: false, context: context, barrierDismissible: false, transitionDuration: Duration.zero, pageBuilder: (BuildContext innerContext, _, __) { return const SizedBox(); }, ); }, child: const Text('Show Dialog'), ); }, ); }, ), )); // Open the dialog. await tester.tap(find.byType(RaisedButton)); expect(rootObserver.dialogCount, 0); expect(nestedObserver.dialogCount, 1); }); testWidgets('reverseTransitionDuration defaults to transitionDuration', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); // Default MaterialPageRoute transition duration should be 300ms. await tester.pumpWidget(MaterialApp( onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<dynamic>( builder: (BuildContext context) { return RaisedButton( onPressed: () { Navigator.of(context).push<void>( MaterialPageRoute<void>( builder: (BuildContext innerContext) { return Container( key: containerKey, color: Colors.green, ); }, ), ); }, child: const Text('Open page'), ); }, ); }, )); // Open the new route. await tester.tap(find.byType(RaisedButton)); await tester.pumpAndSettle(); expect(find.text('Open page'), findsNothing); expect(find.byKey(containerKey), findsOneWidget); // Pop the new route. tester.state<NavigatorState>(find.byType(Navigator)).pop(); await tester.pump(); expect(find.byKey(containerKey), findsOneWidget); // Container should be present halfway through the transition. await tester.pump(const Duration(milliseconds: 150)); expect(find.byKey(containerKey), findsOneWidget); // Container should be present at the very end of the transition. await tester.pump(const Duration(milliseconds: 150)); expect(find.byKey(containerKey), findsOneWidget); // Container have transitioned out after 300ms. await tester.pump(const Duration(milliseconds: 1)); expect(find.byKey(containerKey), findsNothing); }); testWidgets('reverseTransitionDuration can be customized', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); await tester.pumpWidget(MaterialApp( onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<dynamic>( builder: (BuildContext context) { return RaisedButton( onPressed: () { Navigator.of(context).push<void>( ModifiedReverseTransitionDurationRoute<void>( builder: (BuildContext innerContext) { return Container( key: containerKey, color: Colors.green, ); }, // modified value, default MaterialPageRoute transition duration should be 300ms. reverseTransitionDuration: const Duration(milliseconds: 150), ), ); }, child: const Text('Open page'), ); }, ); }, )); // Open the new route. await tester.tap(find.byType(RaisedButton)); await tester.pumpAndSettle(); expect(find.text('Open page'), findsNothing); expect(find.byKey(containerKey), findsOneWidget); // Pop the new route. tester.state<NavigatorState>(find.byType(Navigator)).pop(); await tester.pump(); expect(find.byKey(containerKey), findsOneWidget); // Container should be present halfway through the transition. await tester.pump(const Duration(milliseconds: 75)); expect(find.byKey(containerKey), findsOneWidget); // Container should be present at the very end of the transition. await tester.pump(const Duration(milliseconds: 75)); expect(find.byKey(containerKey), findsOneWidget); // Container have transitioned out after 150ms. await tester.pump(const Duration(milliseconds: 1)); expect(find.byKey(containerKey), findsNothing); }); testWidgets('custom reverseTransitionDuration does not result in interrupted animations', (WidgetTester tester) async { final GlobalKey containerKey = GlobalKey(); await tester.pumpWidget(MaterialApp( theme: ThemeData( pageTransitionsTheme: const PageTransitionsTheme( builders: <TargetPlatform, PageTransitionsBuilder>{ TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(), // use a fade transition }, ), ), onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute<dynamic>( builder: (BuildContext context) { return RaisedButton( onPressed: () { Navigator.of(context).push<void>( ModifiedReverseTransitionDurationRoute<void>( builder: (BuildContext innerContext) { return Container( key: containerKey, color: Colors.green, ); }, // modified value, default MaterialPageRoute transition duration should be 300ms. reverseTransitionDuration: const Duration(milliseconds: 150), ), ); }, child: const Text('Open page'), ); }, ); }, )); // Open the new route. await tester.tap(find.byType(RaisedButton)); await tester.pump(); await tester.pump(const Duration(milliseconds: 200)); // jump partway through the forward transition expect(find.byKey(containerKey), findsOneWidget); // Gets the opacity of the fade transition while animating forwards. final double topFadeTransitionOpacity = _getOpacity(containerKey, tester); // Pop the new route mid-transition. tester.state<NavigatorState>(find.byType(Navigator)).pop(); await tester.pump(); // Transition should not jump. In other words, the fade transition // opacity before and after animation changes directions should remain // the same. expect(_getOpacity(containerKey, tester), topFadeTransitionOpacity); // Reverse transition duration should be: // Forward transition elapsed time: 200ms / 300ms = 2 / 3 // Reverse transition remaining time: 150ms * 2 / 3 = 100ms // Container should be present at the very end of the transition. await tester.pump(const Duration(milliseconds: 100)); expect(find.byKey(containerKey), findsOneWidget); // Container have transitioned out after 100ms. await tester.pump(const Duration(milliseconds: 1)); expect(find.byKey(containerKey), findsNothing); }); }); group('ModalRoute', () { testWidgets('default barrierCurve', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Builder( builder: (BuildContext context) { return Center( child: RaisedButton( child: const Text('X'), onPressed: () { Navigator.of(context).push<void>( _TestDialogRouteWithCustomBarrierCurve<void>( child: const Text('Hello World'), ) ); }, ), ); } ), ), )); final CurveTween _defaultBarrierTween = CurveTween(curve: Curves.ease); int _getExpectedBarrierTweenAlphaValue(double t) { return Color.getAlphaFromOpacity(_defaultBarrierTween.transform(t)); } await tester.tap(find.text('X')); await tester.pump(); final Finder animatedModalBarrier = find.byType(AnimatedModalBarrier); expect(animatedModalBarrier, findsOneWidget); Animation<Color> modalBarrierAnimation; modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect(modalBarrierAnimation.value, Colors.transparent); await tester.pump(const Duration(milliseconds: 25)); modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect( modalBarrierAnimation.value.alpha, closeTo(_getExpectedBarrierTweenAlphaValue(0.25), 1.0), ); await tester.pump(const Duration(milliseconds: 25)); modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect( modalBarrierAnimation.value.alpha, closeTo(_getExpectedBarrierTweenAlphaValue(0.50), 1.0), ); await tester.pump(const Duration(milliseconds: 25)); modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect( modalBarrierAnimation.value.alpha, closeTo(_getExpectedBarrierTweenAlphaValue(0.75), 1.0), ); await tester.pumpAndSettle(); modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect(modalBarrierAnimation.value, Colors.black); }); testWidgets('custom barrierCurve', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Material( child: Builder( builder: (BuildContext context) { return Center( child: RaisedButton( child: const Text('X'), onPressed: () { Navigator.of(context).push<void>( _TestDialogRouteWithCustomBarrierCurve<void>( child: const Text('Hello World'), barrierCurve: Curves.linear, ), ); }, ), ); }, ), ), )); final CurveTween _customBarrierTween = CurveTween(curve: Curves.linear); int _getExpectedBarrierTweenAlphaValue(double t) { return Color.getAlphaFromOpacity(_customBarrierTween.transform(t)); } await tester.tap(find.text('X')); await tester.pump(); final Finder animatedModalBarrier = find.byType(AnimatedModalBarrier); expect(animatedModalBarrier, findsOneWidget); Animation<Color> modalBarrierAnimation; modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect(modalBarrierAnimation.value, Colors.transparent); await tester.pump(const Duration(milliseconds: 25)); modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect( modalBarrierAnimation.value.alpha, closeTo(_getExpectedBarrierTweenAlphaValue(0.25), 1.0), ); await tester.pump(const Duration(milliseconds: 25)); modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect( modalBarrierAnimation.value.alpha, closeTo(_getExpectedBarrierTweenAlphaValue(0.50), 1.0), ); await tester.pump(const Duration(milliseconds: 25)); modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect( modalBarrierAnimation.value.alpha, closeTo(_getExpectedBarrierTweenAlphaValue(0.75), 1.0), ); await tester.pumpAndSettle(); modalBarrierAnimation = tester.widget<AnimatedModalBarrier>(animatedModalBarrier).color; expect(modalBarrierAnimation.value, Colors.black); }); testWidgets('focus traverse correct when pop mutiple page simultaneously', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/48903 final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget(MaterialApp( navigatorKey: navigatorKey, home: const Text('dummy1'), )); final Element textOnPageOne = tester.element(find.text('dummy1')); final FocusScopeNode focusNodeOnPageOne = FocusScope.of(textOnPageOne); expect(focusNodeOnPageOne.hasFocus, isTrue); // Pushes one page. navigatorKey.currentState.push<void>( MaterialPageRoute<void>( builder: (BuildContext context) => const Text('dummy2'), ) ); await tester.pumpAndSettle(); final Element textOnPageTwo = tester.element(find.text('dummy2')); final FocusScopeNode focusNodeOnPageTwo = FocusScope.of(textOnPageTwo); // The focus should be on second page. expect(focusNodeOnPageOne.hasFocus, isFalse); expect(focusNodeOnPageTwo.hasFocus, isTrue); // Pushes another page. navigatorKey.currentState.push<void>( MaterialPageRoute<void>( builder: (BuildContext context) => const Text('dummy3'), ) ); await tester.pumpAndSettle(); final Element textOnPageThree = tester.element(find.text('dummy3')); final FocusScopeNode focusNodeOnPageThree = FocusScope.of(textOnPageThree); // The focus should be on third page. expect(focusNodeOnPageOne.hasFocus, isFalse); expect(focusNodeOnPageTwo.hasFocus, isFalse); expect(focusNodeOnPageThree.hasFocus, isTrue); // Pops two pages simultaneously. navigatorKey.currentState.popUntil((Route<void> route) => route.isFirst); await tester.pumpAndSettle(); // It should refocus page one after pops. expect(focusNodeOnPageOne.hasFocus, isTrue); }); testWidgets('focus traversal is correct when popping mutiple pages simultaneously - with focused children', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/48903 final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); await tester.pumpWidget(MaterialApp( navigatorKey: navigatorKey, home: const Text('dummy1'), )); final Element textOnPageOne = tester.element(find.text('dummy1')); final FocusScopeNode focusNodeOnPageOne = FocusScope.of(textOnPageOne); expect(focusNodeOnPageOne.hasFocus, isTrue); // Pushes one page. navigatorKey.currentState.push<void>( MaterialPageRoute<void>( builder: (BuildContext context) => const Material(child: TextField()), ) ); await tester.pumpAndSettle(); final Element textOnPageTwo = tester.element(find.byType(TextField)); final FocusScopeNode focusNodeOnPageTwo = FocusScope.of(textOnPageTwo); // The focus should be on second page. expect(focusNodeOnPageOne.hasFocus, isFalse); expect(focusNodeOnPageTwo.hasFocus, isTrue); // Move the focus to another node. focusNodeOnPageTwo.nextFocus(); await tester.pumpAndSettle(); expect(focusNodeOnPageTwo.hasFocus, isTrue); expect(focusNodeOnPageTwo.hasPrimaryFocus, isFalse); // Pushes another page. navigatorKey.currentState.push<void>( MaterialPageRoute<void>( builder: (BuildContext context) => const Text('dummy3'), ) ); await tester.pumpAndSettle(); final Element textOnPageThree = tester.element(find.text('dummy3')); final FocusScopeNode focusNodeOnPageThree = FocusScope.of(textOnPageThree); // The focus should be on third page. expect(focusNodeOnPageOne.hasFocus, isFalse); expect(focusNodeOnPageTwo.hasFocus, isFalse); expect(focusNodeOnPageThree.hasFocus, isTrue); // Pops two pages simultaneously. navigatorKey.currentState.popUntil((Route<void> route) => route.isFirst); await tester.pumpAndSettle(); // It should refocus page one after pops. expect(focusNodeOnPageOne.hasFocus, isTrue); }); testWidgets('child with local history can be disposed', (WidgetTester tester) async { // Regression test: https://github.com/flutter/flutter/issues/52478 await tester.pumpWidget(MaterialApp( home: WidgetWithLocalHistory(), )); final WidgetWithLocalHistoryState state = tester.state(find.byType(WidgetWithLocalHistory)); state.addLocalHistory(); // Waits for modal route to update its internal state; await tester.pump(); // Pumps a new widget to dispose WidgetWithLocalHistory. This should cause // it to remove the local history entry from modal route during // finalizeTree. await tester.pumpWidget(const MaterialApp( home: Text('dummy'), )); // Waits for modal route to update its internal state; await tester.pump(); expect(tester.takeException(), null); }); }); } double _getOpacity(GlobalKey key, WidgetTester tester) { final Finder finder = find.ancestor( of: find.byKey(key), matching: find.byType(FadeTransition), ); return tester.widgetList(finder).fold<double>(1.0, (double a, Widget widget) { final FadeTransition transition = widget as FadeTransition; return a * transition.opacity.value; }); } class ModifiedReverseTransitionDurationRoute<T> extends MaterialPageRoute<T> { ModifiedReverseTransitionDurationRoute({ @required WidgetBuilder builder, RouteSettings settings, this.reverseTransitionDuration, bool fullscreenDialog = false, }) : super( builder: builder, settings: settings, fullscreenDialog: fullscreenDialog, ); @override final Duration reverseTransitionDuration; } class MockPageRoute extends Mock implements PageRoute<dynamic> { } class MockRoute extends Mock implements Route<dynamic> { } class MockRouteAware extends Mock implements RouteAware { } class TestPageRouteBuilder extends PageRouteBuilder<void> { TestPageRouteBuilder({RoutePageBuilder pageBuilder}) : super(pageBuilder: pageBuilder); @override Animation<double> createAnimation() { return CurvedAnimation(parent: super.createAnimation(), curve: Curves.easeOutExpo); } } class DialogObserver extends NavigatorObserver { int dialogCount = 0; @override void didPush(Route<dynamic> route, Route<dynamic> previousRoute) { if (route.toString().contains('_DialogRoute')) { dialogCount++; } super.didPush(route, previousRoute); } } class _TestDialogRouteWithCustomBarrierCurve<T> extends PopupRoute<T> { _TestDialogRouteWithCustomBarrierCurve({ @required Widget child, Curve barrierCurve, }) : _barrierCurve = barrierCurve, _child = child; final Widget _child; @override bool get barrierDismissible => true; @override String get barrierLabel => null; @override Color get barrierColor => Colors.black; // easier value to test against @override Curve get barrierCurve { if (_barrierCurve == null) { return super.barrierCurve; } return _barrierCurve; } final Curve _barrierCurve; @override Duration get transitionDuration => const Duration(milliseconds: 100); // easier value to test against @override Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return Semantics( child: _child, scopesRoute: true, explicitChildNodes: true, ); } } class WidgetWithLocalHistory extends StatefulWidget { @override WidgetWithLocalHistoryState createState() => WidgetWithLocalHistoryState(); } class WidgetWithLocalHistoryState extends State<WidgetWithLocalHistory> { LocalHistoryEntry _localHistory; void addLocalHistory() { final ModalRoute<dynamic> route = ModalRoute.of(context); _localHistory = LocalHistoryEntry(); route.addLocalHistoryEntry(_localHistory); } @override void dispose() { super.dispose(); _localHistory.remove(); } @override Widget build(BuildContext context) { return const Text('dummy'); } }