// 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' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

import '../painting/image_test_utils.dart' show TestImageProvider;

Future<ui.Image> createTestImage() {
  final ui.Paint paint = ui.Paint()
    ..style = ui.PaintingStyle.stroke
    ..strokeWidth = 1.0;
  final ui.PictureRecorder recorder = ui.PictureRecorder();
  final ui.Canvas pictureCanvas = ui.Canvas(recorder);
  pictureCanvas.drawCircle(Offset.zero, 20.0, paint);
  final ui.Picture picture = recorder.endRecording();
  return picture.toImage(300, 300);
}

Key firstKey = const Key('first');
Key secondKey = const Key('second');
Key thirdKey = const Key('third');
Key simpleKey = const Key('simple');

Key homeRouteKey = const Key('homeRoute');
Key routeTwoKey = const Key('routeTwo');
Key routeThreeKey = const Key('routeThree');

bool transitionFromUserGestures = false;

final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
  '/': (BuildContext context) => Material(
    child: ListView(
      key: homeRouteKey,
      children: <Widget>[
        const SizedBox(height: 100.0, width: 100.0),
        Card(child: Hero(
          tag: 'a',
          transitionOnUserGestures: transitionFromUserGestures,
          child: Container(height: 100.0, width: 100.0, key: firstKey),
        )),
        const SizedBox(height: 100.0, width: 100.0),
        TextButton(
          child: const Text('two'),
          onPressed: () { Navigator.pushNamed(context, '/two'); },
        ),
        TextButton(
          child: const Text('twoInset'),
          onPressed: () { Navigator.pushNamed(context, '/twoInset'); },
        ),
        TextButton(
          child: const Text('simple'),
          onPressed: () { Navigator.pushNamed(context, '/simple'); },
        ),
      ],
    ),
  ),
  '/two': (BuildContext context) => Material(
    child: ListView(
      key: routeTwoKey,
      children: <Widget>[
        TextButton(
          child: const Text('pop'),
          onPressed: () { Navigator.pop(context); },
        ),
        const SizedBox(height: 150.0, width: 150.0),
        Card(child: Hero(
          tag: 'a',
          transitionOnUserGestures: transitionFromUserGestures,
          child: Container(height: 150.0, width: 150.0, key: secondKey),
        )),
        const SizedBox(height: 150.0, width: 150.0),
        TextButton(
          child: const Text('three'),
          onPressed: () { Navigator.push(context, ThreeRoute()); },
        ),
      ],
    ),
  ),
  // This route is the same as /two except that Hero 'a' is shifted to the right by
  // 50 pixels. When the hero's in-flight bounds between / and /twoInset are animated
  // using MaterialRectArcTween (the default) they'll follow a different path
  // then when the flight starts at /twoInset and returns to /.
  '/twoInset': (BuildContext context) => Material(
    child: ListView(
      key: routeTwoKey,
      children: <Widget>[
        TextButton(
          child: const Text('pop'),
          onPressed: () { Navigator.pop(context); },
        ),
        const SizedBox(height: 150.0, width: 150.0),
        Card(
          child: Padding(
            padding: const EdgeInsets.only(left: 50.0),
            child: Hero(
              tag: 'a',
              transitionOnUserGestures: transitionFromUserGestures,
              child: Container(height: 150.0, width: 150.0, key: secondKey),
            ),
          ),
        ),
        const SizedBox(height: 150.0, width: 150.0),
        TextButton(
          child: const Text('three'),
          onPressed: () { Navigator.push(context, ThreeRoute()); },
        ),
      ],
    ),
  ),
  // This route is the same as /two except that Hero 'a' is shifted to the right by
  // 50 pixels. When the hero's in-flight bounds between / and /twoInset are animated
  // using MaterialRectArcTween (the default) they'll follow a different path
  // then when the flight starts at /twoInset and returns to /.
  '/simple': (BuildContext context) => CupertinoPageScaffold(
    child: Center(
      child: Hero(
        tag: 'a',
        transitionOnUserGestures: transitionFromUserGestures,
        child: Container(height: 150.0, width: 150.0, key: simpleKey),
      ),
    ),
  ),
};

class ThreeRoute extends MaterialPageRoute<void> {
  ThreeRoute()
    : super(builder: (BuildContext context) {
        return Material(
          key: routeThreeKey,
          child: ListView(
            children: <Widget>[
              const SizedBox(height: 200.0, width: 200.0),
              Card(child: Hero(tag: 'a', child: Container(height: 200.0, width: 200.0, key: thirdKey))),
              const SizedBox(height: 200.0, width: 200.0),
            ],
          ),
        );
      });
}

class MutatingRoute extends MaterialPageRoute<void> {
  MutatingRoute()
    : super(builder: (BuildContext context) {
        return Hero(tag: 'a', child: const Text('MutatingRoute'), key: UniqueKey());
      });

  void markNeedsBuild() {
    setState(() {
      // Trigger a rebuild
    });
  }
}

class _SimpleStatefulWidget extends StatefulWidget {
  const _SimpleStatefulWidget({ Key key }) : super(key: key);
  @override
  _SimpleState createState() => _SimpleState();
}

class _SimpleState extends State<_SimpleStatefulWidget> {
  int state = 0;

  @override
  Widget build(BuildContext context) => Text(state.toString());
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({ Key key, this.value = '123' }) : super(key: key);
  final String value;
  @override
  MyStatefulWidgetState createState() => MyStatefulWidgetState();
}

class MyStatefulWidgetState extends State<MyStatefulWidget> {
  @override
  Widget build(BuildContext context) => Text(widget.value);
}

