heroes_test.dart 49.8 KB
Newer Older
Hixie's avatar
Hixie committed
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

Adam Barth's avatar
Adam Barth committed
5
import 'package:flutter_test/flutter_test.dart';
6
import 'package:flutter/material.dart';
7
import 'package:flutter/rendering.dart';
8

9 10 11
Key firstKey = const Key('first');
Key secondKey = const Key('second');
Key thirdKey = const Key('third');
12

13 14 15 16
Key homeRouteKey = const Key('homeRoute');
Key routeTwoKey = const Key('routeTwo');
Key routeThreeKey = const Key('routeThree');

xster's avatar
xster committed
17 18
bool transitionFromUserGestures = false;

19
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
20 21
  '/': (BuildContext context) => Material(
    child: ListView(
22
      key: homeRouteKey,
23
      children: <Widget>[
24
        Container(height: 100.0, width: 100.0),
xster's avatar
xster committed
25 26 27 28 29
        Card(child: Hero(
          tag: 'a',
          transitionOnUserGestures: transitionFromUserGestures,
          child: Container(height: 100.0, width: 100.0, key: firstKey),
        )),
30 31
        Container(height: 100.0, width: 100.0),
        FlatButton(
32
          child: const Text('two'),
33 34
          onPressed: () { Navigator.pushNamed(context, '/two'); }
        ),
35
        FlatButton(
36 37 38
          child: const Text('twoInset'),
          onPressed: () { Navigator.pushNamed(context, '/twoInset'); }
        ),
39 40
      ]
    )
41
  ),
42 43
  '/two': (BuildContext context) => Material(
    child: ListView(
44
      key: routeTwoKey,
45
      children: <Widget>[
46
        FlatButton(
47
          child: const Text('pop'),
48 49
          onPressed: () { Navigator.pop(context); }
        ),
50
        Container(height: 150.0, width: 150.0),
xster's avatar
xster committed
51 52 53 54 55
        Card(child: Hero(
          tag: 'a',
          transitionOnUserGestures: transitionFromUserGestures,
          child: Container(height: 150.0, width: 150.0, key: secondKey),
        )),
56 57
        Container(height: 150.0, width: 150.0),
        FlatButton(
58
          child: const Text('three'),
59
          onPressed: () { Navigator.push(context, ThreeRoute()); },
60
        ),
61 62
      ]
    )
63
  ),
64 65 66 67
  // 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 /.
68 69
  '/twoInset': (BuildContext context) => Material(
    child: ListView(
70 71
      key: routeTwoKey,
      children: <Widget>[
72
        FlatButton(
73 74 75
          child: const Text('pop'),
          onPressed: () { Navigator.pop(context); }
        ),
76 77 78
        Container(height: 150.0, width: 150.0),
        Card(
          child: Padding(
79
            padding: const EdgeInsets.only(left: 50.0),
xster's avatar
xster committed
80 81 82 83 84
            child: Hero(
              tag: 'a',
              transitionOnUserGestures: transitionFromUserGestures,
              child: Container(height: 150.0, width: 150.0, key: secondKey),
            )
85 86
          ),
        ),
87 88
        Container(height: 150.0, width: 150.0),
        FlatButton(
89
          child: const Text('three'),
90
          onPressed: () { Navigator.push(context, ThreeRoute()); },
91 92 93 94
        ),
      ]
    )
  ),
95
};
96

97
class ThreeRoute extends MaterialPageRoute<void> {
98
  ThreeRoute() : super(builder: (BuildContext context) {
99
    return Material(
100
      key: routeThreeKey,
101
      child: ListView(
102
        children: <Widget>[
103 104 105
          Container(height: 200.0, width: 200.0),
          Card(child: Hero(tag: 'a', child: Container(height: 200.0, width: 200.0, key: thirdKey))),
          Container(height: 200.0, width: 200.0),
106 107
        ]
      )
108 109 110 111
    );
  });
}

112
class MutatingRoute extends MaterialPageRoute<void> {
113
  MutatingRoute() : super(builder: (BuildContext context) {
114
    return Hero(tag: 'a', child: const Text('MutatingRoute'), key: UniqueKey());
115 116 117 118 119 120 121 122 123
  });

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

124
class MyStatefulWidget extends StatefulWidget {
125
  const MyStatefulWidget({ Key key, this.value = '123' }) : super(key: key);
126 127
  final String value;
  @override
128
  MyStatefulWidgetState createState() => MyStatefulWidgetState();
129 130 131 132
}

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

136
void main() {
xster's avatar
xster committed
137 138 139 140
  setUp(() {
    transitionFromUserGestures = false;
  });

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

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

145
    // the initial setup.
146

147
    expect(find.byKey(firstKey), isOnstage);
148 149
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), findsNothing);
150

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

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

