// 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.

// @dart = 2.8

import 'dart:ui';

import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.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 {
  const FirstWidget({ Key key }) : super(key: key);
  @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 {
  const SecondWidget({ Key key }) : super(key: key);
  @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({ Key key, this.targetKey, this.onException }) : super(key: key);

  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.headline3),
          ),
        ),
      ),
    );
  }
}

class SlideInOutPageRoute<T> extends PageRouteBuilder<T> {
  SlideInOutPageRoute({WidgetBuilder bodyBuilder, RouteSettings settings}) : super(
    settings: settings,
    pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => bodyBuilder(context),
    transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
        return SlideTransition(
          position: Tween<Offset>(
            begin: const Offset(1.0, 0),
            end: Offset.zero,
          ).animate(animation),
          child: SlideTransition(
            position: Tween<Offset>(
              begin: Offset.zero,
              end: const Offset(-1.0, 0),
            ).animate(secondaryAnimation),
            child: child,
          ),
        );
      },
  );

  @override
  AnimationController get controller => super.controller;
}

void main() {
  testWidgets('Can navigator navigate to and from a stateful widget', (WidgetTester tester) async {
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
      '/': (BuildContext context) => const FirstWidget(), // X
      '/second': (BuildContext context) => const 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, isFlutterError);
    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.name == '/') {
                    return MaterialPageRoute<void>(
                      builder: (BuildContext context) {
                        return ElevatedButton(
                          child: const Text('Next'),
                          onPressed: () {
                            Navigator.of(context).push(
                              MaterialPageRoute<void>(
                                builder: (BuildContext context) {
                                  return ElevatedButton(
                                    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']));
  });

   testWidgets('Pending gestures are rejected', (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));
     final 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']));
   }, skip: true); // https://github.com/flutter/flutter/issues/4771

  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('initial route trigger observer in the right order', (WidgetTester tester) async {
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
      '/' : (BuildContext context) => const Text('/'),
      '/A': (BuildContext context) => const Text('A'),
      '/A/B': (BuildContext context) => const Text('B'),
    };
    final List<NavigatorObservation> observations = <NavigatorObservation>[];
    final TestObserver observer = TestObserver()
      ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
        // Pushes the initial route.
        observations.add(
          NavigatorObservation(
            current: route?.settings?.name,
            previous: previousRoute?.settings?.name,
            operation: 'push'
          )
        );
      };

    await tester.pumpWidget(MaterialApp(
      routes: routes,
      initialRoute: '/A/B',
      navigatorObservers: <NavigatorObserver>[observer],
    ));

    expect(observations.length, 3);
    expect(observations[0].operation, 'push');
    expect(observations[0].current, '/');
    expect(observations[0].previous, isNull);

    expect(observations[1].operation, 'push');
    expect(observations[1].current, '/A');
    expect(observations[1].previous, '/');

    expect(observations[2].operation, 'push');
    expect(observations[2].current, '/A/B');
    expect(observations[2].previous, '/A');
  });

  testWidgets('Route didAdd and dispose in same frame work', (WidgetTester tester) async {
    // Regression Test for https://github.com/flutter/flutter/issues/61346.
    Widget buildNavigator() {
      return Navigator(
        pages: <Page<void>>[
          MaterialPage<void>(
            builder: (BuildContext context) => const Placeholder(),
          )
        ],
        onPopPage: (Route<dynamic> route, dynamic result) => false,
      );
    }
    final TabController controller = TabController(length: 3, vsync: tester);
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: TabBarView(
          controller: controller,
          children: <Widget>[
            buildNavigator(),
            buildNavigator(),
            buildNavigator(),
          ],
        )
      ),
    );

    // This test should finish without crashing.
    controller.index = 2;
    await tester.pumpAndSettle();
  });

  testWidgets('replaceNamed replaces', (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('pushReplacement sets secondaryAnimation after transition, with history change during transition', (WidgetTester tester) async {
    final Map<String, SlideInOutPageRoute<dynamic>> routes = <String, SlideInOutPageRoute<dynamic>>{};
    final Map<String, WidgetBuilder> builders = <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) => OnTapPage(
        id: 'B',
        onTap: () {
          Navigator.pushReplacementNamed(context, '/C');
        },
      ),
      '/C': (BuildContext context) => OnTapPage(
        id: 'C',
        onTap: () {
          Navigator.removeRoute(context, routes['/']);
        },
      ),
    };
    await tester.pumpWidget(MaterialApp(
      onGenerateRoute: (RouteSettings settings) {
        final SlideInOutPageRoute<dynamic> ret = SlideInOutPageRoute<dynamic>(bodyBuilder: builders[settings.name], settings: settings);
        routes[settings.name] = ret;
        return ret;
      }
    ));
    await tester.pumpAndSettle();
    await tester.tap(find.text('/'));
    await tester.pumpAndSettle();
    final double a2 = routes['/A'].secondaryAnimation.value;
    await tester.tap(find.text('A'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 16));
    expect(routes['/A'].secondaryAnimation.value, greaterThan(a2));
    await tester.pumpAndSettle();
    await tester.tap(find.text('B'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200));
    expect(routes['/A'].secondaryAnimation.value, equals(1.0));
    await tester.tap(find.text('C'));
    await tester.pumpAndSettle();
    expect(find.text('C'), isOnstage);
    expect(routes['/A'].secondaryAnimation.value, equals(routes['/C'].animation.value));
    final AnimationController controller = routes['/C'].controller;
    controller.value = 1 - controller.value;
    expect(routes['/A'].secondaryAnimation.value, equals(routes['/C'].animation.value));
  });

  testWidgets('new route removed from navigator history during pushReplacement transition', (WidgetTester tester) async {
    final Map<String, SlideInOutPageRoute<dynamic>> routes = <String, SlideInOutPageRoute<dynamic>>{};
    final Map<String, WidgetBuilder> builders = <String, WidgetBuilder>{
      '/' : (BuildContext context) => OnTapPage(
        id: '/',
        onTap: () {
          Navigator.pushNamed(context, '/A');
        }
      ),
      '/A': (BuildContext context) => OnTapPage(
        id: 'A',
        onTap: () {
          Navigator.pushReplacementNamed(context, '/B');
        }
      ),
      '/B': (BuildContext context) => OnTapPage(
        id: 'B',
        onTap: () {
          Navigator.removeRoute(context, routes['/B']);
        },
      ),
    };
    await tester.pumpWidget(MaterialApp(
      onGenerateRoute: (RouteSettings settings) {
        final SlideInOutPageRoute<dynamic> ret = SlideInOutPageRoute<dynamic>(bodyBuilder: builders[settings.name], settings: settings);
        routes[settings.name] = ret;
        return ret;
      }
    ));
    await tester.pumpAndSettle();
    await tester.tap(find.text('/'));
    await tester.pumpAndSettle();
    await tester.tap(find.text('A'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 200));
    expect(find.text('A'), isOnstage);
    expect(find.text('B'), isOnstage);
    await tester.tap(find.text('B'));
    await tester.pumpAndSettle();
    expect(find.text('/'), isOnstage);
    expect(find.text('B'), findsNothing);
    expect(find.text('A'), findsNothing);
    expect(routes['/'].secondaryAnimation.value, equals(0.0));
    expect(routes['/'].animation.value, equals(1.0));
  });

  testWidgets('pushReplacement triggers secondaryAnimation', (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(
      onGenerateRoute: (RouteSettings settings) {
        return SlideInOutPageRoute<dynamic>(bodyBuilder: routes[settings.name]);
      }
    ));
    await tester.pumpAndSettle();
    final Offset rootOffsetOriginal = tester.getTopLeft(find.text('/'));
    await tester.tap(find.text('/'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 16));
    expect(find.text('/'), isOnstage);
    expect(find.text('A'), isOnstage);
    expect(find.text('B'), findsNothing);
    final Offset rootOffset = tester.getTopLeft(find.text('/'));
    expect(rootOffset.dx, lessThan(rootOffsetOriginal.dx));

    Offset aOffsetOriginal = tester.getTopLeft(find.text('A'));
    await tester.pumpAndSettle();
    Offset aOffset = tester.getTopLeft(find.text('A'));
    expect(aOffset.dx, lessThan(aOffsetOriginal.dx));

    aOffsetOriginal = aOffset;
    await tester.tap(find.text('A'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 16));
    expect(find.text('/'), findsNothing);
    expect(find.text('A'), isOnstage);
    expect(find.text('B'), isOnstage);
    aOffset = tester.getTopLeft(find.text('A'));
    expect(aOffset.dx, lessThan(aOffsetOriginal.dx));
  });

  testWidgets('pushReplacement correctly reports didReplace to the observer', (WidgetTester tester) async {
    // Regression test for  https://github.com/flutter/flutter/issues/56892.
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
      '/' : (BuildContext context) => const OnTapPage(
        id: '/',
      ),
      '/A': (BuildContext context) => const OnTapPage(
        id: 'A',
      ),
      '/A/B': (BuildContext context) => OnTapPage(
        id: 'B',
        onTap: (){
          Navigator.of(context).popUntil((Route<dynamic> route) => route.isFirst);
          Navigator.of(context).pushReplacementNamed('/C');
        },
      ),
      '/C': (BuildContext context) => const OnTapPage(id: 'C',
      ),
    };
    final List<NavigatorObservation> observations = <NavigatorObservation>[];
    final TestObserver observer = TestObserver()
      ..onPopped = (Route<dynamic> route, Route<dynamic> previousRoute) {
        observations.add(
          NavigatorObservation(
            current: route.settings.name,
            previous: previousRoute.settings.name,
            operation: 'didPop'
          )
        );
      }
      ..onReplaced = (Route<dynamic> route, Route<dynamic> previousRoute) {
        observations.add(
          NavigatorObservation(
            current: route.settings.name,
            previous: previousRoute.settings.name,
            operation: 'didReplace'
          )
        );
      };
    await tester.pumpWidget(
      MaterialApp(
        routes: routes,
        navigatorObservers: <NavigatorObserver>[observer],
        initialRoute: '/A/B',
      )
    );
    await tester.pumpAndSettle();
    expect(find.text('B'), isOnstage);

    await tester.tap(find.text('B'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 16));
    expect(observations.length, 3);
    expect(observations[0].current, '/A/B');
    expect(observations[0].previous, '/A');
    expect(observations[0].operation, 'didPop');
    expect(observations[1].current, '/A');
    expect(observations[1].previous, '/');
    expect(observations[1].operation, 'didPop');

    expect(observations[2].current, '/C');
    expect(observations[2].previous, '/');
    expect(observations[2].operation, 'didReplace');

    await tester.pumpAndSettle();
    expect(find.text('C'), isOnstage);
  });

  testWidgets('Able to pop all routes', (WidgetTester tester) async {
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
      '/' : (BuildContext context) => const OnTapPage(
        id: '/',
      ),
      '/A': (BuildContext context) => const OnTapPage(
        id: 'A',
      ),
      '/A/B': (BuildContext context) => OnTapPage(
        id: 'B',
        onTap: (){
          // Pops all routes with bad predicate.
          Navigator.of(context).popUntil((Route<dynamic> route) => false);
        },
      ),
    };
    await tester.pumpWidget(
      MaterialApp(
        routes: routes,
        initialRoute: '/A/B',
      )
    );
    await tester.tap(find.text('B'));
    await tester.pumpAndSettle();
    expect(tester.takeException(), isNull);
  });

  testWidgets('pushAndRemoveUntil triggers secondaryAnimation', (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.pushNamedAndRemoveUntil(context, '/B', (Route<dynamic> route) => false);
        }
      ),
      '/B': (BuildContext context) => const OnTapPage(id: 'B'),
    };

    await tester.pumpWidget(MaterialApp(
      onGenerateRoute: (RouteSettings settings) {
        return SlideInOutPageRoute<dynamic>(bodyBuilder: routes[settings.name]);
      }
    ));
    await tester.pumpAndSettle();
    final Offset rootOffsetOriginal = tester.getTopLeft(find.text('/'));
    await tester.tap(find.text('/'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 16));
    expect(find.text('/'), isOnstage);
    expect(find.text('A'), isOnstage);
    expect(find.text('B'), findsNothing);
    final Offset rootOffset = tester.getTopLeft(find.text('/'));
    expect(rootOffset.dx, lessThan(rootOffsetOriginal.dx));

    Offset aOffsetOriginal = tester.getTopLeft(find.text('A'));
    await tester.pumpAndSettle();
    Offset aOffset = tester.getTopLeft(find.text('A'));
    expect(aOffset.dx, lessThan(aOffsetOriginal.dx));

    aOffsetOriginal = aOffset;
    await tester.tap(find.text('A'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 16));
    expect(find.text('/'), findsNothing);
    expect(find.text('A'), isOnstage);
    expect(find.text('B'), isOnstage);
    aOffset = tester.getTopLeft(find.text('A'));
    expect(aOffset.dx, lessThan(aOffsetOriginal.dx));

    await tester.pumpAndSettle();
    expect(find.text('/'), findsNothing);
    expect(find.text('A'), findsNothing);
    expect(find.text('B'), isOnstage);
  });

  testWidgets('pushAndRemoveUntil does not remove routes below the first route that pass the predicate', (WidgetTester tester) async {
    // Regression https://github.com/flutter/flutter/issues/56688
    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
      '/': (BuildContext context) => const Text('home'),
      '/A': (BuildContext context) => const Text('page A'),
      '/A/B': (BuildContext context) => OnTapPage(
        id: 'B',
        onTap: () {
          Navigator.of(context).pushNamedAndRemoveUntil('/D', ModalRoute.withName('/A'));
        },
      ),
      '/D': (BuildContext context) => const Text('page D'),
    };

    await tester.pumpWidget(
      MaterialApp(
        navigatorKey: navigator,
        routes: routes,
        initialRoute: '/A/B',
      )
    );
    await tester.pumpAndSettle();
    await tester.tap(find.text('B'));
    await tester.pumpAndSettle();
    expect(find.text('page D'), isOnstage);

    navigator.currentState.pop();
    await tester.pumpAndSettle();
    expect(find.text('page A'), isOnstage);

    navigator.currentState.pop();
    await tester.pumpAndSettle();
    expect(find.text('home'), isOnstage);
  });

  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 as Route<String>;
        previousRoute = previous as Route<String>;
      };

    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: TextButton(
        child: const Text('A'),
        onPressed: () {
          key.currentState.push<void>(routeB = MaterialPageRoute<void>(
            settings: const RouteSettings(name: 'B'),
            builder: (BuildContext context) {
              return TextButton(
                child: const Text('B'),
                onPressed: () {
                  key.currentState.push<void>(MaterialPageRoute<int>(
                    settings: const RouteSettings(name: 'C'),
                    builder: (BuildContext context) {
                      return TextButton(
                        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,
      theme: ThemeData(
        pageTransitionsTheme: const PageTransitionsTheme(
          builders: <TargetPlatform, PageTransitionsBuilder>{
            TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
          },
        ),
      ),
      home: TextButton(
        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 TextButton(
                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 TextButton(
                        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, isA<String>());
    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);
  });

  testWidgets("Popping immediately after pushing doesn't crash", (WidgetTester tester) async {
    // Added this test to protect against regression of https://github.com/flutter/flutter/issues/45539
    final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
      '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () {
        Navigator.pushNamed(context, '/A');
        Navigator.of(context).pop();
      }),
      '/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('/'), findsOneWidget);
    expect(find.text('A'), findsNothing);
    expect(isPushed, isTrue);
    expect(isPopped, isTrue);
  });

  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 as FlutterError;
      expect(error, isNotNull);
      expect(error.diagnostics.last, isA<DiagnosticsProperty<NavigatorState>>());
      expect(
        error.toStringDeep(),
        equalsIgnoringHashCodes(
          'FlutterError\n'
          '   Navigator.onGenerateRoute returned null when requested to build\n'
          '   route "/".\n'
          '   The onGenerateRoute callback must never return null, unless an\n'
          '   onUnknownRoute callback is provided as well.\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 as FlutterError;
      expect(error, isNotNull);
      expect(error.diagnostics.last, isA<DiagnosticsProperty<NavigatorState>>());
      expect(
        error.toStringDeep(),
        equalsIgnoringHashCodes(
          'FlutterError\n'
          '   Navigator.onUnknownRoute returned null when requested to build\n'
          '   route "/".\n'
          '   The onUnknownRoute callback must 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);
  });

  testWidgets('Pushing opaque Route does not rebuild routes below', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/45797.

    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
    final Key bottomRoute = UniqueKey();
    final Key topRoute = UniqueKey();
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(
          pageTransitionsTheme: const PageTransitionsTheme(
            builders: <TargetPlatform, PageTransitionsBuilder>{
              TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
            },
          ),
        ),
        navigatorKey: navigator,
        routes: <String, WidgetBuilder>{
          '/' : (BuildContext context) => StatefulTestWidget(key: bottomRoute),
          '/a': (BuildContext context) => StatefulTestWidget(key: topRoute),
        },
      ),
    );
    expect(tester.state<StatefulTestState>(find.byKey(bottomRoute)).rebuildCount, 1);

    navigator.currentState.pushNamed('/a');
    await tester.pumpAndSettle();

    // Bottom route is offstage and did not rebuild.
    expect(find.byKey(bottomRoute), findsNothing);
    expect(tester.state<StatefulTestState>(find.byKey(bottomRoute, skipOffstage: false)).rebuildCount, 1);

    expect(tester.state<StatefulTestState>(find.byKey(topRoute)).rebuildCount, 1);
  });

  testWidgets('initial routes below opaque route are offstage', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> g = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Navigator(
          key: g,
          initialRoute: '/a/b',
          onGenerateRoute: (RouteSettings s) {
            return MaterialPageRoute<void>(
              builder: (BuildContext c) {
                return Text('+${s.name}+');
              },
              settings: s,
            );
          },
        ),
      ),
    );

    expect(find.text('+/+'), findsNothing);
    expect(find.text('+/+', skipOffstage: false), findsOneWidget);
    expect(find.text('+/a+'), findsNothing);
    expect(find.text('+/a+', skipOffstage: false), findsOneWidget);
    expect(find.text('+/a/b+'), findsOneWidget);

    g.currentState.pop();
    await tester.pumpAndSettle();

    expect(find.text('+/+'), findsNothing);
    expect(find.text('+/+', skipOffstage: false), findsOneWidget);
    expect(find.text('+/a+'), findsOneWidget);
    expect(find.text('+/a/b+'), findsNothing);

    g.currentState.pop();
    await tester.pumpAndSettle();

    expect(find.text('+/+'), findsOneWidget);
    expect(find.text('+/a+'), findsNothing);
    expect(find.text('+/a/b+'), findsNothing);
  });

  testWidgets('Can provide custom onGenerateInitialRoutes', (WidgetTester tester) async {
    bool onGenerateInitialRoutesCalled = false;
    final GlobalKey<NavigatorState> g = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Navigator(
          key: g,
          initialRoute: 'Hello World',
          onGenerateInitialRoutes: (NavigatorState navigator, String initialRoute) {
            onGenerateInitialRoutesCalled = true;
            final List<Route<void>> result = <Route<void>>[];
            for (final String route in initialRoute.split(' ')) {
              result.add(MaterialPageRoute<void>(builder: (BuildContext context) {
                return Text(route);
              }));
            }
            return result;
          },
        ),
      ),
    );

    expect(onGenerateInitialRoutesCalled, true);
    expect(find.text('Hello'), findsNothing);
    expect(find.text('World'), findsOneWidget);

    g.currentState.pop();
    await tester.pumpAndSettle();

    expect(find.text('Hello'), findsOneWidget);
    expect(find.text('World'), findsNothing);
  });

  testWidgets('Navigator.of able to handle input context is a navigator context', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> g = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      MaterialApp(
        navigatorKey: g,
        home: const Text('home'),
      )
    );

    final NavigatorState state = Navigator.of(g.currentContext);
    expect(state, g.currentState);
  });

  testWidgets('Navigator.of able to handle input context is a navigator context - root navigator', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> root = GlobalKey<NavigatorState>();
    final GlobalKey<NavigatorState> sub = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      MaterialApp(
        navigatorKey: root,
        home: Navigator(
          key: sub,
          onGenerateRoute: (RouteSettings settings) {
            return MaterialPageRoute<void>(
              settings: settings,
              builder: (BuildContext context) => const Text('dummy'),
            );
          },
        ),
      )
    );

    final NavigatorState state = Navigator.of(sub.currentContext, rootNavigator: true);
    expect(state, root.currentState);
  });

  testWidgets('pushAndRemove until animates the push', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/25080.

    const Duration kFourTenthsOfTheTransitionDuration = Duration(milliseconds: 120);
    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
    final Map<String, MaterialPageRoute<dynamic>> routeNameToContext = <String, MaterialPageRoute<dynamic>>{};

    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Navigator(
          key: navigator,
          initialRoute: 'root',
          onGenerateRoute: (RouteSettings settings) {
            return MaterialPageRoute<void>(
              settings: settings,
              builder: (BuildContext context) {
                routeNameToContext[settings.name] = ModalRoute.of(context) as MaterialPageRoute<dynamic>;
                return Text('Route: ${settings.name}');
              },
            );
          },
        ),
      ),
    );

    expect(find.text('Route: root'), findsOneWidget);

    navigator.currentState.pushNamed('1');
    await tester.pumpAndSettle();

    expect(find.text('Route: 1'), findsOneWidget);

    navigator.currentState.pushNamed('2');
    await tester.pumpAndSettle();

    expect(find.text('Route: 2'), findsOneWidget);

    navigator.currentState.pushNamed('3');
    await tester.pumpAndSettle();

    expect(find.text('Route: 3'), findsOneWidget);
    expect(find.text('Route: 2', skipOffstage: false), findsOneWidget);
    expect(find.text('Route: 1', skipOffstage: false), findsOneWidget);
    expect(find.text('Route: root', skipOffstage: false), findsOneWidget);

    navigator.currentState.pushNamedAndRemoveUntil('4', (Route<dynamic> route) => route.isFirst);
    await tester.pump();

    expect(find.text('Route: 3'), findsOneWidget);
    expect(find.text('Route: 4'), findsOneWidget);
    final Animation<double> route4Entry = routeNameToContext['4'].animation;
    expect(route4Entry.value, 0.0); // Entry animation has not started.

    await tester.pump(kFourTenthsOfTheTransitionDuration);
    expect(find.text('Route: 3'), findsOneWidget);
    expect(find.text('Route: 4'), findsOneWidget);
    expect(route4Entry.value, 0.4);

    await tester.pump(kFourTenthsOfTheTransitionDuration);
    expect(find.text('Route: 3'), findsOneWidget);
    expect(find.text('Route: 4'), findsOneWidget);
    expect(route4Entry.value, 0.8);
    expect(find.text('Route: 2', skipOffstage: false), findsOneWidget);
    expect(find.text('Route: 1', skipOffstage: false), findsOneWidget);
    expect(find.text('Route: root', skipOffstage: false), findsOneWidget);

    // When we hit 1.0 all but root and current have been removed.
    await tester.pump(kFourTenthsOfTheTransitionDuration);
    expect(find.text('Route: 3', skipOffstage: false), findsNothing);
    expect(find.text('Route: 4'), findsOneWidget);
    expect(route4Entry.value, 1.0);
    expect(find.text('Route: 2', skipOffstage: false), findsNothing);
    expect(find.text('Route: 1', skipOffstage: false), findsNothing);
    expect(find.text('Route: root', skipOffstage: false), findsOneWidget);

    navigator.currentState.pop();
    await tester.pumpAndSettle();

    expect(find.text('Route: root'), findsOneWidget);
    expect(find.text('Route: 4', skipOffstage: false), findsNothing);
  });

  testWidgets('Wrapping TickerMode can turn off ticking in routes', (WidgetTester tester) async {
    int tickCount = 0;
    Widget widgetUnderTest({bool enabled}) {
      return TickerMode(
        enabled: enabled,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Navigator(
            initialRoute: 'root',
            onGenerateRoute: (RouteSettings settings) {
              return MaterialPageRoute<void>(
                settings: settings,
                builder: (BuildContext context) {
                  return _TickingWidget(
                    onTick: () {
                      tickCount++;
                    },
                  );
                },
              );
            },
          ),
        ),
      );
    }

    await tester.pumpWidget(widgetUnderTest(enabled: false));
    expect(tickCount, 0);
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(tickCount, 0);

    await tester.pumpWidget(widgetUnderTest(enabled: true));
    expect(tickCount, 0);
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    await tester.pump(const Duration(seconds: 1));
    expect(tickCount, 4);
  });

  testWidgets('Route announce correctly for first route and last route', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/57133.
    Route<void> previousOfFirst = NotAnnounced();
    Route<void> nextOfFirst = NotAnnounced();
    Route<void> popNextOfFirst = NotAnnounced();
    Route<void> firstRoute;

    Route<void> previousOfSecond = NotAnnounced();
    Route<void> nextOfSecond = NotAnnounced();
    Route<void> popNextOfSecond = NotAnnounced();
    Route<void> secondRoute;

    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
    await tester.pumpWidget(
      MaterialApp(
        navigatorKey: navigator,
        initialRoute: '/second',
        onGenerateRoute: (RouteSettings settings) {
          if (settings.name == '/') {
            firstRoute = RouteAnnouncementSpy(
              onDidChangeNext: (Route<void> next) => nextOfFirst = next,
              onDidChangePrevious: (Route<void> previous) => previousOfFirst = previous,
              onDidPopNext: (Route<void> next) => popNextOfFirst = next,
              settings: settings,
            );
            return firstRoute;
          }
          secondRoute = RouteAnnouncementSpy(
            onDidChangeNext: (Route<void> next) => nextOfSecond = next,
            onDidChangePrevious: (Route<void> previous) => previousOfSecond = previous,
            onDidPopNext: (Route<void> next) => popNextOfSecond = next,
            settings: settings,
          );
          return secondRoute;
        },
      ),
    );
    await tester.pumpAndSettle();

    expect(previousOfFirst, isNull);
    expect(nextOfFirst, secondRoute);
    expect(popNextOfFirst, isA<NotAnnounced>());

    expect(previousOfSecond, firstRoute);
    expect(nextOfSecond, isNull);
    expect(popNextOfSecond, isA<NotAnnounced>());

    navigator.currentState.pop();
    expect(popNextOfFirst, secondRoute);
  });

  testWidgets('hero controller scope works', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> top = GlobalKey<NavigatorState>();
    final GlobalKey<NavigatorState> sub = GlobalKey<NavigatorState>();

    final List<NavigatorObservation> observations = <NavigatorObservation>[];
    final HeroControllerSpy spy = HeroControllerSpy()
      ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
        observations.add(
          NavigatorObservation(
            current: route?.settings?.name,
            previous: previousRoute?.settings?.name,
            operation: 'didPush'
          )
        );
      };
    await tester.pumpWidget(
      HeroControllerScope(
        controller: spy,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Navigator(
            key: top,
            initialRoute: 'top1',
            onGenerateRoute: (RouteSettings s) {
              return MaterialPageRoute<void>(
                builder: (BuildContext c) {
                  return Navigator(
                    key: sub,
                    initialRoute: 'sub1',
                    onGenerateRoute: (RouteSettings s) {
                      return MaterialPageRoute<void>(
                        builder: (BuildContext c) {
                          return const Placeholder();
                        },
                        settings: s,
                      );
                    },
                  );
                },
                settings: s,
              );
            },
          ),
        )
      )
    );
    // It should only observe the top navigator.
    expect(observations.length, 1);
    expect(observations[0].current, 'top1');
    expect(observations[0].previous, isNull);

    sub.currentState.push(MaterialPageRoute<void>(
      settings: const RouteSettings(name:'sub2'),
      builder: (BuildContext context) => const Text('sub2')
    ));
    await tester.pumpAndSettle();

    expect(find.text('sub2'), findsOneWidget);
    // It should not record sub navigator.
    expect(observations.length, 1);

    top.currentState.push(MaterialPageRoute<void>(
      settings: const RouteSettings(name:'top2'),
      builder: (BuildContext context) => const Text('top2')
    ));
    await tester.pumpAndSettle();
    expect(observations.length, 2);
    expect(observations[1].current, 'top2');
    expect(observations[1].previous, 'top1');
  });

  testWidgets('hero controller can correctly transfer subscription - replacing navigator', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>();
    final GlobalKey<NavigatorState> key2 = GlobalKey<NavigatorState>();

    final List<NavigatorObservation> observations = <NavigatorObservation>[];
    final HeroControllerSpy spy = HeroControllerSpy()
      ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
        observations.add(
          NavigatorObservation(
            current: route?.settings?.name,
            previous: previousRoute?.settings?.name,
            operation: 'didPush'
          )
        );
      };
    await tester.pumpWidget(
      HeroControllerScope(
        controller: spy,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Navigator(
            key: key1,
            initialRoute: 'navigator1',
            onGenerateRoute: (RouteSettings s) {
              return MaterialPageRoute<void>(
                builder: (BuildContext c) {
                  return const Placeholder();
                },
                settings: s,
              );
            },
          ),
        )
      )
    );
    // Transfer the subscription to another navigator
    await tester.pumpWidget(
      HeroControllerScope(
        controller: spy,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Navigator(
            key: key2,
            initialRoute: 'navigator2',
            onGenerateRoute: (RouteSettings s) {
              return MaterialPageRoute<void>(
                builder: (BuildContext c) {
                  return const Placeholder();
                },
                settings: s,
              );
            },
          ),
        )
      )
    );
    observations.clear();

    key2.currentState.push(MaterialPageRoute<void>(
      settings: const RouteSettings(name:'new route'),
      builder: (BuildContext context) => const Text('new route')
    ));
    await tester.pumpAndSettle();

    expect(find.text('new route'), findsOneWidget);
    // It should record from the new navigator.
    expect(observations.length, 1);
    expect(observations[0].current, 'new route');
    expect(observations[0].previous, 'navigator2');
  });

  testWidgets('hero controller can correctly transfer subscription - swapping navigator', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>();
    final GlobalKey<NavigatorState> key2 = GlobalKey<NavigatorState>();

    final List<NavigatorObservation> observations1 = <NavigatorObservation>[];
    final HeroControllerSpy spy1 = HeroControllerSpy()
      ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
        observations1.add(
          NavigatorObservation(
            current: route?.settings?.name,
            previous: previousRoute?.settings?.name,
            operation: 'didPush'
          )
        );
      };
    final List<NavigatorObservation> observations2 = <NavigatorObservation>[];
    final HeroControllerSpy spy2 = HeroControllerSpy()
      ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
        observations2.add(
          NavigatorObservation(
            current: route?.settings?.name,
            previous: previousRoute?.settings?.name,
            operation: 'didPush'
          )
        );
      };
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Stack(
          children: <Widget>[
            HeroControllerScope(
              controller: spy1,
              child: Navigator(
                key: key1,
                initialRoute: 'navigator1',
                onGenerateRoute: (RouteSettings s) {
                  return MaterialPageRoute<void>(
                    builder: (BuildContext c) {
                      return const Placeholder();
                    },
                    settings: s,
                  );
                },
              )
            ),
            HeroControllerScope(
              controller: spy2,
              child: Navigator(
                key: key2,
                initialRoute: 'navigator2',
                onGenerateRoute: (RouteSettings s) {
                  return MaterialPageRoute<void>(
                    builder: (BuildContext c) {
                      return const Placeholder();
                    },
                    settings: s,
                  );
                },
              )
            ),
          ],
        ),
      ),
    );
    expect(observations1.length, 1);
    expect(observations1[0].current, 'navigator1');
    expect(observations1[0].previous, isNull);
    expect(observations2.length, 1);
    expect(observations2[0].current, 'navigator2');
    expect(observations2[0].previous, isNull);

    // Swaps the spies.
    await tester.pumpWidget(
      Directionality(
        textDirection: TextDirection.ltr,
        child: Stack(
          children: <Widget>[
            HeroControllerScope(
              controller: spy2,
              child: Navigator(
                key: key1,
                initialRoute: 'navigator1',
                onGenerateRoute: (RouteSettings s) {
                  return MaterialPageRoute<void>(
                    builder: (BuildContext c) {
                      return const Placeholder();
                    },
                    settings: s,
                  );
                },
              )
            ),
            HeroControllerScope(
              controller: spy1,
              child: Navigator(
                key: key2,
                initialRoute: 'navigator2',
                onGenerateRoute: (RouteSettings s) {
                  return MaterialPageRoute<void>(
                    builder: (BuildContext c) {
                      return const Placeholder();
                    },
                    settings: s,
                  );
                },
              )
            ),
          ],
        ),
      ),
    );

    // Pushes a route to navigator2.
    key2.currentState.push(MaterialPageRoute<void>(
      settings: const RouteSettings(name:'new route2'),
      builder: (BuildContext context) => const Text('new route2')
    ));
    await tester.pumpAndSettle();
    expect(find.text('new route2'), findsOneWidget);
    // The spy1 should record the push in navigator2.
    expect(observations1.length, 2);
    expect(observations1[1].current, 'new route2');
    expect(observations1[1].previous, 'navigator2');
    // The spy2 should not record anything.
    expect(observations2.length, 1);

    // Pushes a route to navigator1
    key1.currentState.push(MaterialPageRoute<void>(
      settings: const RouteSettings(name:'new route1'),
      builder: (BuildContext context) => const Text('new route1')
    ));
    await tester.pumpAndSettle();
    expect(find.text('new route1'), findsOneWidget);
    // The spy1 should not record anything.
    expect(observations1.length, 2);
    // The spy2 should record the push in navigator1.
    expect(observations2.length, 2);
    expect(observations2[1].current, 'new route1');
    expect(observations2[1].previous, 'navigator1');
  });

  testWidgets('hero controller subscribes to multiple navigators does throw', (WidgetTester tester) async {
    final HeroControllerSpy spy = HeroControllerSpy();
    await tester.pumpWidget(
      HeroControllerScope(
        controller: spy,
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Stack(
            children: <Widget>[
              Navigator(
                initialRoute: 'navigator1',
                onGenerateRoute: (RouteSettings s) {
                  return MaterialPageRoute<void>(
                    builder: (BuildContext c) {
                      return const Placeholder();
                    },
                    settings: s,
                  );
                },
              ),
              Navigator(
                initialRoute: 'navigator2',
                onGenerateRoute: (RouteSettings s) {
                  return MaterialPageRoute<void>(
                    builder: (BuildContext c) {
                      return const Placeholder();
                    },
                    settings: s,
                  );
                },
              ),
            ],
          ),
        ),
      ),
    );
    expect(tester.takeException(), isAssertionError);
  });

  group('Page api', (){
    Widget buildNavigator({
      List<Page<dynamic>> pages,
      PopPageCallback onPopPage,
      GlobalKey<NavigatorState> key,
      TransitionDelegate<dynamic> transitionDelegate,
      List<NavigatorObserver> observers = const <NavigatorObserver>[],
    }) {
      return MediaQuery(
        data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
        child: Localizations(
          locale: const Locale('en', 'US'),
          delegates: const <LocalizationsDelegate<dynamic>>[
            DefaultMaterialLocalizations.delegate,
            DefaultWidgetsLocalizations.delegate
          ],
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Navigator(
              key: key,
              pages: pages,
              onPopPage: onPopPage,
              observers: observers,
              transitionDelegate: transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
            ),
          ),
        ),
      );
    }

    testWidgets('can initialize with pages list', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      final List<TestPage> myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name:'initial'),
        const TestPage(key: ValueKey<String>('2'), name:'second'),
        const TestPage(key: ValueKey<String>('3'), name:'third'),
      ];

      bool onPopPage(Route<dynamic> route, dynamic result) {
        myPages.removeWhere((Page<dynamic> page) => route.settings == page);
        return route.didPop(result);
      }

      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      expect(find.text('third'), findsOneWidget);
      expect(find.text('second'), findsNothing);
      expect(find.text('initial'), findsNothing);

      navigator.currentState.pop();
      await tester.pumpAndSettle();
      expect(find.text('third'), findsNothing);
      expect(find.text('second'), findsOneWidget);
      expect(find.text('initial'), findsNothing);

      navigator.currentState.pop();
      await tester.pumpAndSettle();
      expect(find.text('third'), findsNothing);
      expect(find.text('second'), findsNothing);
      expect(find.text('initial'), findsOneWidget);
    });

    testWidgets('can push and pop pages using page api', (WidgetTester tester) async {
      Animation<double> secondaryAnimationOfRouteOne;
      Animation<double> primaryAnimationOfRouteOne;
      Animation<double> secondaryAnimationOfRouteTwo;
      Animation<double> primaryAnimationOfRouteTwo;
      Animation<double> secondaryAnimationOfRouteThree;
      Animation<double> primaryAnimationOfRouteThree;
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      List<Page<dynamic>> myPages = <Page<dynamic>>[
        CustomBuilderPage<void>(
          key: const ValueKey<String>('1'),
          name:'initial',
          routeBuilder: (BuildContext context, RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                secondaryAnimationOfRouteOne = secondaryAnimation;
                primaryAnimationOfRouteOne = animation;
                return const Text('initial');
              },
            );
          },
        ),
      ];

      bool onPopPage(Route<dynamic> route, dynamic result) {
        myPages.removeWhere((Page<dynamic> page) => route.settings == page);
        return route.didPop(result);
      }

      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      expect(find.text('initial'), findsOneWidget);

      myPages = <Page<dynamic>>[
        CustomBuilderPage<void>(
          key: const ValueKey<String>('1'),
          name:'initial',
          routeBuilder: (BuildContext context, RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                secondaryAnimationOfRouteOne = secondaryAnimation;
                primaryAnimationOfRouteOne = animation;
                return const Text('initial');
              },
            );
          },
        ),
        CustomBuilderPage<void>(
          key: const ValueKey<String>('2'),
          name:'second',
          routeBuilder: (BuildContext context, RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                secondaryAnimationOfRouteTwo = secondaryAnimation;
                primaryAnimationOfRouteTwo = animation;
                return const Text('second');
              },
            );
          },
        ),
        CustomBuilderPage<void>(
          key: const ValueKey<String>('3'),
          name:'third',
          routeBuilder: (BuildContext context, RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                secondaryAnimationOfRouteThree = secondaryAnimation;
                primaryAnimationOfRouteThree = animation;
                return const Text('third');
              },
            );
          },
        )
      ];

      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      // The third page is transitioning, and the secondary animation of first
      // page should chain with the third page. The animation of second page
      // won't start until the third page finishes transition.
      expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteThree.value);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.dismissed);
      expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteThree.status, AnimationStatus.forward);

      await tester.pump(const Duration(milliseconds: 30));
      expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteThree.value);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.dismissed);
      expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteThree.value, 0.1);
      await tester.pumpAndSettle();
      // After transition finishes, the routes' animations are correctly chained.
      expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteThree.value);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed);
      expect(find.text('third'), findsOneWidget);
      expect(find.text('second'), findsNothing);
      expect(find.text('initial'), findsNothing);
      // Starts pops the pages using page api and verify the animations chain
      // correctly.

      myPages = <Page<dynamic>>[
        CustomBuilderPage<void>(
          key: const ValueKey<String>('1'),
          name:'initial',
          routeBuilder: (BuildContext context, RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                secondaryAnimationOfRouteOne = secondaryAnimation;
                primaryAnimationOfRouteOne = animation;
                return const Text('initial');
              },
            );
          },
        ),
        CustomBuilderPage<void>(
          key: const ValueKey<String>('2'),
          name:'second',
          routeBuilder: (BuildContext context, RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                secondaryAnimationOfRouteTwo = secondaryAnimation;
                primaryAnimationOfRouteTwo = animation;
                return const Text('second');
              },
            );
          },
        ),
      ];

      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      await tester.pump(const Duration(milliseconds: 30));
      expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteThree.value);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteThree.value, 0.9);
      await tester.pumpAndSettle();
      expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteThree.value);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteThree.status, AnimationStatus.dismissed);
    });

    testWidgets('can modify routes history and secondary animation still works', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      Animation<double> secondaryAnimationOfRouteOne;
      Animation<double> primaryAnimationOfRouteOne;
      Animation<double> secondaryAnimationOfRouteTwo;
      Animation<double> primaryAnimationOfRouteTwo;
      Animation<double> secondaryAnimationOfRouteThree;
      Animation<double> primaryAnimationOfRouteThree;
      List<Page<dynamic>> myPages = <CustomBuilderPage<void>>[
        CustomBuilderPage<void>(
          key: const ValueKey<String>('1'),
          name:'initial',
          routeBuilder: (BuildContext context, RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                secondaryAnimationOfRouteOne = secondaryAnimation;
                primaryAnimationOfRouteOne = animation;
                return const Text('initial');
              },
            );
          },
        ),
        CustomBuilderPage<void>(
          key: const ValueKey<String>('2'),
          name:'second',
          routeBuilder: (BuildContext context, RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                secondaryAnimationOfRouteTwo = secondaryAnimation;
                primaryAnimationOfRouteTwo = animation;
                return const Text('second');
              },
            );
          },
        ),
        CustomBuilderPage<void>(
          key: const ValueKey<String>('3'),
          name:'third',
          routeBuilder: (BuildContext context, RouteSettings settings) {
            return PageRouteBuilder<void>(
              settings: settings,
              pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
                secondaryAnimationOfRouteThree = secondaryAnimation;
                primaryAnimationOfRouteThree = animation;
                return const Text('third');
              },
            );
          },
        ),
      ];
      bool onPopPage(Route<dynamic> route, dynamic result) {
        myPages.removeWhere((Page<dynamic> page) => route.settings == page);
        return route.didPop(result);
      }
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      expect(find.text('third'), findsOneWidget);
      expect(find.text('second'), findsNothing);
      expect(find.text('initial'), findsNothing);
      expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteThree.value);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed);

      myPages = myPages.reversed.toList();
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      // Reversed routes are still chained up correctly.
      expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value);
      expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed);

      navigator.currentState.pop();
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 30));
      expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value);
      expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteOne.value, 0.9);
      await tester.pumpAndSettle();
      expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value);
      expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.dismissed);

      navigator.currentState.pop();
      await tester.pump();
      await tester.pump(const Duration(milliseconds: 30));
      expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value);
      expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value);
      expect(primaryAnimationOfRouteTwo.value, 0.9);
      expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.dismissed);
      await tester.pumpAndSettle();
      expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value);
      expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed);
      expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value);
      expect(primaryAnimationOfRouteTwo.status, AnimationStatus.dismissed);
      expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed);
      expect(primaryAnimationOfRouteOne.status, AnimationStatus.dismissed);
    });

    testWidgets('can work with pageless route', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      List<TestPage> myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name:'initial'),
        const TestPage(key: ValueKey<String>('2'), name:'second'),
      ];

      bool onPopPage(Route<dynamic> route, dynamic result) {
        myPages.removeWhere((Page<dynamic> page) => route.settings == page);
        return route.didPop(result);
      }

      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      expect(find.text('second'), findsOneWidget);
      expect(find.text('initial'), findsNothing);
      // Pushes two pageless routes to second page route
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('second-pageless1'),
          settings: null,
        )
      );
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('second-pageless2'),
          settings: null,
        )
      );
      await tester.pumpAndSettle();
      // Now the history should look like
      // [initial, second, second-pageless1, second-pageless2].
      expect(find.text('initial'), findsNothing);
      expect(find.text('second'), findsNothing);
      expect(find.text('second-pageless1'), findsNothing);
      expect(find.text('second-pageless2'), findsOneWidget);

      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name:'initial'),
        const TestPage(key: ValueKey<String>('2'), name:'second'),
        const TestPage(key: ValueKey<String>('3'), name:'third'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      await tester.pumpAndSettle();
      expect(find.text('initial'), findsNothing);
      expect(find.text('second'), findsNothing);
      expect(find.text('second-pageless1'), findsNothing);
      expect(find.text('second-pageless2'), findsNothing);
      expect(find.text('third'), findsOneWidget);

      // Pushes one pageless routes to third page route
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('third-pageless1'),
          settings: null,
        )
      );
      await tester.pumpAndSettle();
      // Now the history should look like
      // [initial, second, second-pageless1, second-pageless2, third, third-pageless1].
      expect(find.text('initial'), findsNothing);
      expect(find.text('second'), findsNothing);
      expect(find.text('second-pageless1'), findsNothing);
      expect(find.text('second-pageless2'), findsNothing);
      expect(find.text('third'), findsNothing);
      expect(find.text('third-pageless1'), findsOneWidget);

      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name:'initial'),
        const TestPage(key: ValueKey<String>('3'), name:'third'),
        const TestPage(key: ValueKey<String>('2'), name:'second'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      // Swaps the order without any adding or removing should not trigger any
      // transition. The routes should update without a pumpAndSettle
      // Now the history should look like
      // [initial, third, third-pageless1, second, second-pageless1, second-pageless2].
      expect(find.text('initial'), findsNothing);
      expect(find.text('third'), findsNothing);
      expect(find.text('third-pageless1'), findsNothing);
      expect(find.text('second'), findsNothing);
      expect(find.text('second-pageless1'), findsNothing);
      expect(find.text('second-pageless2'), findsOneWidget);
      // Pops the route one by one to make sure the order is correct.
      navigator.currentState.pop();
      await tester.pumpAndSettle();
      expect(find.text('initial'), findsNothing);
      expect(find.text('third'), findsNothing);
      expect(find.text('third-pageless1'), findsNothing);
      expect(find.text('second'), findsNothing);
      expect(find.text('second-pageless1'), findsOneWidget);
      expect(find.text('second-pageless2'), findsNothing);
      expect(myPages.length, 3);
      navigator.currentState.pop();
      await tester.pumpAndSettle();
      expect(find.text('initial'), findsNothing);
      expect(find.text('third'), findsNothing);
      expect(find.text('third-pageless1'), findsNothing);
      expect(find.text('second'), findsOneWidget);
      expect(find.text('second-pageless1'), findsNothing);
      expect(find.text('second-pageless2'), findsNothing);
      expect(myPages.length, 3);
      navigator.currentState.pop();
      await tester.pumpAndSettle();
      expect(find.text('initial'), findsNothing);
      expect(find.text('third'), findsNothing);
      expect(find.text('third-pageless1'), findsOneWidget);
      expect(find.text('second'), findsNothing);
      expect(find.text('second-pageless1'), findsNothing);
      expect(find.text('second-pageless2'), findsNothing);
      expect(myPages.length, 2);
      navigator.currentState.pop();
      await tester.pumpAndSettle();
      expect(find.text('initial'), findsNothing);
      expect(find.text('third'), findsOneWidget);
      expect(find.text('third-pageless1'), findsNothing);
      expect(find.text('second'), findsNothing);
      expect(find.text('second-pageless1'), findsNothing);
      expect(find.text('second-pageless2'), findsNothing);
      expect(myPages.length, 2);
      navigator.currentState.pop();
      await tester.pumpAndSettle();
      expect(find.text('initial'), findsOneWidget);
      expect(find.text('third'), findsNothing);
      expect(find.text('third-pageless1'), findsNothing);
      expect(find.text('second'), findsNothing);
      expect(find.text('second-pageless1'), findsNothing);
      expect(find.text('second-pageless2'), findsNothing);
      expect(myPages.length, 1);
    });

    testWidgets('complex case 1', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      List<TestPage> myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
      ];
      bool onPopPage(Route<dynamic> route, dynamic result) {
        myPages.removeWhere((Page<dynamic> page) => route.settings == page);
        return route.didPop(result);
      }

      // Add initial page route with one pageless route.
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      bool initialPageless1Completed = false;
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('initial-pageless1'),
          settings: null,
        )
      ).then((_) => initialPageless1Completed = true);
      await tester.pumpAndSettle();

      // Pushes second page route with two pageless routes.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
        const TestPage(key: ValueKey<String>('2'), name: 'second'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      await tester.pumpAndSettle();
      bool secondPageless1Completed = false;
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('second-pageless1'),
          settings: null,
        )
      ).then((_) => secondPageless1Completed = true);
      await tester.pumpAndSettle();
      bool secondPageless2Completed = false;
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('second-pageless2'),
          settings: null,
        )
      ).then((_) => secondPageless2Completed = true);
      await tester.pumpAndSettle();

      // Pushes third page route with one pageless route.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
        const TestPage(key: ValueKey<String>('2'), name: 'second'),
        const TestPage(key: ValueKey<String>('3'), name: 'third'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      await tester.pumpAndSettle();
      bool thirdPageless1Completed = false;
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('third-pageless1'),
          settings: null,
        )
      ).then((_) => thirdPageless1Completed = true);
      await tester.pumpAndSettle();

      // Nothing has been popped.
      expect(initialPageless1Completed, false);
      expect(secondPageless1Completed, false);
      expect(secondPageless2Completed, false);
      expect(thirdPageless1Completed, false);

      // Switches order and removes the initial page route.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('3'), name: 'third'),
        const TestPage(key: ValueKey<String>('2'), name: 'second'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      // The pageless route of initial page route should be completed.
      expect(initialPageless1Completed, true);
      expect(secondPageless1Completed, false);
      expect(secondPageless2Completed, false);
      expect(thirdPageless1Completed, false);

      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('3'), name: 'third'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      await tester.pumpAndSettle();
      expect(secondPageless1Completed, true);
      expect(secondPageless2Completed, true);
      expect(thirdPageless1Completed, false);

      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('4'), name: 'forth'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );
      expect(thirdPageless1Completed, true);
      await tester.pumpAndSettle();
      expect(find.text('forth'), findsOneWidget);
    });

    testWidgets('complex case 1 - with always remove transition delegate', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      final AlwaysRemoveTransitionDelegate transitionDelegate = AlwaysRemoveTransitionDelegate();
      List<TestPage> myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
      ];
      bool onPopPage(Route<dynamic> route, dynamic result) {
        myPages.removeWhere((Page<dynamic> page) => route.settings == page);
        return route.didPop(result);
      }

      // Add initial page route with one pageless route.
      await tester.pumpWidget(
        buildNavigator(
          pages: myPages,
          onPopPage: onPopPage,
          key: navigator,
          transitionDelegate: transitionDelegate
        )
      );
      bool initialPageless1Completed = false;
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('initial-pageless1'),
          settings: null,
        )
      ).then((_) => initialPageless1Completed = true);
      await tester.pumpAndSettle();

      // Pushes second page route with two pageless routes.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
        const TestPage(key: ValueKey<String>('2'), name: 'second'),
      ];
      await tester.pumpWidget(
        buildNavigator(
          pages: myPages,
          onPopPage: onPopPage,
          key: navigator,
          transitionDelegate: transitionDelegate
        )
      );
      bool secondPageless1Completed = false;
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('second-pageless1'),
          settings: null,
        )
      ).then((_) => secondPageless1Completed = true);
      await tester.pumpAndSettle();
      bool secondPageless2Completed = false;
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('second-pageless2'),
          settings: null,
        )
      ).then((_) => secondPageless2Completed = true);
      await tester.pumpAndSettle();

      // Pushes third page route with one pageless route.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
        const TestPage(key: ValueKey<String>('2'), name: 'second'),
        const TestPage(key: ValueKey<String>('3'), name: 'third'),
      ];
      await tester.pumpWidget(
        buildNavigator(
          pages: myPages,
          onPopPage: onPopPage,
          key: navigator,
          transitionDelegate: transitionDelegate
        )
      );
      bool thirdPageless1Completed = false;
      navigator.currentState.push(
        MaterialPageRoute<void>(
          builder: (BuildContext context) => const Text('third-pageless1'),
          settings: null,
        )
      ).then((_) => thirdPageless1Completed = true);
      await tester.pumpAndSettle();

      // Nothing has been popped.
      expect(initialPageless1Completed, false);
      expect(secondPageless1Completed, false);
      expect(secondPageless2Completed, false);
      expect(thirdPageless1Completed, false);

      // Switches order and removes the initial page route.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('3'), name: 'third'),
        const TestPage(key: ValueKey<String>('2'), name: 'second'),
      ];
      await tester.pumpWidget(
        buildNavigator(
          pages: myPages,
          onPopPage: onPopPage,
          key: navigator,
          transitionDelegate: transitionDelegate
        )
      );
      // The pageless route of initial page route should be removed without complete.
      expect(initialPageless1Completed, false);
      expect(secondPageless1Completed, false);
      expect(secondPageless2Completed, false);
      expect(thirdPageless1Completed, false);

      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('3'), name: 'third'),
      ];
      await tester.pumpWidget(
        buildNavigator(
          pages: myPages,
          onPopPage: onPopPage,
          key: navigator,
          transitionDelegate: transitionDelegate
        )
      );
      await tester.pumpAndSettle();
      expect(initialPageless1Completed, false);
      expect(secondPageless1Completed, false);
      expect(secondPageless2Completed, false);
      expect(thirdPageless1Completed, false);

      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('4'), name: 'forth'),
      ];
      await tester.pumpWidget(
        buildNavigator(
          pages: myPages,
          onPopPage: onPopPage,
          key: navigator,
          transitionDelegate: transitionDelegate
        )
      );
      await tester.pump();
      expect(initialPageless1Completed, false);
      expect(secondPageless1Completed, false);
      expect(secondPageless2Completed, false);
      expect(thirdPageless1Completed, false);
      expect(find.text('forth'), findsOneWidget);
    });

    testWidgets('can repush a page that was previously popped before it has finished popping', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      List<Page<dynamic>> myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
        const TestPage(key: ValueKey<String>('2'), name: 'second'),
      ];
      bool onPopPage(Route<dynamic> route, dynamic result) {
        myPages.removeWhere((Page<dynamic> page) => route.settings == page);
        return route.didPop(result);
      }
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );

      // Pops the second page route.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );

      // Re-push the second page again before it finishes popping.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
        const TestPage(key: ValueKey<String>('2'), name: 'second'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );

      // It should not crash the app.
      expect(tester.takeException(), isNull);
      await tester.pumpAndSettle();
      expect(find.text('second'), findsOneWidget);
    });

    testWidgets('can update pages before a route has finished popping', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      List<Page<dynamic>> myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
        const TestPage(key: ValueKey<String>('2'), name: 'second'),
      ];
      bool onPopPage(Route<dynamic> route, dynamic result) {
        myPages.removeWhere((Page<dynamic> page) => route.settings == page);
        return route.didPop(result);
      }
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );

      // Pops the second page route.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );

      // Updates the pages again before second page finishes popping.
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name: 'initial'),
      ];
      await tester.pumpWidget(
        buildNavigator(pages: myPages, onPopPage: onPopPage, key: navigator)
      );

      // It should not crash the app.
      expect(tester.takeException(), isNull);
      await tester.pumpAndSettle();
      expect(find.text('initial'), findsOneWidget);
    });

    testWidgets('pages remove and add trigger observer in the right order', (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
      List<TestPage> myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('1'), name:'first'),
        const TestPage(key: ValueKey<String>('2'), name:'second'),
        const TestPage(key: ValueKey<String>('3'), name:'third'),
      ];
      final List<NavigatorObservation> observations = <NavigatorObservation>[];
      final TestObserver observer = TestObserver()
        ..onPushed = (Route<dynamic> route, Route<dynamic> previousRoute) {
          observations.add(
            NavigatorObservation(
              current: route?.settings?.name,
              previous: previousRoute?.settings?.name,
              operation: 'push'
            )
          );
        }
        ..onRemoved = (Route<dynamic> route, Route<dynamic> previousRoute) {
          observations.add(
            NavigatorObservation(
              current: route?.settings?.name,
              previous: previousRoute?.settings?.name,
              operation: 'remove'
            )
          );
        };
      bool onPopPage(Route<dynamic> route, dynamic result) => false;

      await tester.pumpWidget(
        buildNavigator(
          pages: myPages,
          onPopPage: onPopPage,
          key: navigator,
          observers: <NavigatorObserver>[observer],
        )
      );
      myPages = <TestPage>[
        const TestPage(key: ValueKey<String>('4'), name:'forth'),
        const TestPage(key: ValueKey<String>('5'), name:'fifth'),
      ];

      await tester.pumpWidget(
        buildNavigator(
          pages: myPages,
          onPopPage: onPopPage,
          key: navigator,
          observers: <NavigatorObserver>[observer],
        )
      );

      await tester.pumpAndSettle();
      expect(observations.length, 8);
      // Initial routes are pushed.
      expect(observations[0].operation, 'push');
      expect(observations[0].current, 'first');
      expect(observations[0].previous, isNull);

      expect(observations[1].operation, 'push');
      expect(observations[1].current, 'second');
      expect(observations[1].previous, 'first');

      expect(observations[2].operation, 'push');
      expect(observations[2].current, 'third');
      expect(observations[2].previous, 'second');

      // Pages are updated.
      // New routes are pushed before removing the initial routes.
      expect(observations[3].operation, 'push');
      expect(observations[3].current, 'forth');
      expect(observations[3].previous, 'third');

      expect(observations[4].operation, 'push');
      expect(observations[4].current, 'fifth');
      expect(observations[4].previous, 'forth');

      // Initial routes are removed.
      expect(observations[5].operation, 'remove');
      expect(observations[5].current, 'third');
      expect(observations[5].previous, isNull);

      expect(observations[6].operation, 'remove');
      expect(observations[6].current, 'second');
      expect(observations[6].previous, isNull);

      expect(observations[7].operation, 'remove');
      expect(observations[7].current, 'first');
      expect(observations[7].previous, isNull);
    });
  });
}

