From 2d62d1b88d211a76ee8bef5161668fa298525720 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Tue, 14 Feb 2017 16:03:32 -0800 Subject: [PATCH] Test hero midflight reversals (#8161) --- .../flutter/test/widgets/heroes_test.dart | 155 +++++++++++++++++- packages/flutter_test/lib/src/finders.dart | 10 +- 2 files changed, 158 insertions(+), 7 deletions(-) diff --git a/packages/flutter/test/widgets/heroes_test.dart b/packages/flutter/test/widgets/heroes_test.dart index 1ddc1f9ee97..1b80c78ea09 100644 --- a/packages/flutter/test/widgets/heroes_test.dart +++ b/packages/flutter/test/widgets/heroes_test.dart @@ -9,24 +9,40 @@ Key firstKey = const Key('first'); Key secondKey = const Key('second'); Key thirdKey = const Key('third'); +Key homeRouteKey = const Key('homeRoute'); +Key routeTwoKey = const Key('routeTwo'); +Key routeThreeKey = const Key('routeThree'); + final Map routes = { '/': (BuildContext context) => new Material( child: new ListView( + key: homeRouteKey, children: [ 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), - new FlatButton(child: new Text('two'), onPressed: () => Navigator.pushNamed(context, '/two')), + new FlatButton( + child: new Text('two'), + onPressed: () { Navigator.pushNamed(context, '/two'); } + ), ] ) ), '/two': (BuildContext context) => new Material( child: new ListView( + key: routeTwoKey, children: [ + new FlatButton( + child: new Text('pop'), + onPressed: () { Navigator.pop(context); } + ), 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), - new FlatButton(child: new Text('three'), onPressed: () => Navigator.push(context, new ThreeRoute())), + new FlatButton( + child: new Text('three'), + onPressed: () { Navigator.push(context, new ThreeRoute()); }, + ), ] ) ), @@ -35,6 +51,7 @@ final Map routes = { class ThreeRoute extends MaterialPageRoute { ThreeRoute() : super(builder: (BuildContext context) { return new Material( + key: routeThreeKey, child: new ListView( children: [ new Container(height: 200.0, width: 200.0), @@ -389,4 +406,138 @@ void main() { expect(tester.takeException(), isFlutterError); }); + 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); + }); } diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart index 6e9a819e30b..55b898e7983 100644 --- a/packages/flutter_test/lib/src/finders.dart +++ b/packages/flutter_test/lib/src/finders.dart @@ -181,17 +181,17 @@ class CommonFinders { return new _ElementPredicateFinder(predicate, description: description, skipOffstage: skipOffstage); } - /// Looks for widgets that match the pattern of descendant finder under the - /// widget tree with ancestor as the root. + /// Finds widgets that are descendants of the [of] parameter and that match + /// the [matching] parameter. /// /// Example: /// /// expect(find.descendant( /// of: find.widgetWithText(Row, 'label_1'), matching: find.text('value_1') -/// ), findsOneWidget); + /// ), findsOneWidget); /// - /// If the `skipOffstage` argument is true (the default), then this skips - /// nodes that are [Offstage] or that are from inactive [Route]s. + /// If the [skipOffstage] argument is true (the default), then nodes that are + /// [Offstage] or that are from inactive [Route]s are skipped. Finder descendant({ Finder of, Finder matching, bool skipOffstage: true }) { return new _DescendantFinder(of, matching, skipOffstage: skipOffstage); }