From 56a2d2262cbda7b3df8f70dcd20ecb743e285a29 Mon Sep 17 00:00:00 2001 From: Hans Muller Date: Mon, 1 Aug 2016 15:09:50 -0700 Subject: [PATCH] Pesto home stack (#5168) --- .../flutter_gallery/lib/demo/pesto_demo.dart | 17 ++++-- examples/flutter_gallery/test/pesto_test.dart | 55 +++++++++++++++++++ .../flutter/lib/src/widgets/navigator.dart | 17 +++--- packages/flutter/lib/src/widgets/routes.dart | 11 ++++ packages/flutter/test/widget/routes_test.dart | 42 +++++++++++++- 5 files changed, 128 insertions(+), 14 deletions(-) create mode 100644 examples/flutter_gallery/test/pesto_test.dart diff --git a/examples/flutter_gallery/lib/demo/pesto_demo.dart b/examples/flutter_gallery/lib/demo/pesto_demo.dart index 19a581ec29..2bbdc3d77f 100644 --- a/examples/flutter_gallery/lib/demo/pesto_demo.dart +++ b/examples/flutter_gallery/lib/demo/pesto_demo.dart @@ -142,20 +142,25 @@ class _PestoDemoState extends State { new DrawerItem( child: new Text('Home'), selected: !config.showFavorites, - onPressed: () => Navigator.pushNamed(context, PestoDemo.routeName) + onPressed: () { + Navigator.popUntil(context, ModalRoute.withName('/pesto')); + } ), new DrawerItem( child: new Text('Favorites'), selected: config.showFavorites, - onPressed: () { _showFavorites(context); } + onPressed: () { + if (config.showFavorites) + Navigator.pop(context); + else + _showFavorites(context); + } ), new Divider(), new DrawerItem( child: new Text('Return to Gallery'), onPressed: () { - Navigator.of(context) - ..pop() // Close the drawer. - ..pop(); // Go back to the gallery. + Navigator.popUntil(context, ModalRoute.withName('/')); } ), ] @@ -193,6 +198,7 @@ class _PestoDemoState extends State { void _showFavorites(BuildContext context) { Navigator.push(context, new MaterialPageRoute( + settings: const RouteSettings(name: "/pesto/favorites"), builder: (BuildContext context) { return new PestoDemo(showFavorites: true); } @@ -201,6 +207,7 @@ class _PestoDemoState extends State { void _showRecipe(BuildContext context, Recipe recipe) { Navigator.push(context, new MaterialPageRoute( + settings: const RouteSettings(name: "/pesto/recipe"), builder: (BuildContext context) { return new Theme( data: _kTheme, diff --git a/examples/flutter_gallery/test/pesto_test.dart b/examples/flutter_gallery/test/pesto_test.dart new file mode 100644 index 0000000000..ab0d106b1b --- /dev/null +++ b/examples/flutter_gallery/test/pesto_test.dart @@ -0,0 +1,55 @@ +// Copyright 2016 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. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_gallery/main.dart' as flutter_gallery_main; + +Finder byTooltip(WidgetTester tester, String message) { + return find.byWidgetPredicate((Widget widget) { + return widget is Tooltip && widget.message == message; + }); +} + +Finder findNavigationMenuButton(WidgetTester tester) { + return byTooltip(tester, 'Open navigation menu'); +} + +void main() { + TestWidgetsFlutterBinding binding = + TestWidgetsFlutterBinding.ensureInitialized(); + if (binding is LiveTestWidgetsFlutterBinding) binding.allowAllFrames = true; + + // Regression test for https://github.com/flutter/flutter/pull/5168 + testWidgets('Pesto route management', (WidgetTester tester) async { + flutter_gallery_main + .main(); // builds the app and schedules a frame but doesn't trigger one + await tester.pump(); // see https://github.com/flutter/flutter/issues/1865 + await tester.pump(); // triggers a frame + + expect(find.text('Pesto'), findsOneWidget); + await tester.tap(find.text('Pesto')); + await tester.pump(); // Launch pesto + await tester.pump(const Duration(seconds: 1)); // transition is complete + + Future tapDrawerItem(String title) async { + await tester.tap(findNavigationMenuButton(tester)); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // drawer opening animation + await tester.tap(find.text(title)); + await tester.pump(); + await tester.pump(const Duration(seconds: 1)); // drawer closing animation + await tester.pump(); // maybe open a new page + return tester.pump(const Duration(seconds: 1)); // new page transition + } + await tapDrawerItem('Home'); + await tapDrawerItem('Favorites'); + await tapDrawerItem('Home'); + await tapDrawerItem('Favorites'); + await tapDrawerItem('Home'); + await tapDrawerItem('Return to Gallery'); + + expect(find.text('Flutter gallery'), findsOneWidget); + }); +} diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 76fe999829..273f14647a 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -161,6 +161,9 @@ class NavigatorObserver { void didPop(Route route, Route previousRoute) { } } +/// Signature for the [Navigator.popUntil] predicate argument. +typedef bool RoutePredicate(Route route); + /// A widget that manages a set of child widgets with a stack discipline. /// /// Many apps have a navigator near the top of their widget hierarchy in order @@ -243,10 +246,11 @@ class Navigator extends StatefulWidget { return Navigator.of(context).pop(result); } - /// Calls pop() repeatedly until the given route is the current route. - /// If it is already the current route, nothing happens. - static void popUntil(BuildContext context, Route targetRoute) { - Navigator.of(context).popUntil(targetRoute); + /// Calls [pop()] repeatedly until the predicate returns false. + /// The predicate may be applied to the same route more than once if + /// [Route.willHandlePopInternally] is true. + static void popUntil(BuildContext context, RoutePredicate predicate) { + Navigator.of(context).popUntil(predicate); } /// Whether the navigator that most tightly encloses the given context can be popped. @@ -463,9 +467,8 @@ class NavigatorState extends State { return true; } - void popUntil(Route targetRoute) { - assert(_history.contains(targetRoute)); - while (!targetRoute.isCurrent) + void popUntil(RoutePredicate predicate) { + while (!predicate(_history.last)) pop(); } diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 3be41cf2ae..bdb12abcb5 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -487,6 +487,17 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute route) { + return !route.willHandlePopInternally + && route is ModalRoute && route.settings.name == name; + }; + } // The API for subclasses to override - used by _ModalScope diff --git a/packages/flutter/test/widget/routes_test.dart b/packages/flutter/test/widget/routes_test.dart index 3e2eca1c17..cb2573a721 100644 --- a/packages/flutter/test/widget/routes_test.dart +++ b/packages/flutter/test/widget/routes_test.dart @@ -11,7 +11,7 @@ final List results = []; Set routes = new HashSet(); -class TestRoute extends Route { +class TestRoute extends LocalHistoryRoute { TestRoute(this.name); final String name; @@ -329,7 +329,7 @@ void main() { await runNavigatorTest( tester, host, - () { host.popUntil(routeB); }, + () { host.popUntil((Route route) => route == routeB); }, [ 'C: didPop null', 'C: dispose', @@ -341,4 +341,42 @@ void main() { expect(routes.isEmpty, isTrue); results.clear(); }); + + testWidgets('Route localHistory - popUntil', (WidgetTester tester) async { + TestRoute routeA = new TestRoute('A'); + routeA.addLocalHistoryEntry(new LocalHistoryEntry( + onRemove: () { routeA.log('onRemove 0'); } + )); + routeA.addLocalHistoryEntry(new LocalHistoryEntry( + onRemove: () { routeA.log('onRemove 1'); } + )); + GlobalKey navigatorKey = new GlobalKey(); + await tester.pumpWidget(new Navigator( + key: navigatorKey, + onGenerateRoute: (_) => routeA + )); + NavigatorState host = navigatorKey.currentState; + await runNavigatorTest( + tester, + host, + () { host.popUntil((Route route) => !route.willHandlePopInternally); }, + [ + 'A: install', + 'A: didPush', + 'A: didChangeNext null', + 'A: didPop null', + 'A: onRemove 1', + 'A: didPop null', + 'A: onRemove 0', + ] + ); + + await runNavigatorTest( + tester, + host, + () { host.popUntil((Route route) => !route.willHandlePopInternally); }, + [ + ] + ); + }); }