typedef AnnouncementCallBack = void Function(Route<dynamic>);

class NotAnnounced extends Route<void> {/* A place holder for not announced route*/}

class RouteAnnouncementSpy extends Route<void> {
  RouteAnnouncementSpy({
    this.onDidChangePrevious,
    this.onDidChangeNext,
    this.onDidPopNext,
    RouteSettings settings,
  }) : super(settings: settings);
  final AnnouncementCallBack onDidChangePrevious;
  final AnnouncementCallBack onDidChangeNext;
  final AnnouncementCallBack onDidPopNext;

  @override
  List<OverlayEntry> get overlayEntries => <OverlayEntry>[
    OverlayEntry(
      builder: (BuildContext context) => const Placeholder(),
    )
  ];

  @override
  void didChangeNext(Route<dynamic> nextRoute) {
    super.didChangeNext(nextRoute);
    if (onDidChangeNext != null)
      onDidChangeNext(nextRoute);
  }

  @override
  void didChangePrevious(Route<dynamic> previousRoute) {
    super.didChangePrevious(previousRoute);
    if (onDidChangePrevious != null)
      onDidChangePrevious(previousRoute);
  }

  @override
  void didPopNext(Route<dynamic> nextRoute) {
    super.didPopNext(nextRoute);
    if (onDidPopNext != null)
      onDidPopNext(nextRoute);
  }
}