157
    expect(find.byKey(firstKey), isOnstage);
158
    expect(find.byKey(firstKey), isInCard);
159 160
    expect(find.byKey(secondKey, skipOffstage: false), isOffstage);
    expect(find.byKey(secondKey, skipOffstage: false), isInCard);
161

162
    await tester.pump();
163

164 165
    // 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.
166

167
    expect(find.byKey(firstKey), findsNothing);
168
    expect(find.byKey(secondKey), isOnstage);
169
    expect(find.byKey(secondKey), isNotInCard);
170

171
    await tester.pump();
172

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

175
    expect(find.byKey(firstKey), findsNothing);
176
    expect(find.byKey(secondKey), isOnstage);
177
    expect(find.byKey(secondKey), isNotInCard);
178

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

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

185
    expect(find.byKey(firstKey), findsNothing);
186
    expect(find.byKey(secondKey), isOnstage);
187
    expect(find.byKey(secondKey), isInCard);
188

189
    await tester.pump();
190

191
    // Should not change anything.
192

193
    expect(find.byKey(firstKey), findsNothing);
194
    expect(find.byKey(secondKey), isOnstage);
195
    expect(find.byKey(secondKey), isInCard);
196

197
    // Now move on to view 3
198

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

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

205
    expect(find.byKey(secondKey), isOnstage);
206
    expect(find.byKey(secondKey), isInCard);
207 208
    expect(find.byKey(thirdKey, skipOffstage: false), isOffstage);
    expect(find.byKey(thirdKey, skipOffstage: false), isInCard);
209

210
    await tester.pump();
211

212 213
    // 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.
214

215
    expect(find.byKey(secondKey), findsNothing);
216
    expect(find.byKey(thirdKey), isOnstage);
217
    expect(find.byKey(thirdKey), isNotInCard);
218

219
    await tester.pump();
220

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

223
    expect(find.byKey(secondKey), findsNothing);
224
    expect(find.byKey(thirdKey), isOnstage);
225
    expect(find.byKey(thirdKey), isNotInCard);
226

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

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

232
    expect(find.byKey(secondKey), findsNothing);
233
    expect(find.byKey(thirdKey), isOnstage);
234
    expect(find.byKey(thirdKey), isInCard);
235

236
    await tester.pump();
237

238
    // Should not change anything.
239

240
    expect(find.byKey(secondKey), findsNothing);
241
    expect(find.byKey(thirdKey), isOnstage);
242
    expect(find.byKey(thirdKey), isInCard);
243
  });
244

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

248 249 250
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: ListView(
251
          children: <Widget>[
252
            const Hero(tag: 'a', child: Text('foo')),
253 254
            Builder(builder: (BuildContext context) {
              return FlatButton(child: const Text('two'), onPressed: () => Navigator.push(context, route));
255 256 257 258
            })
          ]
        )
      )
259 260 261
    ));

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

    route.markNeedsBuild();

266 267
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(seconds: 1));
268
  });
269

270
  testWidgets('Heroes animation is fastOutSlowIn', (WidgetTester tester) async {
271
    await tester.pumpWidget(MaterialApp(routes: routes));
272 273 274 275 276 277
    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.

278
    const Duration duration = Duration(milliseconds: 300);
279
    const Curve curve = Curves.fastOutSlowIn;
280 281
    final double initialHeight = tester.getSize(find.byKey(firstKey, skipOffstage: false)).height;
    final double finalHeight = tester.getSize(find.byKey(secondKey, skipOffstage: false)).height;
282
    final double deltaHeight = finalHeight - initialHeight;
283
    const double epsilon = 0.001;
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309

    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)
    );
  });

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

