heroes_test.dart 43.9 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');

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

82
};
83

84
class ThreeRoute extends MaterialPageRoute<Null> {
85 86
  ThreeRoute() : super(builder: (BuildContext context) {
    return new Material(
87
      key: routeThreeKey,
88
      child: new ListView(
89 90 91 92 93 94
        children: <Widget>[
          new Container(height: 200.0, width: 200.0),
          new Card(child: new Hero(tag: 'a', child: new Container(height: 200.0, width: 200.0, key: thirdKey))),
          new Container(height: 200.0, width: 200.0),
        ]
      )
95 96 97 98
    );
  });
}

99 100
class MutatingRoute extends MaterialPageRoute<Null> {
  MutatingRoute() : super(builder: (BuildContext context) {
101
    return new Hero(tag: 'a', child: const Text('MutatingRoute'), key: new UniqueKey());
102 103 104 105 106 107 108 109 110
  });

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

111
class MyStatefulWidget extends StatefulWidget {
112
  const MyStatefulWidget({ Key key, this.value: '123' }) : super(key: key);
113 114 115 116 117 118 119
  final String value;
  @override
  MyStatefulWidgetState createState() => new MyStatefulWidgetState();
}

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

123
void main() {
124
  testWidgets('Heroes animate', (WidgetTester tester) async {
125

126
    await tester.pumpWidget(new MaterialApp(routes: routes));
127

128
    // the initial setup.
129

130
    expect(find.byKey(firstKey), isOnstage);
131 132
    expect(find.byKey(firstKey), isInCard);
    expect(find.byKey(secondKey), findsNothing);
133

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

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

140
    expect(find.byKey(firstKey), isOnstage);
141
    expect(find.byKey(firstKey), isInCard);
142 143
    expect(find.byKey(secondKey, skipOffstage: false), isOffstage);
    expect(find.byKey(secondKey, skipOffstage: false), isInCard);
144

145
    await tester.pump();
146

147 148
    // 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.
149

150
    expect(find.byKey(firstKey), findsNothing);
151
    expect(find.byKey(secondKey), isOnstage);
152
    expect(find.byKey(secondKey), isNotInCard);
153

154
    await tester.pump();
155

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

158
    expect(find.byKey(firstKey), findsNothing);
159
    expect(find.byKey(secondKey), isOnstage);
160
    expect(find.byKey(secondKey), isNotInCard);
161

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

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

168
    expect(find.byKey(firstKey), findsNothing);
169
    expect(find.byKey(secondKey), isOnstage);
170
    expect(find.byKey(secondKey), isInCard);
171

172
    await tester.pump();
173

174
    // Should not change anything.
175

176
    expect(find.byKey(firstKey), findsNothing);
177
    expect(find.byKey(secondKey), isOnstage);
178
    expect(find.byKey(secondKey), isInCard);
179

180
    // Now move on to view 3
181

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

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

188
    expect(find.byKey(secondKey), isOnstage);
189
    expect(find.byKey(secondKey), isInCard);
190 191
    expect(find.byKey(thirdKey, skipOffstage: false), isOffstage);
    expect(find.byKey(thirdKey, skipOffstage: false), isInCard);
192

193
    await tester.pump();
194

195 196
    // 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.
197

198
    expect(find.byKey(secondKey), findsNothing);
199
    expect(find.byKey(thirdKey), isOnstage);
200
    expect(find.byKey(thirdKey), isNotInCard);
201

202
    await tester.pump();
203

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

206
    expect(find.byKey(secondKey), findsNothing);
207
    expect(find.byKey(thirdKey), isOnstage);
208
    expect(find.byKey(thirdKey), isNotInCard);
209

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

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

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

219
    await tester.pump();
220

221
    // Should not change anything.
222

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

228
  testWidgets('Destination hero is rebuilt midflight', (WidgetTester tester) async {
229
    final MutatingRoute route = new MutatingRoute();
230 231

    await tester.pumpWidget(new MaterialApp(
232
      home: new Material(
233
        child: new ListView(
234
          children: <Widget>[
235
            const Hero(tag: 'a', child: const Text('foo')),
236
            new Builder(builder: (BuildContext context) {
237
              return new FlatButton(child: const Text('two'), onPressed: () => Navigator.push(context, route));
238 239 240 241
            })
          ]
        )
      )
242 243 244
    ));

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

    route.markNeedsBuild();

249 250
    await tester.pump(const Duration(milliseconds: 10));
    await tester.pump(const Duration(seconds: 1));
251
  });
252

253 254 255 256 257 258 259 260 261 262
  testWidgets('Heroes animation is fastOutSlowIn', (WidgetTester tester) async {
    await tester.pumpWidget(new 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.

    final Duration duration = const Duration(milliseconds: 300);
    final Curve curve = Curves.fastOutSlowIn;
263 264
    final double initialHeight = tester.getSize(find.byKey(firstKey, skipOffstage: false)).height;
    final double finalHeight = tester.getSize(find.byKey(secondKey, skipOffstage: false)).height;
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
    final double deltaHeight = finalHeight - initialHeight;
    final 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)
    );
  });

293
  testWidgets('Heroes are not interactive', (WidgetTester tester) async {
294
    final List<String> log = <String>[];
295 296 297 298 299 300 301 302 303 304 305 306

    await tester.pumpWidget(new MaterialApp(
      home: new Center(
        child: new Hero(
          tag: 'foo',
          child: new GestureDetector(
            onTap: () {
              log.add('foo');
            },
            child: new Container(
              width: 100.0,
              height: 100.0,
307
              child: const Text('foo')
308 309 310 311 312 313 314
            )
          )
        )
      ),
      routes: <String, WidgetBuilder>{
        '/next': (BuildContext context) {
          return new Align(
315
            alignment: Alignment.topLeft,
316 317 318 319 320 321 322 323 324
            child: new Hero(
              tag: 'foo',
              child: new GestureDetector(
                onTap: () {
                  log.add('bar');
                },
                child: new Container(
                  width: 100.0,
                  height: 150.0,
325
                  child: const Text('bar')
326 327 328 329 330 331 332 333 334 335 336 337 338
                )
              )
            )
          );
        }
      }
    ));

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

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

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

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

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

357
    await tester.pump(const Duration(seconds: 1));
358 359 360 361
    expect(find.text('foo'), findsNothing);
    await tester.tap(find.text('bar'));
    expect(log, equals(<String>['bar']));
  });
362 363 364 365 366 367 368 369 370 371 372 373

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

374
    final Finder heroes = find.byType(Hero);
375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
    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 {
    await tester.pumpWidget(new MaterialApp(
      onGenerateRoute: (RouteSettings settings) {
        return new MaterialPageRoute<Null>(
          settings: settings,
          builder: (BuildContext context) => new Hero(tag: 'test', child: new Container()),
        );
      },
    ));
    await tester.pump();

400
    final Finder heroes = find.byType(Hero);
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
    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
422 423 424 425 426 427

  testWidgets('One route, two heroes, same tag, throws', (WidgetTester tester) async {
    await tester.pumpWidget(new MaterialApp(
      home: new Material(
        child: new ListView(
          children: <Widget>[
428 429
            const Hero(tag: 'a', child: const Text('a')),
            const Hero(tag: 'a', child: const Text('a too')),
Hans Muller's avatar
Hans Muller committed
430 431 432
            new Builder(
              builder: (BuildContext context) {
                return new FlatButton(
433
                  child: const Text('push'),
Hans Muller's avatar
Hans Muller committed
434 435 436
                  onPressed: () {
                    Navigator.push(context, new PageRouteBuilder<Null>(
                      pageBuilder: (BuildContext context, Animation<double> _, Animation<double> __) {
437
                        return const Text('fail');
Hans Muller's avatar
Hans Muller committed
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
                      },
                    ));
                  },
                );
              },
            ),
          ],
        ),
      ),
    ));

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

454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 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 525 526 527 528 529 530 531 532 533 534 535 536 537 538 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
  testWidgets('Hero push transition interrupted by a pop', (WidgetTester tester) async {
    await tester.pumpWidget(new 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.
    final 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(
      new 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 height100ms = tester.getSize(find.byKey(firstKey)).height;
    expect(height100ms, greaterThan(height150ms));
    expect(finalHeight, lessThan(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.
    final 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);
  });
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611

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

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

    // Show a 100x100 Hero tagged 'H' with key homeHeroKey
    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          body: new Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return new ListView(
                children: <Widget> [
                  new Card(
                    child: new Hero(tag: 'H', child: new Container(key: homeHeroKey, height: 100.0, width: 100.0)),
                  ),
                  new FlatButton(
633
                    child: const Text('PUSH'),
634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 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
                    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 midlfight
    heroCardSetState(() {
      routeIncludesHero = false;
    });
    await tester.pump();

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

  });
691 692 693 694 695 696 697 698 699 700 701 702

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

    // Show a 200x200 Hero tagged 'H', with key routeHeroKey
    final MaterialPageRoute<Null> route = new MaterialPageRoute<Null>(
      builder: (BuildContext context) {
        return new Material(
          child: new ListView(
            children: <Widget>[
703
              const SizedBox(height: 100.0),
704 705 706 707 708 709
              // This container will appear at Y=100
              new Container(
                key: routeContainerKey,
                child: new Hero(tag: 'H', child: new Container(key: routeHeroKey, height: 200.0, width: 200.0))
              ),
              new FlatButton(
710
                child: const Text('POP'),
711 712
                onPressed: () { Navigator.pop(context); }
              ),
713
              const SizedBox(height: 600.0),
714 715 716 717 718 719 720 721 722 723 724 725 726 727
            ],
          )
        );
      },
    );

    // Show a 100x100 Hero tagged 'H' with key homeHeroKey
    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          body: new Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return new ListView(
                children: <Widget> [
728
                  const SizedBox(height: 200.0),
729 730 731 732 733
                  // This container will appear at Y=200
                  new Container(
                    child: new Hero(tag: 'H', child: new Container(key: homeHeroKey, height: 100.0, width: 100.0)),
                  ),
                  new FlatButton(
734
                    child: const Text('PUSH'),
735 736
                    onPressed: () { Navigator.push(context, route); }
                  ),
737
                  const SizedBox(height: 600.0),
738 739 740 741 742 743 744 745 746 747 748 749 750
                ],
              );
            },
          ),
        ),
      )
    );

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

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

    await tester.pump(const Duration(milliseconds: 100));
755
    final double yAt100ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
756 757 758 759 760
    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.
761
    await(tester.drag(find.byKey(routeContainerKey), const Offset(0.0, -25.0)));
762 763
    await tester.pump();
    await tester.pump(const Duration(milliseconds: 10));
764
    final double yAt110ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
765 766 767 768 769
    expect(yAt110ms, lessThan(yAt100ms));
    expect(yAt110ms, greaterThan(75.0));

    await tester.pump(const Duration(milliseconds: 300));
    await tester.pump();
770
    final double finalHeroY = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
771 772 773 774 775 776 777 778 779 780 781 782 783 784
    expect(finalHeroY, 75.0); // 100 less 25 for the scroll
  });

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

    // Show a 200x200 Hero tagged 'H', with key routeHeroKey
    final MaterialPageRoute<Null> route = new MaterialPageRoute<Null>(
      builder: (BuildContext context) {
        return new Material(
          child: new ListView(
            children: <Widget>[
785
              const SizedBox(height: 100.0),
786 787 788 789 790
              // This container will appear at Y=100
              new Container(
                key: routeContainerKey,
                child: new Hero(tag: 'H', child: new Container(key: routeHeroKey, height: 200.0, width: 200.0))
              ),
791
              const SizedBox(height: 800.0),
792 793 794 795 796 797 798 799 800 801 802 803 804 805
            ],
          )
        );
      },
    );

    // Show a 100x100 Hero tagged 'H' with key homeHeroKey
    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          body: new Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return new ListView(
                children: <Widget> [
806
                  const SizedBox(height: 200.0),
807 808 809 810 811
                  // This container will appear at Y=200
                  new Container(
                    child: new Hero(tag: 'H', child: new Container(key: homeHeroKey, height: 100.0, width: 100.0)),
                  ),
                  new FlatButton(
812
                    child: const Text('PUSH'),
813 814 815 816 817 818 819 820 821 822 823 824 825 826 827
                    onPressed: () { Navigator.push(context, route); }
                  ),
                ],
              );
            },
          ),
        ),
      )
    );

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

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

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