class _TickingWidget extends StatefulWidget {
  const _TickingWidget({this.onTick});

  final VoidCallback onTick;

  @override
  State<_TickingWidget> createState() => _TickingWidgetState();
}

class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProviderStateMixin {
  Ticker _ticker;

  @override
  void initState() {
    super.initState();
    _ticker = createTicker((Duration _) {
      widget.onTick();
    })..start();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }

  @override
  void dispose() {
    _ticker.dispose();
    super.dispose();
  }
}

class AlwaysRemoveTransitionDelegate extends TransitionDelegate<void> {
  @override
  Iterable<RouteTransitionRecord> resolve({
    List<RouteTransitionRecord> newPageRouteHistory,
    Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
    Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
  }) {
    final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
    void handleExitingRoute(RouteTransitionRecord location) {
      if (!locationToExitingPageRoute.containsKey(location))
        return;

      final RouteTransitionRecord exitingPageRoute = locationToExitingPageRoute[location];
      if (exitingPageRoute.isWaitingForExitingDecision) {
        final bool hasPagelessRoute = pageRouteToPagelessRoutes.containsKey(exitingPageRoute);
        exitingPageRoute.markForRemove();
        if (hasPagelessRoute) {
          final List<RouteTransitionRecord> pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute];
          for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) {
            pagelessRoute.markForRemove();
          }
        }
      }
      results.add(exitingPageRoute);