313 314 315
    await tester.pumpWidget(MaterialApp(
      home: Center(
        child: Hero(
316
          tag: 'foo',
317
          child: GestureDetector(
318 319 320
            onTap: () {
              log.add('foo');
            },
321
            child: Container(
322 323
              width: 100.0,
              height: 100.0,
324
              child: const Text('foo')
325 326 327 328 329 330
            )
          )
        )
      ),
      routes: <String, WidgetBuilder>{
        '/next': (BuildContext context) {
331
          return Align(
332
            alignment: Alignment.topLeft,
333
            child: Hero(
334
              tag: 'foo',
335
              child: GestureDetector(
336 337 338
                onTap: () {
                  log.add('bar');
                },
339
                child: Container(
340 341
                  width: 100.0,
                  height: 150.0,
342
                  child: const Text('bar')
343 344 345 346 347 348 349 350 351 352 353 354 355
                )
              )
            )
          );
        }
      }
    ));

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

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

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

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

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

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

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

391
    final Finder heroes = find.byType(Hero);
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    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)

    // this is verifying that there's no crash

    // TODO(ianh): once https://github.com/flutter/flutter/issues/5631 is fixed, remove this line:
    await tester.pump(const Duration(hours: 1));
  });

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

417
    final Finder heroes = find.byType(Hero);
418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
    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();

    // this is verifying that there's no crash

    // TODO(ianh): once https://github.com/flutter/flutter/issues/5631 is fixed, remove this line:
    await tester.pump(const Duration(hours: 1));
  });
Hans Muller's avatar
Hans Muller committed
439 440

  testWidgets('One route, two heroes, same tag, throws', (WidgetTester tester) async {
441 442 443
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: ListView(
Hans Muller's avatar
Hans Muller committed
444
          children: <Widget>[
445 446
            const Hero(tag: 'a', child: Text('a')),
            const Hero(tag: 'a', child: Text('a too')),
447
            Builder(
Hans Muller's avatar
Hans Muller committed
448
              builder: (BuildContext context) {
449
                return FlatButton(
450
                  child: const Text('push'),
Hans Muller's avatar
Hans Muller committed
451
                  onPressed: () {
452
                    Navigator.push(context, PageRouteBuilder<void>(
Hans Muller's avatar
Hans Muller committed
453
                      pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
454
                        return const Text('fail');
Hans Muller's avatar
Hans Muller committed
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
                      },
                    ));
                  },
                );
              },
            ),
          ],
        ),
      ),
    ));

    await tester.tap(find.text('push'));
    await tester.pump();
    expect(tester.takeException(), isFlutterError);
  });