836
    await(tester.drag(find.byKey(routeContainerKey), const Offset(0.0, -400.0)));
837 838 839 840 841 842
    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.
843
    final double yAt110ms = tester.getTopLeft(find.byKey(routeHeroKey)).dy;
844 845 846 847 848 849 850 851
    expect(yAt110ms, lessThan(yAt100ms));
    expect(yAt110ms, greaterThan(100.0));

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

852 853 854 855 856 857 858 859 860 861 862 863 864 865 866
  testWidgets('Aborted flight', (WidgetTester tester) async {
    // See https://github.com/flutter/flutter/issues/5798
    final Key heroABKey = const Key('AB hero');
    final Key heroBCKey = const Key('BC hero');

    // Show a 150x150 Hero tagged 'BC'
    final MaterialPageRoute<Null> routeC = new MaterialPageRoute<Null>(
      builder: (BuildContext context) {
        return new Material(
          child: new ListView(
            children: <Widget>[
              // This container will appear at Y=0
              new Container(
                child: new Hero(tag: 'BC', child: new Container(key: heroBCKey, height: 150.0))
              ),
867
              const SizedBox(height: 800.0),
868 869 870 871 872 873 874 875 876 877 878 879
            ],
          )
        );
      },
    );

    // Show a height=200 Hero tagged 'AB' and a height=50 Hero tagged 'BC'
    final MaterialPageRoute<Null> routeB = new MaterialPageRoute<Null>(
      builder: (BuildContext context) {
        return new Material(
          child: new ListView(
            children: <Widget>[
880
              const SizedBox(height: 100.0),
881 882 883 884 885
              // This container will appear at Y=100
              new Container(
                child: new Hero(tag: 'AB', child: new Container(key: heroABKey, height: 200.0))
              ),
              new FlatButton(
886
                child: const Text('PUSH C'),
887 888 889 890 891
                onPressed: () { Navigator.push(context, routeC); }
              ),
              new Container(
                child: new Hero(tag: 'BC', child: new Container(height: 150.0))
              ),
892
              const SizedBox(height: 800.0),
893 894 895 896 897 898 899 900 901 902 903 904 905 906
            ],
          )
        );
      },
    );

    // Show a 100x100 Hero tagged 'AB' with key heroABKey
    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          body: new Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return new ListView(
                children: <Widget> [
907
                  const SizedBox(height: 200.0),
908 909 910 911 912
                  // This container will appear at Y=200
                  new Container(
                    child: new Hero(tag: 'AB', child: new Container(height: 100.0, width: 100.0)),
                  ),
                  new FlatButton(
913
                    child: const Text('PUSH B'),
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928
                    onPressed: () { Navigator.push(context, routeB); }
                  ),
                ],
              );
            },
          ),
        ),
      )
    );

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

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

    await tester.pump(const Duration(milliseconds: 200));