      handleExitingRoute(exitingPageRoute);
    }
    handleExitingRoute(null);

    for (final RouteTransitionRecord pageRoute in newPageRouteHistory) {
      if (pageRoute.isWaitingForEnteringDecision) {
        pageRoute.markForAdd();
      }
      results.add(pageRoute);
      handleExitingRoute(pageRoute);

    }
    return results;
  }
}

class TestPage extends Page<void> {
  const TestPage({
    LocalKey key,
    String name,
    Object arguments,
  }) : super(key: key, name: name, arguments: arguments);

  @override
  Route<void> createRoute(BuildContext context) {
    return MaterialPageRoute<void>(
      builder: (BuildContext context) => Text(name),
      settings: this,
    );
  }
}

class NoAnimationPageRoute extends PageRouteBuilder<void> {
  NoAnimationPageRoute({WidgetBuilder pageBuilder})
      : super(pageBuilder: (BuildContext context, __, ___) {
          return pageBuilder(context);
        });

  @override
  AnimationController createAnimationController() {
    return super.createAnimationController()..value = 1.0;
  }
}

class StatefulTestWidget extends StatefulWidget {
  const StatefulTestWidget({Key key}) : super(key: key);

  @override
  State<StatefulTestWidget> createState() => StatefulTestState();
}

class StatefulTestState extends State<StatefulTestWidget> {
  int rebuildCount = 0;

  @override
  Widget build(BuildContext context) {
    rebuildCount += 1;
    return Container();
  }
}

class HeroControllerSpy extends HeroController {
  OnObservation onPushed;
  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
    if (onPushed != null) {
      onPushed(route, previousRoute);
    }
  }
}

class NavigatorObservation {
  const NavigatorObservation({this.previous, this.current, this.operation});
  final String previous;
  final String current;
  final String operation;
}