471
  testWidgets('Hero push transition interrupted by a pop', (WidgetTester tester) async {
472
    await tester.pumpWidget(MaterialApp(
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524
      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.
525
    const double epsilon = 0.001;
526 527 528 529 530 531 532 533 534 535 536 537
    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(
538
      MaterialApp(routes: routes)
539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
    );

    // 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));
589 590 591
    final double height200ms = tester.getSize(find.byKey(firstKey)).height;
    expect(height200ms, greaterThan(height150ms));
    expect(finalHeight, lessThan(height200ms));
592 593 594

    // 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.
595
    const double epsilon = 0.001;
596 597 598 599 600 601 602 603 604
    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);
  });
605 606

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

    // Show a 200x200 Hero tagged 'H', with key routeHeroKey
613
    final MaterialPageRoute<void> route = MaterialPageRoute<void>(
614
      builder: (BuildContext context) {
615 616
        return Material(
          child: ListView(
617
            children: <Widget>[
618
              StatefulBuilder(
619 620
                builder: (BuildContext context, StateSetter setState) {
                  heroCardSetState = setState;
621
                  return Card(
622
                    child: routeIncludesHero
623 624
                      ? Hero(tag: 'H', child: Container(key: routeHeroKey, height: 200.0, width: 200.0))
                      : Container(height: 200.0, width: 200.0),
625 626 627
                  );
                },
              ),
628
              FlatButton(
629
                child: const Text('POP'),
630 631 632 633 634 635 636 637 638 639
                onPressed: () { Navigator.pop(context); }
              ),
            ],
          )
        );
      },
    );

    // Show a 100x100 Hero tagged 'H' with key homeHeroKey
    await tester.pumpWidget(
640 641 642
      MaterialApp(
        home: Scaffold(
          body: Builder(
643
            builder: (BuildContext context) { // Navigator.push() needs context
644
              return ListView(
645
                children: <Widget> [
646 647
                  Card(
                    child: Hero(tag: 'H', child: Container(key: homeHeroKey, height: 100.0, width: 100.0)),
648
                  ),
649
                  FlatButton(
650
                    child: const Text('PUSH'),
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696
                    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));

Josh Soref's avatar
Josh Soref committed
697
    // Remove the destination hero midflight
698 699 700 701 702 703 704 705 706 707
    heroCardSetState(() {
      routeIncludesHero = false;
    });
    await tester.pump();

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

  });
708 709

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

    // Show a 200x200 Hero tagged 'H', with key routeHeroKey
715
    final MaterialPageRoute<void> route = MaterialPageRoute<void>(
716
      builder: (BuildContext context) {
717 718
        return Material(
          child: ListView(
719
            children: <Widget>[
720
              const SizedBox(height: 100.0),
721
              // This container will appear at Y=100
722
              Container(
723
                key: routeContainerKey,
724
                child: Hero(tag: 'H', child: Container(key: routeHeroKey, height: 200.0, width: 200.0))
725
              ),
726
              FlatButton(
727
                child: const Text('POP'),
728 729
                onPressed: () { Navigator.pop(context); }
              ),
730
              const SizedBox(height: 600.0),
731 732 733 734 735 736 737 738
            ],
          )
        );
      },
    );

    // Show a 100x100 Hero tagged 'H' with key homeHeroKey
    await tester.pumpWidget(
739 740 741
      MaterialApp(
        home: Scaffold(
          body: Builder(
742
            builder: (BuildContext context) { // Navigator.push() needs context
743
              return ListView(
744
                children: <Widget> [
745
                  const SizedBox(height: 200.0),
746
                  // This container will appear at Y=200
747 748
                  Container(
                    child: Hero(tag: 'H', child: Container(key: homeHeroKey, height: 100.0, width: 100.0)),
749
                  ),
750
                  FlatButton(
751
                    child: const Text('PUSH'),
752 753
                    onPressed: () { Navigator.push(context, route); }
                  ),
754
                  const SizedBox(height: 600.0),
755 756 757 758 759 760 761 762 763 764 765 766 767
                ],
              );
            },
          ),
        ),
      )
    );

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

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

    await tester.pump(const Duration(milliseconds: 100));
772
    final double yAt100ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
773 774 775 776 777
    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.
778
    await tester.drag(find.byKey(routeContainerKey), const Offset(0.0, -25.0));
779 780
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 10));
781
    final double yAt110ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
782 783 784 785 786
    expect(yAt110ms, lessThan(yAt100ms));
    expect(yAt110ms, greaterThan(75.0));

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

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

    // Show a 200x200 Hero tagged 'H', with key routeHeroKey
797
    final MaterialPageRoute<void> route = MaterialPageRoute<void>(
798
      builder: (BuildContext context) {
799 800
        return Material(
          child: ListView(
801
            cacheExtent: 0.0,
802
            children: <Widget>[
803
              const SizedBox(height: 100.0),
804
              // This container will appear at Y=100
805
              Container(
806
                key: routeContainerKey,
807
                child: Hero(tag: 'H', child: Container(key: routeHeroKey, height: 200.0, width: 200.0))
808
              ),
809
              const SizedBox(height: 800.0),
810 811 812 813 814 815 816 817
            ],
          )
        );
      },
    );

    // Show a 100x100 Hero tagged 'H' with key homeHeroKey
    await tester.pumpWidget(
818 819 820
      MaterialApp(
        home: Scaffold(
          body: Builder(
821
            builder: (BuildContext context) { // Navigator.push() needs context
822
              return ListView(
823
                children: <Widget> [
824
                  const SizedBox(height: 200.0),
825
                  // This container will appear at Y=200
826 827
                  Container(
                    child: Hero(tag: 'H', child: Container(key: homeHeroKey, height: 100.0, width: 100.0)),
828
                  ),
829
                  FlatButton(
830
                    child: const Text('PUSH'),
831 832 833 834 835 836 837 838 839 840 841 842 843 844 845
                    onPressed: () { Navigator.push(context, route); }
                  ),
                ],
              );
            },
          ),
        ),
      )
    );

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

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

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

854
    await tester.drag(find.byKey(routeContainerKey), const Offset(0.0, -400.0));
855 856 857 858 859 860
    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.
861
    final double yAt110ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
862 863 864 865 866 867 868 869
    expect(yAt110ms, lessThan(yAt100ms));
    expect(yAt110ms, greaterThan(100.0));

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

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

    // Show a 150x150 Hero tagged 'BC'
876
    final MaterialPageRoute<void> routeC = MaterialPageRoute<void>(
877
      builder: (BuildContext context) {
878 879
        return Material(
          child: ListView(
880 881
            children: <Widget>[
              // This container will appear at Y=0
882 883
              Container(
                child: Hero(tag: 'BC', child: Container(key: heroBCKey, height: 150.0))
884
              ),
885
              const SizedBox(height: 800.0),
886 887 888 889 890 891 892
            ],
          )
        );
      },
    );

    // Show a height=200 Hero tagged 'AB' and a height=50 Hero tagged 'BC'
893
    final MaterialPageRoute<void> routeB = MaterialPageRoute<void>(
894
      builder: (BuildContext context) {
895 896
        return Material(
          child: ListView(
897
            children: <Widget>[
898
              const SizedBox(height: 100.0),
899
              // This container will appear at Y=100
900 901
              Container(
                child: Hero(tag: 'AB', child: Container(key: heroABKey, height: 200.0))
902
              ),
903
              FlatButton(
904
                child: const Text('PUSH C'),
905 906
                onPressed: () { Navigator.push(context, routeC); }
              ),
907 908
              Container(
                child: Hero(tag: 'BC', child: Container(height: 150.0))
909
              ),
910
              const SizedBox(height: 800.0),
911 912 913 914 915 916 917 918
            ],
          )
        );
      },
    );

    // Show a 100x100 Hero tagged 'AB' with key heroABKey
    await tester.pumpWidget(
919 920 921
      MaterialApp(
        home: Scaffold(
          body: Builder(
922
            builder: (BuildContext context) { // Navigator.push() needs context
923
              return ListView(
924
                children: <Widget> [
925
                  const SizedBox(height: 200.0),
926
                  // This container will appear at Y=200
927 928
                  Container(
                    child: Hero(tag: 'AB', child: Container(height: 100.0, width: 100.0)),
929
                  ),
930
                  FlatButton(
931
                    child: const Text('PUSH B'),
932 933 934 935 936 937 938 939 940 941 942 943 944 945 946
                    onPressed: () { Navigator.push(context, routeB); }
                  ),
                ],
              );
            },
          ),
        ),
      )
    );

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

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

    await tester.pump(const Duration(milliseconds: 200));
951
    final double yAt200ms = tester.getTopLeft(find.byKey(heroABKey)).dy;
952 953 954 955 956 957 958 959 960 961 962 963
    // 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));
964
    expect(tester.getTopLeft(find.byKey(heroABKey)).dy, 100.0);
965 966 967

    // One Opacity widget per Hero, only one now has opacity 0.0
    final Iterable<RenderOpacity> renderers = tester.renderObjectList(find.byType(Opacity));
968
    final Iterable<double> opacities = renderers.map<double>((RenderOpacity r) => r.opacity);
969 970 971 972
    expect(opacities.singleWhere((double opacity) => opacity == 0.0), 0.0);

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

976
  testWidgets('Stateful hero child state survives flight', (WidgetTester tester) async {
977
    final MaterialPageRoute<void> route = MaterialPageRoute<void>(
978
      builder: (BuildContext context) {
979 980
        return Material(
          child: ListView(
981
            children: <Widget>[
982
              const Card(
983
                child: Hero(
984
                  tag: 'H',
985
                  child: SizedBox(
986
                    height: 200.0,
987
                    child: MyStatefulWidget(value: '456'),
988 989 990
                  ),
                ),
              ),
991
              FlatButton(
992
                child: const Text('POP'),
993 994 995 996 997 998 999 1000 1001
                onPressed: () { Navigator.pop(context); }
              ),
            ],
          )
        );
      },
    );

    await tester.pumpWidget(
1002 1003 1004
      MaterialApp(
        home: Scaffold(
          body: Builder(
1005
            builder: (BuildContext context) { // Navigator.push() needs context
1006
              return ListView(
1007
                children: <Widget> [
1008
                  const Card(
1009
                    child: Hero(
1010
                      tag: 'H',
1011
                      child: SizedBox(
1012
                        height: 100.0,
1013
                        child: MyStatefulWidget(value: '456'),
1014 1015 1016
                      ),
                    ),
                  ),
1017
                  FlatButton(
1018
                    child: const Text('PUSH'),
1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057
                    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));
    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);

  });
1058 1059 1060

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

    final Map<String, WidgetBuilder> createRectTweenHeroRoutes = <String, WidgetBuilder>{
1065 1066
      '/': (BuildContext context) => Material(
        child: Column(
1067 1068
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
1069
            Hero(
1070 1071
              tag: 'a',
              createRectTween: createRectTween,
1072
              child: Container(height: 100.0, width: 100.0, key: firstKey),
1073
            ),
1074
            FlatButton(
1075 1076 1077 1078 1079 1080
              child: const Text('two'),
              onPressed: () { Navigator.pushNamed(context, '/two'); }
            ),
          ]
        )
      ),
1081 1082
      '/two': (BuildContext context) => Material(
        child: Column(
1083 1084
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
1085
            SizedBox(
1086
              height: 200.0,
1087
              child: FlatButton(
1088 1089 1090 1091
                child: const Text('pop'),
                onPressed: () { Navigator.pop(context); }
              ),
            ),
1092
            Hero(
1093 1094
              tag: 'a',
              createRectTween: createRectTween,
1095
              child: Container(height: 200.0, width: 100.0, key: secondKey),
1096 1097 1098 1099 1100 1101
            ),
          ],
        ),
      ),
    };

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

1105
    const double epsilon = 0.001;
1106
    const Duration duration = Duration(milliseconds: 300);
1107
    const Curve curve = Curves.fastOutSlowIn;
1108
    final MaterialPointArcTween pushCenterTween = MaterialPointArcTween(
1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124
      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));
1125
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
1126 1127 1128 1129

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

    await tester.pump(duration * 0.25);
    actualHeroCenter = tester.getCenter(find.byKey(secondKey));
    predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.75));
1135
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145

    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

1146
    final MaterialPointArcTween popCenterTween = MaterialPointArcTween(
1147 1148 1149 1150 1151 1152 1153 1154 1155
      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.flipped.transform(0.25));
1156
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
1157 1158 1159 1160

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

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

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

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

1177
    const double epsilon = 0.001;
1178
    const Duration duration = Duration(milliseconds: 300);
1179 1180 1181 1182 1183 1184 1185 1186 1187

    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;
1188

1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263
    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);
  });