Future<void> main() async {
  final ui.Image testImage = await createTestImage();
  assert(testImage != null);

  setUp(() {
    transitionFromUserGestures = false;
  });

  testWidgets('Heroes animate', (WidgetTester tester) async {

    await tester.pumpWidget(MaterialApp(routes: routes));

    // the initial setup.

    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), findsNothing);

    await tester.tap(find.text('two'));
    await tester.pump(); // begin navigation

    // at this stage, the second route is offstage, so that we can form the
    // hero party.

    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey, skipOffstage: false), isOffstage);
    expect(find.byKey(secondKey, skipOffstage: false), isInCard);

    await tester.pump();

    // at this stage, the heroes have just gone on their journey, we are
    // seeing them at t=16ms. The original page no longer contains the hero.

    expect(find.byKey(firstKey), findsNothing);

    expect(find.byKey(secondKey), findsOneWidget);
    expect(find.byKey(secondKey), isNotInCard);
    expect(find.byKey(secondKey), isOnstage);

    await tester.pump();

    // t=32ms for the journey. Surely they are still at it.

    expect(find.byKey(firstKey), findsNothing);

    expect(find.byKey(secondKey), findsOneWidget);

    expect(find.byKey(secondKey), findsOneWidget);
    expect(find.byKey(secondKey), isNotInCard);
    expect(find.byKey(secondKey), isOnstage);

    await tester.pump(const Duration(seconds: 1));

    // t=1.032s for the journey. The journey has ended (it ends this frame, in
    // fact). The hero should now be in the new page, onstage. The original
    // widget will be back as well now (though not visible).

    expect(find.byKey(firstKey), findsNothing);
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);

    await tester.pump();

    // Should not change anything.

    expect(find.byKey(firstKey), findsNothing);
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);

    // Now move on to view 3

    await tester.tap(find.text('three'));
    await tester.pump(); // begin navigation

    // at this stage, the second route is offstage, so that we can form the
    // hero party.

    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);
    expect(find.byKey(thirdKey, skipOffstage: false), isOffstage);
    expect(find.byKey(thirdKey, skipOffstage: false), isInCard);

    await tester.pump();

    // at this stage, the heroes have just gone on their journey, we are
    // seeing them at t=16ms. The original page no longer contains the hero.

    expect(find.byKey(secondKey), findsNothing);
    expect(find.byKey(thirdKey), isOnstage);
    expect(find.byKey(thirdKey), isNotInCard);

    await tester.pump();

    // t=32ms for the journey. Surely they are still at it.

    expect(find.byKey(secondKey), findsNothing);
    expect(find.byKey(thirdKey), isOnstage);
    expect(find.byKey(thirdKey), isNotInCard);

    await tester.pump(const Duration(seconds: 1));

    // t=1.032s for the journey. The journey has ended (it ends this frame, in
    // fact). The hero should now be in the new page, onstage.

    expect(find.byKey(secondKey), findsNothing);
    expect(find.byKey(thirdKey), isOnstage);
    expect(find.byKey(thirdKey), isInCard);

    await tester.pump();

    // Should not change anything.

    expect(find.byKey(secondKey), findsNothing);
    expect(find.byKey(thirdKey), isOnstage);
    expect(find.byKey(thirdKey), isInCard);
  });

  testWidgets('Heroes still animate after hero controller is swapped.', (WidgetTester tester) async {
    final GlobalKey<NavigatorState> key = GlobalKey<NavigatorState>();
    final UniqueKey heroKey = UniqueKey();
    await tester.pumpWidget(
      HeroControllerScope(
        controller: HeroController(),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Navigator(
            key: key,
            initialRoute: 'navigator1',
            onGenerateRoute: (RouteSettings s) {
              return MaterialPageRoute<void>(
                builder: (BuildContext c) {
                  return Hero(
                    tag: 'hero',
                    child: Container(),
                    flightShuttleBuilder: (
                      BuildContext flightContext,
                      Animation<double> animation,
                      HeroFlightDirection flightDirection,
                      BuildContext fromHeroContext,
                      BuildContext toHeroContext,
                    ) {
                      return Container(key: heroKey);
                    },
                  );
                },
                settings: s,
              );
            },
          ),
        ),
      ),
    );
    key.currentState.push(MaterialPageRoute<void>(
      builder: (BuildContext c) {
        return Hero(
          tag: 'hero',
          child: Container(),
          flightShuttleBuilder: (
            BuildContext flightContext,
            Animation<double> animation,
            HeroFlightDirection flightDirection,
            BuildContext fromHeroContext,
            BuildContext toHeroContext,
            ) {
            return Container(key: heroKey);
          },
        );
      },
    ));
    expect(find.byKey(heroKey), findsNothing);
    // Begins the navigation
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 30));
    expect(find.byKey(heroKey), isOnstage);
    // Pumps a new hero controller.
    await tester.pumpWidget(
      HeroControllerScope(
        controller: HeroController(),
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: Navigator(
            key: key,
            initialRoute: 'navigator1',
            onGenerateRoute: (RouteSettings s) {
              return MaterialPageRoute<void>(
                builder: (BuildContext c) {
                  return Hero(
                    tag: 'hero',
                    child: Container(),
                    flightShuttleBuilder: (
                      BuildContext flightContext,
                      Animation<double> animation,
                      HeroFlightDirection flightDirection,
                      BuildContext fromHeroContext,
                      BuildContext toHeroContext,
                      ) {
                      return Container(key: heroKey);
                    },
                  );
                },
                settings: s,
              );
            },
          ),
        ),
      ),
    );
    // The original animation still flies.
    expect(find.byKey(heroKey), isOnstage);
    // Waits for the animation finishes.
    await tester.pumpAndSettle();
    expect(find.byKey(heroKey), findsNothing);
  });

  testWidgets('Heroes animate should hide original hero', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(routes: routes));
    // Checks initial state.
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), findsNothing);

    await tester.tap(find.text('two'));
    await tester.pumpAndSettle(); // Waits for transition finishes.

    expect(find.byKey(firstKey), findsNothing);
    final Offstage first = tester.widget(
      find.ancestor(
        of: find.byKey(firstKey, skipOffstage: false),
        matching: find.byType(Offstage, skipOffstage: false),
      ).first,
    );
    // Original hero should stay hidden.
    expect(first.offstage, isTrue);
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);
  });

  testWidgets('Destination hero is rebuilt midflight', (WidgetTester tester) async {
    final MutatingRoute route = MutatingRoute();

    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: ListView(
          children: <Widget>[
            const Hero(tag: 'a', child: Text('foo')),
            Builder(builder: (BuildContext context) {
              return TextButton(child: const Text('two'), onPressed: () => Navigator.push(context, route));
            }),
          ],
        ),
      ),
    ));

    await tester.tap(find.text('two'));
    await tester.pump(const Duration(milliseconds: 10));

    route.markNeedsBuild();

    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(seconds: 1));
  });

  testWidgets('Heroes animation is fastOutSlowIn', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(routes: routes));
    await tester.tap(find.text('two'));
    await tester.pump(); // begin navigation

    // Expect the height of the secondKey Hero to vary from 100 to 150
    // over duration and according to curve.

    const Duration duration = Duration(milliseconds: 300);
    const Curve curve = Curves.fastOutSlowIn;
    final double initialHeight = tester.getSize(find.byKey(firstKey, skipOffstage: false)).height;
    final double finalHeight = tester.getSize(find.byKey(secondKey, skipOffstage: false)).height;
    final double deltaHeight = finalHeight - initialHeight;
    const double epsilon = 0.001;

    await tester.pump(duration * 0.25);
    expect(
      tester.getSize(find.byKey(secondKey)).height,
      closeTo(curve.transform(0.25) * deltaHeight + initialHeight, epsilon),
    );

    await tester.pump(duration * 0.25);
    expect(
      tester.getSize(find.byKey(secondKey)).height,
      closeTo(curve.transform(0.50) * deltaHeight + initialHeight, epsilon),
    );

    await tester.pump(duration * 0.25);
    expect(
      tester.getSize(find.byKey(secondKey)).height,
      closeTo(curve.transform(0.75) * deltaHeight + initialHeight, epsilon),
    );

    await tester.pump(duration * 0.25);
    expect(
      tester.getSize(find.byKey(secondKey)).height,
      closeTo(curve.transform(1.0) * deltaHeight + initialHeight, epsilon),
    );
  });

  testWidgets('Heroes are not interactive', (WidgetTester tester) async {
    final List<String> log = <String>[];

    await tester.pumpWidget(MaterialApp(
      home: Center(
        child: Hero(
          tag: 'foo',
          child: GestureDetector(
            onTap: () {
              log.add('foo');
            },
            child: Container(
              width: 100.0,
              height: 100.0,
              child: const Text('foo'),
            ),
          ),
        ),
      ),
      routes: <String, WidgetBuilder>{
        '/next': (BuildContext context) {
          return Align(
            alignment: Alignment.topLeft,
            child: Hero(
              tag: 'foo',
              child: GestureDetector(
                onTap: () {
                  log.add('bar');
                },
                child: Container(
                  width: 100.0,
                  height: 150.0,
                  child: const Text('bar'),
                ),
              ),
            ),
          );
        },
      },
    ));

    expect(log, isEmpty);
    await tester.tap(find.text('foo'));
    expect(log, equals(<String>['foo']));
    log.clear();

    final NavigatorState navigator = tester.state(find.byType(Navigator));
    navigator.pushNamed('/next');

    expect(log, isEmpty);
    await tester.tap(find.text('foo', skipOffstage: false));
    expect(log, isEmpty);

    await tester.pump(const Duration(milliseconds: 10));
    await tester.tap(find.text('foo', skipOffstage: false));
    expect(log, isEmpty);
    await tester.tap(find.text('bar', skipOffstage: false));
    expect(log, isEmpty);

    await tester.pump(const Duration(milliseconds: 10));
    expect(find.text('foo'), findsNothing);
    await tester.tap(find.text('bar', skipOffstage: false));
    expect(log, isEmpty);

    await tester.pump(const Duration(seconds: 1));
    expect(find.text('foo'), findsNothing);
    await tester.tap(find.text('bar'));
    expect(log, equals(<String>['bar']));
  });

  testWidgets('Popping on first frame does not cause hero observer to crash', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      onGenerateRoute: (RouteSettings settings) {
        return MaterialPageRoute<void>(
          settings: settings,
          builder: (BuildContext context) => Hero(tag: 'test', child: Container()),
        );
      },
    ));
    await tester.pump();

    final Finder heroes = find.byType(Hero);
    expect(heroes, findsOneWidget);

    Navigator.pushNamed(heroes.evaluate().first, 'test');
    await tester.pump(); // adds the new page to the tree...

    Navigator.pop(heroes.evaluate().first);
    await tester.pump(); // ...and removes it straight away (since it's already at 0.0)
  });

  testWidgets('Overlapping starting and ending a hero transition works ok', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      onGenerateRoute: (RouteSettings settings) {
        return MaterialPageRoute<void>(
          settings: settings,
          builder: (BuildContext context) => Hero(tag: 'test', child: Container()),
        );
      },
    ));
    await tester.pump();

    final Finder heroes = find.byType(Hero);
    expect(heroes, findsOneWidget);

    Navigator.pushNamed(heroes.evaluate().first, 'test');
    await tester.pump();
    await tester.pump(const Duration(hours: 1));

    Navigator.pushNamed(heroes.evaluate().first, 'test');
    await tester.pump();
    await tester.pump(const Duration(hours: 1));

    Navigator.pop(heroes.evaluate().first);
    await tester.pump();
    Navigator.pop(heroes.evaluate().first);
    await tester.pump(const Duration(hours: 1)); // so the first transition is finished, but the second hasn't started
    await tester.pump();
  });

  testWidgets('One route, two heroes, same tag, throws', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: ListView(
          children: <Widget>[
            const Hero(tag: 'a', child: Text('a')),
            const Hero(tag: 'a', child: Text('a too')),
            Builder(
              builder: (BuildContext context) {
                return TextButton(
                  child: const Text('push'),
                  onPressed: () {
                    Navigator.push(context, PageRouteBuilder<void>(
                      pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
                        return const Text('fail');
                      },
                    ));
                  },
                );
              },
            ),
          ],
        ),
      ),
    ));

    await tester.tap(find.text('push'));
    await tester.pump();
    final dynamic exception = tester.takeException();
    expect(exception, isFlutterError);
    final FlutterError error = exception as FlutterError;
    expect(error.diagnostics.length, 3);
    final DiagnosticsNode last = error.diagnostics.last;
    expect(last, isA<DiagnosticsProperty<StatefulElement>>());
    expect(
      last.toStringDeep(),
      equalsIgnoringHashCodes(
        '# Here is the subtree for one of the offending heroes: Hero\n',
      ),
    );
    expect(last.style, DiagnosticsTreeStyle.dense);
    expect(
      error.toStringDeep(),
      equalsIgnoringHashCodes(
        'FlutterError\n'
        '   There are multiple heroes that share the same tag within a\n'
        '   subtree.\n'
        '   Within each subtree for which heroes are to be animated (i.e. a\n'
        '   PageRoute subtree), each Hero must have a unique non-null tag.\n'
        '   In this case, multiple heroes had the following tag: a\n'
        '   ├# Here is the subtree for one of the offending heroes: Hero\n',
      ),
    );
  });

  testWidgets('Hero push transition interrupted by a pop', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      routes: routes
    ));

    // Initially the firstKey Card on the '/' route is visible
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), findsNothing);

    // Pushes MaterialPageRoute '/two'.
    await tester.tap(find.text('two'));

    // Start the flight of Hero 'a' from route '/' to route '/two'. Route '/two'
    // is now offstage.
    await tester.pump();

    final double initialHeight = tester.getSize(find.byKey(firstKey)).height;
    final double finalHeight = tester.getSize(find.byKey(secondKey, skipOffstage: false)).height;
    expect(finalHeight, greaterThan(initialHeight)); // simplify the checks below

    // Build the first hero animation frame in the navigator's overlay.
    await tester.pump();

    // At this point the hero widgets have been replaced by placeholders
    // and the destination hero has been moved to the overlay.
    expect(find.descendant(of: find.byKey(homeRouteKey), matching: find.byKey(firstKey)), findsNothing);
    expect(find.descendant(of: find.byKey(routeTwoKey), matching: find.byKey(secondKey)), findsNothing);
    expect(find.byKey(firstKey), findsNothing);
    expect(find.byKey(secondKey), isOnstage);

    // The duration of a MaterialPageRoute's transition is 300ms.
    // At 150ms Hero 'a' is mid-flight.
    await tester.pump(const Duration(milliseconds: 150));
    final double height150ms = tester.getSize(find.byKey(secondKey)).height;
    expect(height150ms, greaterThan(initialHeight));
    expect(height150ms, lessThan(finalHeight));

    // Pop route '/two' before the push transition to '/two' has finished.
    await tester.tap(find.text('pop'));

    // Restart the flight of Hero 'a'. Now it's flying from route '/two' to
    // route '/'.
    await tester.pump();

    // After flying in the opposite direction for 50ms Hero 'a' will
    // be smaller than it was, but bigger than its initial size.
    await tester.pump(const Duration(milliseconds: 50));
    final double height100ms = tester.getSize(find.byKey(secondKey)).height;
    expect(height100ms, lessThan(height150ms));
    expect(finalHeight, greaterThan(height100ms));

    // Hero a's return flight at 149ms. The outgoing (push) flight took
    // 150ms so we should be just about back to where Hero 'a' started.
    const double epsilon = 0.001;
    await tester.pump(const Duration(milliseconds: 99));
    closeTo(tester.getSize(find.byKey(secondKey)).height - initialHeight, epsilon);

    // The flight is finished. We're back to where we started.
    await tester.pump(const Duration(milliseconds: 300));
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), findsNothing);
  });

  testWidgets('Hero pop transition interrupted by a push', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(routes: routes)
    );

    // Pushes MaterialPageRoute '/two'.
    await tester.tap(find.text('two'));
    await tester.pump();
    await tester.pump(const Duration(seconds: 1));

    // Now the secondKey Card on the '/2' route is visible
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);
    expect(find.byKey(firstKey), findsNothing);

    // Pop MaterialPageRoute '/two'.
    await tester.tap(find.text('pop'));

    // Start the flight of Hero 'a' from route '/two' to route '/'. Route '/two'
    // is now offstage.
    await tester.pump();

    final double initialHeight = tester.getSize(find.byKey(secondKey)).height;
    final double finalHeight = tester.getSize(find.byKey(firstKey, skipOffstage: false)).height;
    expect(finalHeight, lessThan(initialHeight)); // simplify the checks below

    // Build the first hero animation frame in the navigator's overlay.
    await tester.pump();

    // At this point the hero widgets have been replaced by placeholders
    // and the destination hero has been moved to the overlay.
    expect(find.descendant(of: find.byKey(homeRouteKey), matching: find.byKey(firstKey)), findsNothing);
    expect(find.descendant(of: find.byKey(routeTwoKey), matching: find.byKey(secondKey)), findsNothing);
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(secondKey), findsNothing);

    // The duration of a MaterialPageRoute's transition is 300ms.
    // At 150ms Hero 'a' is mid-flight.
    await tester.pump(const Duration(milliseconds: 150));
    final double height150ms = tester.getSize(find.byKey(firstKey)).height;
    expect(height150ms, lessThan(initialHeight));
    expect(height150ms, greaterThan(finalHeight));

    // Push route '/two' before the pop transition from '/two' has finished.
    await tester.tap(find.text('two'));

    // Restart the flight of Hero 'a'. Now it's flying from route '/' to
    // route '/two'.
    await tester.pump();

    // After flying in the opposite direction for 50ms Hero 'a' will
    // be smaller than it was, but bigger than its initial size.
    await tester.pump(const Duration(milliseconds: 50));
    final double height200ms = tester.getSize(find.byKey(firstKey)).height;
    expect(height200ms, greaterThan(height150ms));
    expect(finalHeight, lessThan(height200ms));

    // Hero a's return flight at 149ms. The outgoing (push) flight took
    // 150ms so we should be just about back to where Hero 'a' started.
    const double epsilon = 0.001;
    await tester.pump(const Duration(milliseconds: 99));
    closeTo(tester.getSize(find.byKey(firstKey)).height - initialHeight, epsilon);

    // The flight is finished. We're back to where we started.
    await tester.pump(const Duration(milliseconds: 300));
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);
    expect(find.byKey(firstKey), findsNothing);
  });

  testWidgets('Destination hero disappears mid-flight', (WidgetTester tester) async {
    const Key homeHeroKey = Key('home hero');
    const Key routeHeroKey = Key('route hero');
    bool routeIncludesHero = true;
    StateSetter heroCardSetState;

    // Show a 200x200 Hero tagged 'H', with key routeHeroKey
    final MaterialPageRoute<void> route = MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Material(
          child: ListView(
            children: <Widget>[
              StatefulBuilder(
                builder: (BuildContext context, StateSetter setState) {
                  heroCardSetState = setState;
                  return Card(
                    child: routeIncludesHero
                      ? Hero(tag: 'H', child: Container(key: routeHeroKey, height: 200.0, width: 200.0))
                      : const SizedBox(height: 200.0, width: 200.0),
                  );
                },
              ),
              TextButton(
                child: const Text('POP'),
                onPressed: () { Navigator.pop(context); },
              ),
            ],
          ),
        );
      },
    );

    // Show a 100x100 Hero tagged 'H' with key homeHeroKey
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return ListView(
                children: <Widget> [
                  Card(
                    child: Hero(tag: 'H', child: Container(key: homeHeroKey, height: 100.0, width: 100.0)),
                  ),
                  TextButton(
                    child: const Text('PUSH'),
                    onPressed: () { Navigator.push(context, route); },
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );

    // Pushes route
    await tester.tap(find.text('PUSH'));
    await tester.pump();
    await tester.pump();
    final double initialHeight = tester.getSize(find.byKey(routeHeroKey)).height;

    await tester.pump(const Duration(milliseconds: 10));
    double midflightHeight = tester.getSize(find.byKey(routeHeroKey)).height;
    expect(midflightHeight, greaterThan(initialHeight));
    expect(midflightHeight, lessThan(200.0));

    await tester.pump(const Duration(milliseconds: 300));
    await tester.pump();
    double finalHeight = tester.getSize(find.byKey(routeHeroKey)).height;
    expect(finalHeight, 200.0);

    // Complete the flight
    await tester.pump(const Duration(milliseconds: 100));

    // Rebuild route with its Hero

    heroCardSetState(() {
      routeIncludesHero = true;
    });
    await tester.pump();

    // Pops route
    await tester.tap(find.text('POP'));
    await tester.pump();
    await tester.pump();

    await tester.pump(const Duration(milliseconds: 10));
    midflightHeight = tester.getSize(find.byKey(homeHeroKey)).height;
    expect(midflightHeight, lessThan(finalHeight));
    expect(midflightHeight, greaterThan(100.0));

    // Remove the destination hero midflight
    heroCardSetState(() {
      routeIncludesHero = false;
    });
    await tester.pump();

    await tester.pump(const Duration(milliseconds: 300));
    finalHeight = tester.getSize(find.byKey(homeHeroKey)).height;
    expect(finalHeight, 100.0);

  });

  testWidgets('Destination hero scrolls mid-flight', (WidgetTester tester) async {
    const Key homeHeroKey = Key('home hero');
    const Key routeHeroKey = Key('route hero');
    const Key routeContainerKey = Key('route hero container');

    // Show a 200x200 Hero tagged 'H', with key routeHeroKey
    final MaterialPageRoute<void> route = MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Material(
          child: ListView(
            children: <Widget>[
              const SizedBox(height: 100.0),
              // This container will appear at Y=100
              Container(
                key: routeContainerKey,
                child: Hero(tag: 'H', child: Container(key: routeHeroKey, height: 200.0, width: 200.0)),
              ),
              TextButton(
                child: const Text('POP'),
                onPressed: () { Navigator.pop(context); },
              ),
              const SizedBox(height: 600.0),
            ],
          ),
        );
      },
    );

    // Show a 100x100 Hero tagged 'H' with key homeHeroKey
    await tester.pumpWidget(
      MaterialApp(
        theme: ThemeData(
          pageTransitionsTheme: const PageTransitionsTheme(
            builders: <TargetPlatform, PageTransitionsBuilder>{
              TargetPlatform.android: FadeUpwardsPageTransitionsBuilder(),
            },
          ),
        ),
        home: Scaffold(
          body: Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return ListView(
                children: <Widget> [
                  const SizedBox(height: 200.0),
                  // This container will appear at Y=200
                  Container(
                    child: Hero(tag: 'H', child: Container(key: homeHeroKey, height: 100.0, width: 100.0)),
                  ),
                  TextButton(
                    child: const Text('PUSH'),
                    onPressed: () { Navigator.push(context, route); },
                  ),
                  const SizedBox(height: 600.0),
                ],
              );
            },
          ),
        ),
      ),
    );

    // Pushes route
    await tester.tap(find.text('PUSH'));
    await tester.pump();
    await tester.pump();

    final double initialY = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
    expect(initialY, 200.0);

    await tester.pump(const Duration(milliseconds: 100));
    final double yAt100ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
    expect(yAt100ms, lessThan(200.0));
    expect(yAt100ms, greaterThan(100.0));

    // Scroll the target upwards by 25 pixels. The Hero flight's Y coordinate
    // will be redirected from 100 to 75.
    await tester.drag(find.byKey(routeContainerKey), const Offset(0.0, -25.0));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 10));
    final double yAt110ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
    expect(yAt110ms, lessThan(yAt100ms));
    expect(yAt110ms, greaterThan(75.0));

    await tester.pump(const Duration(milliseconds: 300));
    await tester.pump();
    final double finalHeroY = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
    expect(finalHeroY, 75.0); // 100 less 25 for the scroll
  });

  testWidgets('Destination hero scrolls out of view mid-flight', (WidgetTester tester) async {
    const Key homeHeroKey = Key('home hero');
    const Key routeHeroKey = Key('route hero');
    const Key routeContainerKey = Key('route hero container');

    // Show a 200x200 Hero tagged 'H', with key routeHeroKey
    final MaterialPageRoute<void> route = MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Material(
          child: ListView(
            cacheExtent: 0.0,
            children: <Widget>[
              const SizedBox(height: 100.0),
              // This container will appear at Y=100
              Container(
                key: routeContainerKey,
                child: Hero(tag: 'H', child: Container(key: routeHeroKey, height: 200.0, width: 200.0)),
              ),
              const SizedBox(height: 800.0),
            ],
          ),
        );
      },
    );

    // Show a 100x100 Hero tagged 'H' with key homeHeroKey
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return ListView(
                children: <Widget> [
                  const SizedBox(height: 200.0),
                  // This container will appear at Y=200
                  Container(
                    child: Hero(tag: 'H', child: Container(key: homeHeroKey, height: 100.0, width: 100.0)),
                  ),
                  TextButton(
                    child: const Text('PUSH'),
                    onPressed: () { Navigator.push(context, route); },
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );

    // Pushes route
    await tester.tap(find.text('PUSH'));
    await tester.pump();
    await tester.pump();

    final double initialY = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
    expect(initialY, 200.0);

    await tester.pump(const Duration(milliseconds: 100));
    final double yAt100ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
    expect(yAt100ms, lessThan(200.0));
    expect(yAt100ms, greaterThan(100.0));

    await tester.drag(find.byKey(routeContainerKey), const Offset(0.0, -400.0));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 10));
    expect(find.byKey(routeContainerKey), findsNothing); // Scrolled off the top

    // Flight continues (the hero will fade out) even though the destination
    // no longer exists.
    final double yAt110ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
    expect(yAt110ms, lessThan(yAt100ms));
    expect(yAt110ms, greaterThan(100.0));

    await tester.pump(const Duration(milliseconds: 300));
    await tester.pump();
    expect(find.byKey(routeHeroKey), findsNothing);
  });

  testWidgets('Aborted flight', (WidgetTester tester) async {
    // See https://github.com/flutter/flutter/issues/5798
    const Key heroABKey = Key('AB hero');
    const Key heroBCKey = Key('BC hero');

    // Show a 150x150 Hero tagged 'BC'
    final MaterialPageRoute<void> routeC = MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Material(
          child: ListView(
            children: <Widget>[
              // This container will appear at Y=0
              Container(
                child: Hero(
                  tag: 'BC',
                  child: Container(
                    key: heroBCKey,
                    height: 150.0,
                    child: const Text('Hero'),
                  ),
                ),
              ),
              const SizedBox(height: 800.0),
            ],
          ),
        );
      },
    );

    // Show a height=200 Hero tagged 'AB' and a height=50 Hero tagged 'BC'
    final MaterialPageRoute<void> routeB = MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Material(
          child: ListView(
            children: <Widget>[
              const SizedBox(height: 100.0),
              // This container will appear at Y=100
              Container(
                child: Hero(
                  tag: 'AB',
                  child: Container(
                    key: heroABKey,
                    height: 200.0,
                    child: const Text('Hero'),
                  ),
                ),
              ),
              TextButton(
                child: const Text('PUSH C'),
                onPressed: () { Navigator.push(context, routeC); },
              ),
              Container(
                child: Hero(
                  tag: 'BC',
                  child: Container(
                    height: 150.0,
                    child: const Text('Hero'),
                  ),
                ),
              ),
              const SizedBox(height: 800.0),
            ],
          ),
        );
      },
    );

    // Show a 100x100 Hero tagged 'AB' with key heroABKey
    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return ListView(
                children: <Widget> [
                  const SizedBox(height: 200.0),
                  // This container will appear at Y=200
                  Container(
                    child: Hero(
                      tag: 'AB',
                      child: Container(
                        height: 100.0,
                        width: 100.0,
                        child: const Text('Hero'),
                      ),
                    ),
                  ),
                  TextButton(
                    child: const Text('PUSH B'),
                    onPressed: () { Navigator.push(context, routeB); },
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );

    // Pushes routeB
    await tester.tap(find.text('PUSH B'));
    await tester.pump();
    await tester.pump();

    final double initialY = tester.getTopLeft(find.byKey(heroABKey)).dy;
    expect(initialY, 200.0);

    await tester.pump(const Duration(milliseconds: 200));
    final double yAt200ms = tester.getTopLeft(find.byKey(heroABKey)).dy;
    // Hero AB is mid flight.
    expect(yAt200ms, lessThan(200.0));
    expect(yAt200ms, greaterThan(100.0));

    // Pushes route C, causes hero AB's flight to abort, hero BC's flight to start
    await tester.tap(find.text('PUSH C'));
    await tester.pump();
    await tester.pump();

    // Hero AB's aborted flight finishes where it was expected although
    // it's been faded out.
    await tester.pump(const Duration(milliseconds: 100));
    expect(tester.getTopLeft(find.byKey(heroABKey)).dy, 100.0);

    bool _isVisible(Element node) {
      bool isVisible = true;
      node.visitAncestorElements((Element ancestor) {
        final RenderObject r = ancestor.renderObject;
        if (r is RenderOpacity && r.opacity == 0) {
          isVisible = false;
          return false;
        }
        return true;
      });
      return isVisible;
    }

    // Of all heroes only one should be visible now.
    final Iterable<Element> elements = find.text('Hero').evaluate();
    expect(elements.where(_isVisible).length, 1);

    // Hero BC's flight finishes normally.
    await tester.pump(const Duration(milliseconds: 300));
    expect(tester.getTopLeft(find.byKey(heroBCKey)).dy, 0.0);
  });

  testWidgets('Stateful hero child state survives flight', (WidgetTester tester) async {
    final MaterialPageRoute<void> route = MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Material(
          child: ListView(
            children: <Widget>[
              const Card(
                child: Hero(
                  tag: 'H',
                  child: SizedBox(
                    height: 200.0,
                    child: MyStatefulWidget(value: '456'),
                  ),
                ),
              ),
              TextButton(
                child: const Text('POP'),
                onPressed: () { Navigator.pop(context); },
              ),
            ],
          ),
        );
      },
    );

    await tester.pumpWidget(
      MaterialApp(
        home: Scaffold(
          body: Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return ListView(
                children: <Widget> [
                  const Card(
                    child: Hero(
                      tag: 'H',
                      child: SizedBox(
                        height: 100.0,
                        child: MyStatefulWidget(value: '456'),
                      ),
                    ),
                  ),
                  TextButton(
                    child: const Text('PUSH'),
                    onPressed: () { Navigator.push(context, route); },
                  ),
                ],
              );
            },
          ),
        ),
      ),
    );

    expect(find.text('456'), findsOneWidget);

    // Push route.
    await tester.tap(find.text('PUSH'));
    await tester.pump();
    await tester.pump();

    // Push flight underway.
    await tester.pump(const Duration(milliseconds: 100));
    // Visible in the hero animation.
    expect(find.text('456'), findsOneWidget);

    // Push flight finished.
    await tester.pump(const Duration(milliseconds: 300));
    expect(find.text('456'), findsOneWidget);

    // Pop route.
    await tester.tap(find.text('POP'));
    await tester.pump();
    await tester.pump();

    // Pop flight underway.
    await tester.pump(const Duration(milliseconds: 100));
    expect(find.text('456'), findsOneWidget);

    // Pop flight finished
    await tester.pump(const Duration(milliseconds: 300));
    expect(find.text('456'), findsOneWidget);

  });

  testWidgets('Hero createRectTween', (WidgetTester tester) async {
    RectTween createRectTween(Rect begin, Rect end) {
      return MaterialRectCenterArcTween(begin: begin, end: end);
    }

    final Map<String, WidgetBuilder> createRectTweenHeroRoutes = <String, WidgetBuilder>{
      '/': (BuildContext context) => Material(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Hero(
              tag: 'a',
              createRectTween: createRectTween,
              child: Container(height: 100.0, width: 100.0, key: firstKey),
            ),
            TextButton(
              child: const Text('two'),
              onPressed: () { Navigator.pushNamed(context, '/two'); },
            ),
          ],
        ),
      ),
      '/two': (BuildContext context) => Material(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 200.0,
              child: TextButton(
                child: const Text('pop'),
                onPressed: () { Navigator.pop(context); },
              ),
            ),
            Hero(
              tag: 'a',
              createRectTween: createRectTween,
              child: Container(height: 200.0, width: 100.0, key: secondKey),
            ),
          ],
        ),
      ),
    };

    await tester.pumpWidget(MaterialApp(routes: createRectTweenHeroRoutes));
    expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0));

    const double epsilon = 0.001;
    const Duration duration = Duration(milliseconds: 300);
    const Curve curve = Curves.fastOutSlowIn;
    final MaterialPointArcTween pushCenterTween = MaterialPointArcTween(
      begin: const Offset(50.0, 50.0),
      end: const Offset(400.0, 300.0),
    );

    await tester.tap(find.text('two'));
    await tester.pump(); // begin navigation

    // Verify that the center of the secondKey Hero flies along the
    // pushCenterTween arc for the push /two flight.

    await tester.pump();
    expect(tester.getCenter(find.byKey(secondKey)), const Offset(50.0, 50.0));

    await tester.pump(duration * 0.25);
    Offset actualHeroCenter = tester.getCenter(find.byKey(secondKey));
    Offset predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.25));
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));

    await tester.pump(duration * 0.25);
    actualHeroCenter = tester.getCenter(find.byKey(secondKey));
    predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.5));
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));

    await tester.pump(duration * 0.25);
    actualHeroCenter = tester.getCenter(find.byKey(secondKey));
    predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.75));
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));

    await tester.pumpAndSettle();
    expect(tester.getCenter(find.byKey(secondKey)), const Offset(400.0, 300.0));

    // Verify that the center of the firstKey Hero flies along the
    // pushCenterTween arc for the pop /two flight.

    await tester.tap(find.text('pop'));
    await tester.pump(); // begin navigation

    final MaterialPointArcTween popCenterTween = MaterialPointArcTween(
      begin: const Offset(400.0, 300.0),
      end: const Offset(50.0, 50.0),
    );
    await tester.pump();
    expect(tester.getCenter(find.byKey(firstKey)), const Offset(400.0, 300.0));

    await tester.pump(duration * 0.25);
    actualHeroCenter = tester.getCenter(find.byKey(firstKey));
    predictedHeroCenter = popCenterTween.lerp(curve.transform(0.25));
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));

    await tester.pump(duration * 0.25);
    actualHeroCenter = tester.getCenter(find.byKey(firstKey));
    predictedHeroCenter = popCenterTween.lerp(curve.transform(0.5));
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));

    await tester.pump(duration * 0.25);
    actualHeroCenter = tester.getCenter(find.byKey(firstKey));
    predictedHeroCenter = popCenterTween.lerp(curve.transform(0.75));
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));

    await tester.pumpAndSettle();
    expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0, 50.0));
  });

  testWidgets('Hero createRectTween for Navigator that is not full screen', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/25272

    RectTween createRectTween(Rect begin, Rect end) {
      return RectTween(begin: begin, end: end);
    }

    final Map<String, WidgetBuilder> createRectTweenHeroRoutes = <String, WidgetBuilder>{
      '/': (BuildContext context) => Material(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Hero(
              tag: 'a',
              createRectTween: createRectTween,
              child: Container(height: 100.0, width: 100.0, key: firstKey),
            ),
            TextButton(
              child: const Text('two'),
              onPressed: () { Navigator.pushNamed(context, '/two'); },
            ),
          ],
        ),
      ),
      '/two': (BuildContext context) => Material(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 200.0,
              child: TextButton(
                child: const Text('pop'),
                onPressed: () { Navigator.pop(context); },
              ),
            ),
            Hero(
              tag: 'a',
              createRectTween: createRectTween,
              child: Container(height: 200.0, width: 100.0, key: secondKey),
            ),
          ],
        ),
      ),
    };

    const double leftPadding = 10.0;

    // MaterialApp and its Navigator are offset from the left
    await tester.pumpWidget(Padding(
      padding: const EdgeInsets.only(left: leftPadding),
      child: MaterialApp(routes: createRectTweenHeroRoutes),
    ));
    expect(tester.getCenter(find.byKey(firstKey)), const Offset(leftPadding + 50.0, 50.0));

    const double epsilon = 0.001;
    const Duration duration = Duration(milliseconds: 300);
    const Curve curve = Curves.fastOutSlowIn;
    final RectTween pushRectTween = RectTween(
      begin: const Rect.fromLTWH(leftPadding, 0.0, 100.0, 100.0),
      end: const Rect.fromLTWH(350.0 + leftPadding / 2, 200.0, 100.0, 200.0),
    );

    await tester.tap(find.text('two'));
    await tester.pump(); // begin navigation

    // Verify that the rect of the secondKey Hero transforms as the
    // pushRectTween rect for the push /two flight.

    await tester.pump();
    expect(tester.getCenter(find.byKey(secondKey)), const Offset(50.0 + leftPadding, 50.0));

    await tester.pump(duration * 0.25);
    Rect actualHeroRect = tester.getRect(find.byKey(secondKey));
    Rect predictedHeroRect = pushRectTween.lerp(curve.transform(0.25));
    expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect));

    await tester.pump(duration * 0.25);
    actualHeroRect = tester.getRect(find.byKey(secondKey));
    predictedHeroRect = pushRectTween.lerp(curve.transform(0.5));
    expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect));

    await tester.pump(duration * 0.25);
    actualHeroRect = tester.getRect(find.byKey(secondKey));
    predictedHeroRect = pushRectTween.lerp(curve.transform(0.75));
    expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect));

    await tester.pumpAndSettle();
    expect(tester.getCenter(find.byKey(secondKey)), const Offset(400.0 + leftPadding / 2, 300.0));

    // Verify that the rect of the firstKey Hero transforms as the
    // pushRectTween rect for the pop /two flight.

    await tester.tap(find.text('pop'));
    await tester.pump(); // begin navigation

    final RectTween popRectTween = RectTween(
      begin: const Rect.fromLTWH(350.0 + leftPadding / 2, 200.0, 100.0, 200.0),
      end: const Rect.fromLTWH(leftPadding, 0.0, 100.0, 100.0),
    );
    await tester.pump();
    expect(tester.getCenter(find.byKey(firstKey)), const Offset(400.0 + leftPadding / 2, 300.0));

    await tester.pump(duration * 0.25);
    actualHeroRect = tester.getRect(find.byKey(firstKey));
    predictedHeroRect = popRectTween.lerp(curve.transform(0.25));
    expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect));

    await tester.pump(duration * 0.25);
    actualHeroRect = tester.getRect(find.byKey(firstKey));
    predictedHeroRect = popRectTween.lerp(curve.transform(0.5));
    expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect));

    await tester.pump(duration * 0.25);
    actualHeroRect = tester.getRect(find.byKey(firstKey));
    predictedHeroRect = popRectTween.lerp(curve.transform(0.75));
    expect(actualHeroRect, within<Rect>(distance: epsilon, from: predictedHeroRect));

    await tester.pumpAndSettle();
    expect(tester.getCenter(find.byKey(firstKey)), const Offset(50.0 + leftPadding, 50.0));
  });


  testWidgets('Pop interrupts push, reverses flight', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(routes: routes));
    await tester.tap(find.text('twoInset'));
    await tester.pump(); // begin navigation from / to /twoInset.

    const double epsilon = 0.001;
    const Duration duration = Duration(milliseconds: 300);

    await tester.pump();
    final double x0 = tester.getTopLeft(find.byKey(secondKey)).dx;

    // Flight begins with the secondKey Hero widget lined up with the firstKey widget.
    expect(x0, 4.0);

    await tester.pump(duration * 0.1);
    final double x1 = tester.getTopLeft(find.byKey(secondKey)).dx;

    await tester.pump(duration * 0.1);
    final double x2 = tester.getTopLeft(find.byKey(secondKey)).dx;

    await tester.pump(duration * 0.1);
    final double x3 = tester.getTopLeft(find.byKey(secondKey)).dx;

    await tester.pump(duration * 0.1);
    final double x4 = tester.getTopLeft(find.byKey(secondKey)).dx;

    // Pop route /twoInset before the push transition from / to /twoInset has finished.
    await tester.tap(find.text('pop'));


    // We expect the hero to take the same path as it did flying from /
    // to /twoInset as it does now, flying from '/twoInset' back to /. The most
    // important checks below are the first (x4) and last (x0): the hero should
    // not jump from where it was when the push transition was interrupted by a
    // pop, and it should end up where the push started.

    await tester.pump();
    expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x4, epsilon));

    await tester.pump(duration * 0.1);
    expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x3, epsilon));

    await tester.pump(duration * 0.1);
    expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x2, epsilon));

    await tester.pump(duration * 0.1);
    expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x1, epsilon));

    await tester.pump(duration * 0.1);
    expect(tester.getTopLeft(find.byKey(secondKey)).dx, closeTo(x0, epsilon));

    // Below: show that a different pop Hero path is in fact taken after
    // a completed push transition.

    // Complete the pop transition and we're back to showing /.
    await tester.pumpAndSettle();
    expect(tester.getTopLeft(find.byKey(firstKey)).dx, 4.0); // Card contents are inset by 4.0.

    // Push /twoInset and wait for the transition to finish.
    await tester.tap(find.text('twoInset'));
    await tester.pumpAndSettle();
    expect(tester.getTopLeft(find.byKey(secondKey)).dx, 54.0);

    // Start the pop transition from /twoInset to /.
    await tester.tap(find.text('pop'));
    await tester.pump();

    // Now the firstKey widget is the flying hero widget and it starts
    // out lined up with the secondKey widget.
    await tester.pump();
    expect(tester.getTopLeft(find.byKey(firstKey)).dx, 54.0);

    // x0-x4 are the top left x coordinates for the beginning 40% of
    // the incoming flight. Advance the outgoing flight to the same
    // place.
    await tester.pump(duration * 0.6);

    await tester.pump(duration * 0.1);
    expect(tester.getTopLeft(find.byKey(firstKey)).dx, isNot(closeTo(x4, epsilon)));

    await tester.pump(duration * 0.1);
    expect(tester.getTopLeft(find.byKey(firstKey)).dx, isNot(closeTo(x3, epsilon)));

    // At this point the flight path arcs do start to get pretty close so
    // there's no point in comparing them.
    await tester.pump(duration * 0.1);

    // After the remaining 40% of the incoming flight is complete, we
    // expect to end up where the outgoing flight started.
    await tester.pump(duration * 0.1);
    expect(tester.getTopLeft(find.byKey(firstKey)).dx, x0);
  });

  testWidgets('Can override flight shuttle', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: ListView(
          children: <Widget>[
            const Hero(tag: 'a', child: Text('foo')),
            Builder(builder: (BuildContext context) {
              return TextButton(
                child: const Text('two'),
                onPressed: () => Navigator.push<void>(context, MaterialPageRoute<void>(
                  builder: (BuildContext context) {
                    return Material(
                      child: Hero(
                        tag: 'a',
                        child: const Text('bar'),
                        flightShuttleBuilder: (
                          BuildContext flightContext,
                          Animation<double> animation,
                          HeroFlightDirection flightDirection,
                          BuildContext fromHeroContext,
                          BuildContext toHeroContext,
                        ) {
                          return const Text('baz');
                        },
                      ),
                    );
                  },
                )),
              );
            }),
          ],
        ),
      ),
    ));

    await tester.tap(find.text('two'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 10));

    expect(find.text('foo'), findsNothing);
    expect(find.text('bar'), findsNothing);
    expect(find.text('baz'), findsOneWidget);
  });

  testWidgets('Can override flight launch pads', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: ListView(
          children: <Widget>[
            Hero(
              tag: 'a',
              child: const Text('Batman'),
              placeholderBuilder: (BuildContext context, Size heroSize, Widget child) {
                return const Text('Venom');
              },
            ),
            Builder(builder: (BuildContext context) {
              return TextButton(
                child: const Text('two'),
                onPressed: () => Navigator.push<void>(context, MaterialPageRoute<void>(
                  builder: (BuildContext context) {
                    return Material(
                      child: Hero(
                        tag: 'a',
                        child: const Text('Wolverine'),
                        placeholderBuilder: (BuildContext context, Size size, Widget child) {
                          return const Text('Joker');
                        },
                      ),
                    );
                  },
                )),
              );
            }),
          ],
        ),
      ),
    ));

    await tester.tap(find.text('two'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 10));

    expect(find.text('Batman'), findsNothing);
    // This shows up once but in the Hero because by default, the destination
    // Hero child is the widget in flight.
    expect(find.text('Wolverine'), findsOneWidget);
    expect(find.text('Venom'), findsOneWidget);
    expect(find.text('Joker'), findsOneWidget);
  });

  testWidgets('Heroes do not transition on back gestures by default', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
     routes: routes,
    ));

    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), findsNothing);

    await tester.tap(find.text('two'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 500));

    expect(find.byKey(firstKey), findsNothing);
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);

    final TestGesture  gesture = await tester.startGesture(const Offset(5.0, 200.0));
    await gesture.moveBy(const Offset(20.0, 0.0));
    await gesture.moveBy(const Offset(180.0, 0.0));
    await gesture.up();
    await tester.pump();

    await tester.pump();

    // Both Heroes exist and are seated in their normal parents.
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);

    // To make sure the hero had all chances of starting.
    await tester.pump(const Duration(milliseconds: 100));
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets('Heroes can transition on gesture in one frame', (WidgetTester tester) async {
    transitionFromUserGestures = true;
    await tester.pumpWidget(MaterialApp(
      routes: routes,
    ));

    await tester.tap(find.text('two'));
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 500));

    expect(find.byKey(firstKey), findsNothing);
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);

    final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0));
    await gesture.moveBy(const Offset(200.0, 0.0));
    await tester.pump();

    // We're going to page 1 so page 1's Hero is lifted into flight.
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isNotInCard);
    expect(find.byKey(secondKey), findsNothing);

    // Move further along.
    await gesture.moveBy(const Offset(500.0, 0.0));
    await tester.pump();

    // Same results.
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isNotInCard);
    expect(find.byKey(secondKey), findsNothing);

    await gesture.up();
    // Finish transition.
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 500));

    // Hero A is back in the card.
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), findsNothing);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets('Heroes animate should hide destination hero and display original hero in case of dismissed', (WidgetTester tester) async {
    transitionFromUserGestures = true;
    await tester.pumpWidget(MaterialApp(
      routes: routes,
    ));

    await tester.tap(find.text('two'));
    await tester.pumpAndSettle();

    expect(find.byKey(firstKey), findsNothing);
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);

    final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0));
    await gesture.moveBy(const Offset(50.0, 0.0));
    await tester.pump();
    // It will only register the drag if we move a second time.
    await gesture.moveBy(const Offset(50.0, 0.0));
    await tester.pump();

    // We're going to page 1 so page 1's Hero is lifted into flight.
    expect(find.byKey(firstKey), isOnstage);
    expect(find.byKey(firstKey), isNotInCard);
    expect(find.byKey(secondKey), findsNothing);

    // Dismisses hero transition.
    await gesture.up();
    await tester.pump();
    await tester.pumpAndSettle();

    // We goes back to second page.
    expect(find.byKey(firstKey), findsNothing);
    expect(find.byKey(secondKey), isOnstage);
    expect(find.byKey(secondKey), isInCard);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  testWidgets('Handles transitions when a non-default initial route is set', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      routes: routes,
      initialRoute: '/two',
    ));
    expect(tester.takeException(), isNull);
    expect(find.text('two'), findsNothing);
    expect(find.text('three'), findsOneWidget);
  });

  testWidgets('Can push/pop on outer Navigator if nested Navigator contains Heroes', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/28042.

    const String heroTag = 'You are my hero!';
    final GlobalKey<NavigatorState> rootNavigator = GlobalKey();
    final GlobalKey<NavigatorState> nestedNavigator = GlobalKey();
    final Key nestedRouteHeroBottom = UniqueKey();
    final Key nestedRouteHeroTop = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        navigatorKey: rootNavigator,
        home: Navigator(
          key: nestedNavigator,
          onGenerateRoute: (RouteSettings settings) {
            return MaterialPageRoute<void>(
              builder: (BuildContext context) {
                return Hero(
                  tag: heroTag,
                  child: Placeholder(
                    key: nestedRouteHeroBottom,
                  ),
                );
              }
            );
          },
        ),
      ),
    );

    nestedNavigator.currentState.push(MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Hero(
          tag: heroTag,
          child: Placeholder(
            key: nestedRouteHeroTop,
          ),
        );
      },
    ));
    await tester.pumpAndSettle();

    // Both heroes are in the tree, one is offstage
    expect(find.byKey(nestedRouteHeroTop), findsOneWidget);
    expect(find.byKey(nestedRouteHeroBottom), findsNothing);
    expect(find.byKey(nestedRouteHeroBottom, skipOffstage: false), findsOneWidget);

    rootNavigator.currentState.push(MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return const Text('Foo');
      },
    ));
    await tester.pumpAndSettle();

    expect(find.text('Foo'), findsOneWidget);
    // Both heroes are still in the tree, both are offstage.
    expect(find.byKey(nestedRouteHeroBottom), findsNothing);
    expect(find.byKey(nestedRouteHeroTop), findsNothing);
    expect(find.byKey(nestedRouteHeroBottom, skipOffstage: false), findsOneWidget);
    expect(find.byKey(nestedRouteHeroTop, skipOffstage: false), findsOneWidget);

    // Doesn't crash.
    expect(tester.takeException(), isNull);

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

    expect(find.text('Foo'), findsNothing);
    // Both heroes are in the tree, one is offstage
    expect(find.byKey(nestedRouteHeroTop), findsOneWidget);
    expect(find.byKey(nestedRouteHeroBottom), findsNothing);
    expect(find.byKey(nestedRouteHeroBottom, skipOffstage: false), findsOneWidget);
  });

  testWidgets('Can hero from route in root Navigator to route in nested Navigator', (WidgetTester tester) async {
    const String heroTag = 'foo';
    final GlobalKey<NavigatorState> rootNavigator = GlobalKey();
    final Key smallContainer = UniqueKey();
    final Key largeContainer = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        navigatorKey: rootNavigator,
        home: Center(
          child: Card(
            child: Hero(
              tag: heroTag,
              child: Container(
                key: largeContainer,
                color: Colors.red,
                height: 200.0,
                width: 200.0,
              ),
            ),
          ),
        ),
      ),
    );


    // The initial setup.
    expect(find.byKey(largeContainer), isOnstage);
    expect(find.byKey(largeContainer), isInCard);
    expect(find.byKey(smallContainer, skipOffstage: false), findsNothing);

    rootNavigator.currentState.push(
      MaterialPageRoute<void>(
        builder: (BuildContext context) {
          return Center(
            child: Card(
              child: Hero(
                tag: heroTag,
                child: Container(
                  key: smallContainer,
                  color: Colors.red,
                  height: 100.0,
                  width: 100.0,
                ),
              ),
            ),
          );
        }
      ),
    );
    await tester.pump();

    // The second route exists offstage.
    expect(find.byKey(largeContainer), isOnstage);
    expect(find.byKey(largeContainer), isInCard);
    expect(find.byKey(smallContainer, skipOffstage: false), isOffstage);
    expect(find.byKey(smallContainer, skipOffstage: false), isInCard);

    await tester.pump();

    // The hero started flying.
    expect(find.byKey(largeContainer), findsNothing);
    expect(find.byKey(smallContainer), isOnstage);
    expect(find.byKey(smallContainer), isNotInCard);

    await tester.pump(const Duration(milliseconds: 100));

    // The hero is in-flight.
    expect(find.byKey(largeContainer), findsNothing);
    expect(find.byKey(smallContainer), isOnstage);
    expect(find.byKey(smallContainer), isNotInCard);
    final Size size = tester.getSize(find.byKey(smallContainer));
    expect(size.height, greaterThan(100));
    expect(size.width, greaterThan(100));
    expect(size.height, lessThan(200));
    expect(size.width, lessThan(200));

    await tester.pumpAndSettle();

    // The transition has ended.
    expect(find.byKey(largeContainer), findsNothing);
    expect(find.byKey(smallContainer), isOnstage);
    expect(find.byKey(smallContainer), isInCard);
    expect(tester.getSize(find.byKey(smallContainer)), const Size(100,100));
  });

  testWidgets('Hero within a Hero, throws', (WidgetTester tester) async {
    await tester.pumpWidget(
      const MaterialApp(
        home: Material(
          child: Hero(
            tag: 'a',
            child: Hero(
              tag: 'b',
              child: Text('Child of a Hero'),
            ),
          ),
        ),
      ),
    );

    expect(tester.takeException(), isAssertionError);
  });

  testWidgets('Hero within a Hero subtree, throws', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Container(
            child: const Hero(
              tag: 'a',
              child: Hero(
                tag: 'b',
                child: Text('Child of a Hero'),
              ),
            ),
          ),
        ),
      ),
    );

    expect(tester.takeException(), isAssertionError);
  });

  testWidgets('Hero within a Hero subtree with Builder, throws', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Hero(
            tag: 'a',
            child: Builder(
              builder: (BuildContext context) {
                return const Hero(
                  tag: 'b',
                  child: Text('Child of a Hero'),
                );
              },
            ),
          ),
        ),
      ),
    );

    expect(tester.takeException(),isAssertionError);
  });

  testWidgets('Hero within a Hero subtree with LayoutBuilder, throws', (WidgetTester tester) async {
    await tester.pumpWidget(
      MaterialApp(
        home: Material(
          child: Hero(
            tag: 'a',
            child: LayoutBuilder(
              builder: (BuildContext context, BoxConstraints constraints) {
                return const Hero(
                  tag: 'b',
                  child: Text('Child of a Hero'),
                );
              },
            ),
          ),
        ),
      ),
    );

    expect(tester.takeException(), isAssertionError);
  });

  testWidgets('Heroes fly on pushReplacement', (WidgetTester tester) async {
    // Regression test for https://github.com/flutter/flutter/issues/28041.

    const String heroTag = 'foo';
    final GlobalKey<NavigatorState> navigator = GlobalKey();
    final Key smallContainer = UniqueKey();
    final Key largeContainer = UniqueKey();

    await tester.pumpWidget(
      MaterialApp(
        navigatorKey: navigator,
        home: Center(
          child: Card(
            child: Hero(
              tag: heroTag,
              child: Container(
                key: largeContainer,
                color: Colors.red,
                height: 200.0,
                width: 200.0,
              ),
            ),
          ),
        ),
      ),
    );

    // The initial setup.
    expect(find.byKey(largeContainer), isOnstage);
    expect(find.byKey(largeContainer), isInCard);
    expect(find.byKey(smallContainer, skipOffstage: false), findsNothing);

    navigator.currentState.pushReplacement(
      MaterialPageRoute<void>(
          builder: (BuildContext context) {
            return Center(
              child: Card(
                child: Hero(
                  tag: heroTag,
                  child: Container(
                    key: smallContainer,
                    color: Colors.red,
                    height: 100.0,
                    width: 100.0,
                  ),
                ),
              ),
            );
          }
      ),
    );
    await tester.pump();

    // The second route exists offstage.
    expect(find.byKey(largeContainer), isOnstage);
    expect(find.byKey(largeContainer), isInCard);
    expect(find.byKey(smallContainer, skipOffstage: false), isOffstage);
    expect(find.byKey(smallContainer, skipOffstage: false), isInCard);

    await tester.pump();

    // The hero started flying.
    expect(find.byKey(largeContainer), findsNothing);
    expect(find.byKey(smallContainer), isOnstage);
    expect(find.byKey(smallContainer), isNotInCard);

    await tester.pump(const Duration(milliseconds: 100));

    // The hero is in-flight.
    expect(find.byKey(largeContainer), findsNothing);
    expect(find.byKey(smallContainer), isOnstage);
    expect(find.byKey(smallContainer), isNotInCard);
    final Size size = tester.getSize(find.byKey(smallContainer));
    expect(size.height, greaterThan(100));
    expect(size.width, greaterThan(100));
    expect(size.height, lessThan(200));
    expect(size.width, lessThan(200));

    await tester.pumpAndSettle();

    // The transition has ended.
    expect(find.byKey(largeContainer), findsNothing);
    expect(find.byKey(smallContainer), isOnstage);
    expect(find.byKey(smallContainer), isInCard);
    expect(tester.getSize(find.byKey(smallContainer)), const Size(100,100));
  });

  testWidgets('On an iOS back swipe and snap, only a single flight should take place', (WidgetTester tester) async {
    int shuttlesBuilt = 0;
    final HeroFlightShuttleBuilder shuttleBuilder = (
      BuildContext flightContext,
      Animation<double> animation,
      HeroFlightDirection flightDirection,
      BuildContext fromHeroContext,
      BuildContext toHeroContext,
    ) {
      shuttlesBuilt += 1;
      return const Text("I'm flying in a jetplane");
    };

    final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
    await tester.pumpWidget(
      CupertinoApp(
        navigatorKey: navigatorKey,
        home: Hero(
          tag: navigatorKey,
          // Since we're popping, only the destination route's builder is used.
          flightShuttleBuilder: shuttleBuilder,
          transitionOnUserGestures: true,
          child: const Text('1'),
        ),
      ),
    );

    final CupertinoPageRoute<void> route2 = CupertinoPageRoute<void>(
      builder: (BuildContext context) {
        return CupertinoPageScaffold(
          child: Hero(
            tag: navigatorKey,
            transitionOnUserGestures: true,
            child: const Text('2'),
          ),
        );
      }
    );

    navigatorKey.currentState.push(route2);
    await tester.pumpAndSettle();

    expect(shuttlesBuilt, 1);

    final TestGesture gesture = await tester.startGesture(const Offset(5.0, 200.0));
    await gesture.moveBy(const Offset(500.0, 0.0));
    await tester.pump();
    // Starting the back swipe creates a new hero shuttle.
    expect(shuttlesBuilt, 2);

    await gesture.up();
    await tester.pump();
    // After the lift, no additional shuttles should be created since it's the
    // same hero flight.
    expect(shuttlesBuilt, 2);

    // Did go far enough to snap out of this route.
    await tester.pump(const Duration(milliseconds: 301));
    expect(find.text('2'), findsNothing);
    // Still one shuttle.
    expect(shuttlesBuilt, 2);
  });

  testWidgets("From hero's state should be preserved, "
    'heroes work well with child widgets that has global keys',
    (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
      final GlobalKey<_SimpleState> key1 = GlobalKey<_SimpleState>();
      final GlobalKey key2 = GlobalKey();

      await tester.pumpWidget(
        CupertinoApp(
          navigatorKey: navigatorKey,
          home: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Hero(
                tag: 'hero',
                transitionOnUserGestures: true,
                child: _SimpleStatefulWidget(key: key1),
              ),
              const SizedBox(
                width: 10,
                height: 10,
                child: Text('1'),
              ),
            ],
          ),
        ),
      );

      final CupertinoPageRoute<void> route2 = CupertinoPageRoute<void>(
        builder: (BuildContext context) {
          return CupertinoPageScaffold(
            child: Hero(
              tag: 'hero',
              transitionOnUserGestures: true,
              // key2 is a `GlobalKey`. The hero animation should not
              // assert by having the same global keyed widget in more
              // than one place in the tree.
              child: _SimpleStatefulWidget(key: key2),
            ),
          );
        }
      );

      final _SimpleState state1 = key1.currentState;
      state1.state = 1;

      navigatorKey.currentState.push(route2);
      await tester.pump();

      expect(state1.mounted, isTrue);

      await tester.pumpAndSettle();
      expect(state1.state, 1);
      // The element should be mounted and unique.
      expect(state1.mounted, isTrue);

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

      // State is preserved.
      expect(state1.state, 1);
      // The element should be mounted and unique.
      expect(state1.mounted, isTrue);
  });

  testWidgets("Hero works with images that don't have both width and height specified",
    // Regression test for https://github.com/flutter/flutter/issues/32356
    // and https://github.com/flutter/flutter/issues/31503
    (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
      const Key imageKey1 = Key('image1');
      const Key imageKey2 = Key('image2');
      final TestImageProvider imageProvider = TestImageProvider(testImage);

      await tester.pumpWidget(
        CupertinoApp(
          navigatorKey: navigatorKey,
          home: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              Hero(
                tag: 'hero',
                transitionOnUserGestures: true,
                child: Container(
                  width: 100,
                  child: Image(
                    image: imageProvider,
                    key: imageKey1,
                  ),
                ),
              ),
              const SizedBox(
                width: 10,
                height: 10,
                child: Text('1'),
              ),
            ],
          ),
        ),
      );

      final CupertinoPageRoute<void> route2 = CupertinoPageRoute<void>(
        builder: (BuildContext context) {
          return CupertinoPageScaffold(
            child: Hero(
              tag: 'hero',
              transitionOnUserGestures: true,
              child: Container(
                child: Image(
                  image: imageProvider,
                  key: imageKey2,
                ),
              ),
            ),
          );
        }
      );

      // Load image before measuring the `Rect` of the `RenderImage`.
      imageProvider.complete();
      await tester.pump();
      final RenderImage renderImage = tester.renderObject(
        find.descendant(of: find.byKey(imageKey1), matching: find.byType(RawImage))
      );

      // Before push image1 should be laid out correctly.
      expect(renderImage.size, const Size(100, 100));

      navigatorKey.currentState.push(route2);
      await tester.pump();

      final TestGesture gesture = await tester.startGesture(const Offset(0.01, 300));
      await tester.pump();

      // Move (almost) across the screen, to make the animation as close to finish
      // as possible.
      await gesture.moveTo(const Offset(800, 200));
      await tester.pump();

      // image1 should snap to the top left corner of the Row widget.
      expect(
        tester.getRect(find.byKey(imageKey1, skipOffstage: false)),
        rectMoreOrLessEquals(tester.getTopLeft(find.widgetWithText(Row, '1')) & const Size(100, 100), epsilon: 0.01),
      );

      // Text should respect the correct final size of image1.
      expect(
        tester.getTopRight(find.byKey(imageKey1, skipOffstage: false)).dx,
        moreOrLessEquals(tester.getTopLeft(find.text('1')).dx, epsilon: 0.01),
      );
    },
  );

  // Regression test for https://github.com/flutter/flutter/issues/38183.
  testWidgets('Remove user gesture driven flights when the gesture is invalid', (WidgetTester tester) async {
    transitionFromUserGestures = true;
    await tester.pumpWidget(MaterialApp(
      routes: routes,
    ));

    await tester.tap(find.text('simple'));
    await tester.pump();
    await tester.pumpAndSettle();

    expect(find.byKey(simpleKey), findsOneWidget);

    // Tap once to trigger a flight.
    await tester.tapAt(const Offset(10, 200));
    await tester.pumpAndSettle();

    // Wait till the previous gesture is accepted.
    await tester.pump(const Duration(milliseconds: 500));

    // Tap again to trigger another flight, see if it throws.
    await tester.tapAt(const Offset(10, 200));
    await tester.pumpAndSettle();

    // The simple route should still be on top.
    expect(find.byKey(simpleKey), findsOneWidget);
    expect(tester.takeException(), isNull);
  }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS,  TargetPlatform.macOS }));

  // Regression test for https://github.com/flutter/flutter/issues/40239.
  testWidgets(
    'In a pop transition, when fromHero is null, the to hero should eventually become visible',
    (WidgetTester tester) async {
      final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
      StateSetter setState;
      bool shouldDisplayHero = true;
      await tester.pumpWidget(
        CupertinoApp(
          navigatorKey: navigatorKey,
          home: Hero(
            tag: navigatorKey,
            child: const Placeholder(),
          ),
        ),
      );

      final CupertinoPageRoute<void> route2 = CupertinoPageRoute<void>(
        builder: (BuildContext context) {
          return CupertinoPageScaffold(
            child: StatefulBuilder(
              builder: (BuildContext context, StateSetter setter) {
                setState = setter;
                return shouldDisplayHero
                  ? Hero(tag: navigatorKey, child: const Text('text'))
                  : const SizedBox();
              },
            ),
          );
        },
      );

      navigatorKey.currentState.push(route2);
      await tester.pumpAndSettle();

      expect(find.text('text'), findsOneWidget);
      expect(find.byType(Placeholder), findsNothing);

      setState(() { shouldDisplayHero = false; });
      await tester.pumpAndSettle();

      expect(find.text('text'), findsNothing);

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

      expect(find.byType(Placeholder), findsOneWidget);
    },
  );

  testWidgets('popped hero uses fastOutSlowIn curve', (WidgetTester tester) async {
    final Key container1 = UniqueKey();
    final Key container2 = UniqueKey();
    final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();

    final Animatable<Size> tween = SizeTween(
      begin: const Size(200, 200),
      end: const Size(100, 100),
    ).chain(CurveTween(curve: Curves.fastOutSlowIn));


    await tester.pumpWidget(
      MaterialApp(
        navigatorKey: navigator,
        home: Scaffold(
          body: Center(
            child: Hero(
              tag: 'test',
              createRectTween: (Rect begin, Rect end) {
                return RectTween(begin: begin, end: end);
              },
              child: Container(
                key: container1,
                height: 100,
                width: 100,
              ),
            ),
          ),
        ),
      ),
    );
    final Size originalSize = tester.getSize(find.byKey(container1));
    expect(originalSize, const Size(100, 100));

    navigator.currentState.push(MaterialPageRoute<void>(builder: (BuildContext context) {
      return Scaffold(
        body: Center(
          child: Hero(
            tag: 'test',
            createRectTween: (Rect begin, Rect end) {
              return RectTween(begin: begin, end: end);
            },
            child: Container(
              key: container2,
              height: 200,
              width: 200,
            ),
          ),
        ),
      );
    }));
    await tester.pumpAndSettle();
    final Size newSize = tester.getSize(find.byKey(container2));
    expect(newSize, const Size(200, 200));

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

    // Jump 25% into the transition (total length = 300ms)
    await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms
    Size heroSize = tester.getSize(find.byKey(container1));
    expect(heroSize, tween.transform(0.25));

    // Jump to 50% into the transition.
    await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms
    heroSize = tester.getSize(find.byKey(container1));
    expect(heroSize, tween.transform(0.50));

    // Jump to 75% into the transition.
    await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms
    heroSize = tester.getSize(find.byKey(container1));
    expect(heroSize, tween.transform(0.75));

    // Jump to 100% into the transition.
    await tester.pump(const Duration(milliseconds: 75)); // 25% of 300ms
    heroSize = tester.getSize(find.byKey(container1));
    expect(heroSize, tween.transform(1.0));
  });
}