933
    final double yAt200ms = tester.getTopLeft(find.byKey(heroABKey)).dy;
934 935 936 937 938 939 940 941 942 943 944 945
    // 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));
946
    expect(tester.getTopLeft(find.byKey(heroABKey)).dy, 100.0);
947 948 949 950 951 952 953 954

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

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

958 959 960 961 962 963
  testWidgets('Stateful hero child state survives flight', (WidgetTester tester) async {
    final MaterialPageRoute<Null> route = new MaterialPageRoute<Null>(
      builder: (BuildContext context) {
        return new Material(
          child: new ListView(
            children: <Widget>[
964 965
              const Card(
                child: const Hero(
966
                  tag: 'H',
967
                  child: const SizedBox(
968
                    height: 200.0,
969
                    child: const MyStatefulWidget(value: '456'),
970 971 972 973
                  ),
                ),
              ),
              new FlatButton(
974
                child: const Text('POP'),
975 976 977 978 979 980 981 982 983 984 985 986 987 988 989
                onPressed: () { Navigator.pop(context); }
              ),
            ],
          )
        );
      },
    );

    await tester.pumpWidget(
      new MaterialApp(
        home: new Scaffold(
          body: new Builder(
            builder: (BuildContext context) { // Navigator.push() needs context
              return new ListView(
                children: <Widget> [
990 991
                  const Card(
                    child: const Hero(
992
                      tag: 'H',
993
                      child: const SizedBox(
994
                        height: 100.0,
995
                        child: const MyStatefulWidget(value: '456'),
996 997 998 999
                      ),
                    ),
                  ),
                  new FlatButton(
1000
                    child: const Text('PUSH'),
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
                    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);

  });
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106

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

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

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

    final double epsilon = 0.001;
    final Duration duration = const Duration(milliseconds: 300);
    final Curve curve = Curves.fastOutSlowIn;
    final MaterialPointArcTween pushCenterTween = new 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));
1107
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
1108 1109 1110 1111

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

    await tester.pump(duration * 0.25);
    actualHeroCenter = tester.getCenter(find.byKey(secondKey));
    predictedHeroCenter = pushCenterTween.lerp(curve.transform(0.75));
1117
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137

    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 = new 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.flipped.transform(0.25));
1138
    expect(actualHeroCenter, within<Offset>(distance: epsilon, from: predictedHeroCenter));
1139 1140 1141 1142

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

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

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

1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169
  testWidgets('Pop interrupts push, reverses flight', (WidgetTester tester) async {
    await tester.pumpWidget(new MaterialApp(routes: routes));
    await tester.tap(find.text('twoInset'));
    await tester.pump(); // begin navigation from / to /twoInset.

    final double epsilon = 0.001;
    final Duration duration = const 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;
1170

1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 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
    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);
  });
1246
}