// 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:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/widgets.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(OverlayEntry insertionPoint) { log('install'); final OverlayEntry entry = OverlayEntry( builder: (BuildContext context) => Container(), opaque: true, ); _entries.add(entry); navigator.overlay?.insert(entry, above: insertionPoint); routes.add(this); super.install(insertionPoint); } @override TickerFuture didPush() { log('didPush'); return super.didPush(); } @override void didReplace(Route<dynamic> oldRoute) { expect(oldRoute, isInstanceOf<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, isInstanceOf<TestRoute>()); final TestRoute castRoute = nextRoute as TestRoute; log('didPopNext ${castRoute.name}'); super.didPopNext(castRoute); } @override void didChangeNext(Route<dynamic> nextRoute) { expect(nextRoute, anyOf(isNull, isInstanceOf<TestRoute>())); final TestRoute castRoute = nextRoute as TestRoute; log('didChangeNext ${castRoute?.name}'); super.didChangeNext(castRoute); } @override void dispose() { log('dispose'); for (OverlayEntry entry in _entries) entry.remove(); _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'); expect(settings2.isInitialRoute, false); final RouteSettings settings3 = settings2.copyWith(isInitialRoute: true); expect(settings3.name, 'B'); expect(settings3.isInitialRoute, true); }); 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', (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: didPush', 'initial: didChangeNext null', ], ); TestRoute second; await runNavigatorTest( tester, host, () { host.push(second = TestRoute('second')); }, <String>[ 'second: install', 'second: didPush', 'second: didChangeNext null', 'initial: 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.replace(oldRoute: second, newRoute: TestRoute('two')); }, <String>[ 'two: install', 'two: didReplace second', 'two: didChangeNext third', 'initial: didChangeNext two', 'second: dispose', ], ); await runNavigatorTest( tester, host, () { host.pop('hello'); }, <String>[ 'third: didPop hello', 'third: dispose', 'two: didPopNext third', ], ); await runNavigatorTest( tester, host, () { host.pop('good bye'); }, <String>[ 'two: didPop good bye', 'two: dispose', 'initial: didPopNext two', ], ); 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: didPush', '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', 'third: dispose', 'second: didPopNext third', ], ); 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', 'four: dispose', 'second: didPopNext four', ], ); 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: didPush', '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', 'C: dispose', 'b: didPopNext C', ], ); 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: didPush', '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('TrasitionRoute', () { 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('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); }); }); } 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); } }