1264 1265

  testWidgets('Can override flight shuttle', (WidgetTester tester) async {
1266 1267 1268
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: ListView(
1269 1270
          children: <Widget>[
            const Hero(tag: 'a', child: Text('foo')),
1271 1272
            Builder(builder: (BuildContext context) {
              return FlatButton(
1273
                child: const Text('two'),
1274
                onPressed: () => Navigator.push<void>(context, MaterialPageRoute<void>(
1275
                  builder: (BuildContext context) {
1276 1277
                    return Material(
                      child: Hero(
1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309
                        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 {
1310 1311 1312
    await tester.pumpWidget(MaterialApp(
      home: Material(
        child: ListView(
1313
          children: <Widget>[
1314
            Hero(
1315 1316 1317 1318 1319 1320
              tag: 'a',
              child: const Text('Batman'),
              placeholderBuilder: (BuildContext context, Widget child) {
                return const Text('Venom');
              },
            ),
1321 1322
            Builder(builder: (BuildContext context) {
              return FlatButton(
1323
                child: const Text('two'),
1324
                onPressed: () => Navigator.push<void>(context, MaterialPageRoute<void>(
1325
                  builder: (BuildContext context) {
1326 1327
                    return Material(
                      child: Hero(
1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354
                        tag: 'a',
                        child: const Text('Wolverine'),
                        placeholderBuilder: (BuildContext context, 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);
  });
xster's avatar
xster committed
1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375

  testWidgets('Heroes do not transition on back gestures by default', (WidgetTester tester) async {
    await tester.pumpWidget(MaterialApp(
      theme: ThemeData(
        platform: TargetPlatform.iOS,
      ),
      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);

1376 1377 1378 1379 1380
    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();
xster's avatar
xster committed
1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442

    await tester.pump();

    // Both Heros exist and 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);
  });

  testWidgets('Heroes can transition on gesture in one frame', (WidgetTester tester) async {
    transitionFromUserGestures = true;
    await tester.pumpWidget(MaterialApp(
      theme: ThemeData(
        platform: TargetPlatform.iOS,
      ),
      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);
  });
1443 1444 1445 1446 1447 1448 1449 1450

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