Commit ea6bf470 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

Fix the losing of state when pushing opaque routes (#5624)

Fixes https://github.com/flutter/flutter/issues/5283

Other changes in this patch:

Rename OffStage to Offstage.
Fixes https://github.com/flutter/flutter/issues/5378

Add a lot of docs.

Some minor punctuation and whitespace fixes.
parent 43d0eeb8
...@@ -148,12 +148,17 @@ class _CupertinoBackGestureController extends NavigationGestureController { ...@@ -148,12 +148,17 @@ class _CupertinoBackGestureController extends NavigationGestureController {
/// ///
/// [MaterialApp] creates material page routes for entries in the /// [MaterialApp] creates material page routes for entries in the
/// [MaterialApp.routes] map. /// [MaterialApp.routes] map.
///
/// By default, when a modal route is replaced by another, the previous route
/// remains in memory. To free all the resources when this is not necessary, set
/// [maintainState] to false.
class MaterialPageRoute<T> extends PageRoute<T> { class MaterialPageRoute<T> extends PageRoute<T> {
/// Creates a page route for use in a material design app. /// Creates a page route for use in a material design app.
MaterialPageRoute({ MaterialPageRoute({
this.builder, this.builder,
Completer<T> completer, Completer<T> completer,
RouteSettings settings: const RouteSettings() RouteSettings settings: const RouteSettings(),
this.maintainState: true,
}) : super(completer: completer, settings: settings) { }) : super(completer: completer, settings: settings) {
assert(builder != null); assert(builder != null);
assert(opaque); assert(opaque);
...@@ -162,6 +167,9 @@ class MaterialPageRoute<T> extends PageRoute<T> { ...@@ -162,6 +167,9 @@ class MaterialPageRoute<T> extends PageRoute<T> {
/// Builds the primary contents of the route. /// Builds the primary contents of the route.
final WidgetBuilder builder; final WidgetBuilder builder;
@override
final bool maintainState;
@override @override
Duration get transitionDuration => const Duration(milliseconds: 300); Duration get transitionDuration => const Duration(milliseconds: 300);
......
...@@ -148,7 +148,53 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, ...@@ -148,7 +148,53 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
/// Pump the rendering pipeline to generate a frame. /// Pump the rendering pipeline to generate a frame.
/// ///
/// Called automatically by the engine when it is time to lay out and paint a frame. /// This method is called by [handleBeginFrame], which itself is called
/// automatically by the engine when when it is time to lay out and paint a
/// frame.
///
/// Each frame consists of the following phases:
///
/// 1. The animation phase: The [handleBeginFrame] method, which is registered
/// with [ui.window.onBeginFrame], invokes all the transient frame callbacks
/// registered with [scheduleFrameCallback] and [addFrameCallback], in
/// registration order. This includes all the [Ticker] instances that are
/// driving [AnimationController] objects, which means all of the active
/// [Animation] objects tick at this point.
///
/// [handleBeginFrame] then invokes all the persistent frame callbacks, of which
/// the most notable is this method, [beginFrame], which proceeds as follows:
///
/// 2. The layout phase: All the dirty [RenderObject]s in the system are laid
/// out (see [RenderObject.performLayout]). See [RenderObject.markNeedsLayout]
/// for further details on marking an object dirty for layout.
///
/// 3. The compositing bits phase: The compositing bits on any dirty
/// [RenderObject] objects are updated. See
/// [RenderObject.markNeedsCompositingBitsUpdate].
///
/// 4. The paint phase: All the dirty [RenderObject]s in the system are
/// repainted (see [RenderObject.paint]). This generates the [Layer] tree. See
/// [RenderObject.markNeedsPaint] for further details on marking an object
/// dirty for paint.
///
/// 5. The compositing phase: The layer tree is turned into a [ui.Scene] and
/// sent to the GPU.
///
/// 6. The semantics phase: All the dirty [RenderObject]s in the system have
/// their semantics updated (see [RenderObject.semanticAnnotator]). This
/// generates the [SemanticsNode] tree. See
/// [RenderObject.markNeedsSemanticsUpdate] for further details on marking an
/// object dirty for semantics.
///
/// For more details on steps 2-6, see [PipelineOwner].
///
/// 7. The finalization phase: After [beginFrame] returns, [handleBeginFrame]
/// then invokes post-frame callbacks (registered with [addPostFrameCallback].
///
/// Some bindings (for example, the [WidgetsBinding]) add extra steps to this
/// list.
//
// When editing the above, also update widgets/binding.dart's copy.
void beginFrame() { void beginFrame() {
assert(renderView != null); assert(renderView != null);
pipelineOwner.flushLayout(); pipelineOwner.flushLayout();
......
...@@ -12,7 +12,7 @@ import 'package:mojo_services/mojo/gfx/composition/scene_token.mojom.dart' as mo ...@@ -12,7 +12,7 @@ import 'package:mojo_services/mojo/gfx/composition/scene_token.mojom.dart' as mo
import 'debug.dart'; import 'debug.dart';
/// A composited layer /// A composited layer.
/// ///
/// During painting, the render tree generates a tree of composited layers that /// During painting, the render tree generates a tree of composited layers that
/// are uploaded into the engine and displayed by the compositor. This class is /// are uploaded into the engine and displayed by the compositor. This class is
......
...@@ -1956,26 +1956,24 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { ...@@ -1956,26 +1956,24 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
/// Mark this node as needing an update to its semantics /// Mark this node as needing an update to its semantics
/// description. /// description.
/// ///
/// If the change did not involve a removal or addition of /// If the change did not involve a removal or addition of semantics, only the
/// semantics, only the change of semantics (e.g. isChecked changing /// change of semantics (e.g. isChecked changing from true to false, as
/// from true to false, as opposed to isChecked changing from being /// opposed to isChecked changing from being true to not being changed at
/// true to not being changed at all), then you can pass the /// all), then you can pass the onlyChanges argument with the value true to
/// onlyChanges argument with the value true to reduce the cost. If /// reduce the cost. If semantics are being added or removed, more work needs
/// semantics are being added or removed, more work needs to be done /// to be done to update the semantics tree. If you pass 'onlyChanges: true'
/// to update the semantics tree. If you pass 'onlyChanges: true' /// but this node, which previously had a SemanticsNode, no longer has one, or
/// but this node, which previously had a SemanticsNode, no longer /// previously did not set any semantics, but now does, or previously had a
/// has one, or previously did not set any semantics, but now does, /// child that returned annotators, but no longer does, or other such
/// or previously had a child that returned annotators, but no /// combinations, then you will either assert during the subsequent call to
/// longer does, or other such combinations, then you will either /// [PipelineOwner.flushSemantics()] or you will have out-of-date information
/// assert during the subsequent call to [flushSemantics()] or you /// in the semantics tree.
/// will have out-of-date information in the semantics tree. ///
/// /// If the geometry might have changed in any way, then again, more work needs
/// If the geometry might have changed in any way, then again, more /// to be done to update the semantics tree (to deal with clips). You can pass
/// work needs to be done to update the semantics tree (to deal with /// the noGeometry argument to avoid this work in the case where only the
/// clips). You can pass the noGeometry argument to avoid this work /// labels or flags changed. If you pass 'noGeometry: true' when the geometry
/// in the case where only the labels or flags changed. If you pass /// did change, the semantic tree will be out of date.
/// 'noGeometry: true' when the geometry did change, the semantic
/// tree will be out of date.
void markNeedsSemanticsUpdate({ bool onlyChanges: false, bool noGeometry: false }) { void markNeedsSemanticsUpdate({ bool onlyChanges: false, bool noGeometry: false }) {
assert(!attached || !owner._debugDoingSemantics); assert(!attached || !owner._debugDoingSemantics);
if ((attached && owner._semanticsOwner == null) || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry))) if ((attached && owner._semanticsOwner == null) || (_needsSemanticsUpdate && onlyChanges && (_needsSemanticsGeometryUpdate || noGeometry)))
...@@ -2089,31 +2087,30 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { ...@@ -2089,31 +2087,30 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
visitChildren(visitor); visitChildren(visitor);
} }
/// Returns functions that will annotate a SemanticsNode with the /// Returns a function that will annotate a [SemanticsNode] with the semantics
/// semantics of this RenderObject. /// of this [RenderObject].
/// ///
/// To annotate a SemanticsNode for this node, return all the /// To annotate a SemanticsNode for this node, return an annotator that
/// annotators provided by the superclass, plus an annotator that /// adds the annotations. When the behavior of the annotator would
/// adds the annotations. When the behavior of the annotators would
/// change (e.g. the box is now checked rather than unchecked), call /// change (e.g. the box is now checked rather than unchecked), call
/// [markNeedsSemanticsUpdate()] to indicate to the rendering system /// [markNeedsSemanticsUpdate] to indicate to the rendering system
/// that the semantics tree needs to be rebuilt. /// that the semantics tree needs to be rebuilt.
/// ///
/// To introduce a new SemanticsNode, set hasSemantics to true for /// To introduce a new SemanticsNode, set hasSemantics to true for
/// this object. The functions returned by this function will be used /// this object. The function returned by this function will be used
/// to annotate the SemanticsNode for this object. /// to annotate the SemanticsNode for this object.
/// ///
/// Semantic annotations are persistent. Values set in one pass will /// Semantic annotations are persistent. Values set in one pass will
/// still be set in the next pass. Therefore it is important to /// still be set in the next pass. Therefore it is important to
/// explicitly set fields to false once they are no longer true -- /// explicitly set fields to false once they are no longer true;
/// setting them to true when they are to be enabled, and not /// setting them to true when they are to be enabled, and not
/// setting them at all when they are not, will mean they remain set /// setting them at all when they are not, will mean they remain set
/// once enabled once and will never get unset. /// once enabled once and will never get unset.
/// ///
/// If the number of annotators you return will change from zero to /// If the value return will change from null to non-null (or vice versa), and
/// non-zero, and hasSemantics isn't true, then the associated call /// [hasSemantics] isn't true, then the associated call to
/// to markNeedsSemanticsUpdate() must not have 'onlyChanges' set, as /// [markNeedsSemanticsUpdate] must not have `onlyChanges` set, as it is
/// it is possible that the node should be entirely removed. /// possible that the node should be entirely removed.
SemanticAnnotator get semanticAnnotator => null; SemanticAnnotator get semanticAnnotator => null;
...@@ -2247,6 +2244,12 @@ abstract class RenderObjectWithChildMixin<ChildType extends RenderObject> implem ...@@ -2247,6 +2244,12 @@ abstract class RenderObjectWithChildMixin<ChildType extends RenderObject> implem
_child.detach(); _child.detach();
} }
@override
void redepthChildren() {
if (_child != null)
redepthChild(_child);
}
@override @override
void visitChildren(RenderObjectVisitor visitor) { void visitChildren(RenderObjectVisitor visitor) {
if (_child != null) if (_child != null)
......
...@@ -32,7 +32,7 @@ export 'package:flutter/gestures.dart' show ...@@ -32,7 +32,7 @@ export 'package:flutter/gestures.dart' show
/// the proxy box with its child. However, RenderProxyBox is a useful base class /// the proxy box with its child. However, RenderProxyBox is a useful base class
/// for render objects that wish to mimic most, but not all, of the properties /// for render objects that wish to mimic most, but not all, of the properties
/// of their child. /// of their child.
class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> { class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox>, RenderProxyBoxMixin {
/// Creates a proxy render box. /// Creates a proxy render box.
/// ///
/// Proxy render boxes are rarely created directly because they simply proxy /// Proxy render boxes are rarely created directly because they simply proxy
...@@ -41,7 +41,15 @@ class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox ...@@ -41,7 +41,15 @@ class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox
RenderProxyBox([RenderBox child = null]) { RenderProxyBox([RenderBox child = null]) {
this.child = child; this.child = child;
} }
}
/// Implementation of [RenderProxyBox].
///
/// This class can be used as a mixin for situations where the proxying behavior
/// of [RenderProxyBox] is desired but inheriting from [RenderProxyBox] is
/// impractical (e.g. because you want to mix in other classes as well).
// TODO(ianh): Remove this class once https://github.com/dart-lang/sdk/issues/15101 is fixed
abstract class RenderProxyBoxMixin implements RenderBox, RenderObjectWithChildMixin<RenderBox> {
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
if (child != null) if (child != null)
...@@ -2016,9 +2024,9 @@ class RenderIgnorePointer extends RenderProxyBox { ...@@ -2016,9 +2024,9 @@ class RenderIgnorePointer extends RenderProxyBox {
/// Lays the child out as if it was in the tree, but without painting anything, /// Lays the child out as if it was in the tree, but without painting anything,
/// without making the child available for hit testing, and without taking any /// without making the child available for hit testing, and without taking any
/// room in the parent. /// room in the parent.
class RenderOffStage extends RenderProxyBox { class RenderOffstage extends RenderProxyBox {
/// Creates an off-stage render object. /// Creates an offstage render object.
RenderOffStage({ RenderOffstage({
bool offstage: true, bool offstage: true,
RenderBox child RenderBox child
}) : _offstage = offstage, super(child) { }) : _offstage = offstage, super(child) {
......
...@@ -422,14 +422,14 @@ class RenderPositionedBox extends RenderAligningShiftedBox { ...@@ -422,14 +422,14 @@ class RenderPositionedBox extends RenderAligningShiftedBox {
/// ignoring the child's dimensions. /// ignoring the child's dimensions.
/// ///
/// For example, if you wanted a box to always render 50 pixels high, regardless /// For example, if you wanted a box to always render 50 pixels high, regardless
/// of where it was rendered, you would wrap it in a RenderOverflow with /// of where it was rendered, you would wrap it in a
/// minHeight and maxHeight set to 50.0. Generally speaking, to avoid confusing /// RenderConstrainedOverflowBox with minHeight and maxHeight set to 50.0.
/// behavior around hit testing, a RenderOverflowBox should usually be wrapped /// Generally speaking, to avoid confusing behavior around hit testing, a
/// in a RenderClipRect. /// RenderConstrainedOverflowBox should usually be wrapped in a RenderClipRect.
/// ///
/// The child is positioned at the top left of the box. To position a smaller /// The child is positioned according to [alignment]. To position a smaller
/// child inside a larger parent, use [RenderPositionedBox] and /// child inside a larger parent, use [RenderPositionedBox] and
/// [RenderConstrainedBox] rather than RenderOverflowBox. /// [RenderConstrainedBox] rather than RenderConstrainedOverflowBox.
class RenderConstrainedOverflowBox extends RenderAligningShiftedBox { class RenderConstrainedOverflowBox extends RenderAligningShiftedBox {
/// Creates a render object that lets its child overflow itself. /// Creates a render object that lets its child overflow itself.
RenderConstrainedOverflowBox({ RenderConstrainedOverflowBox({
......
...@@ -199,13 +199,13 @@ class StackParentData extends ContainerBoxParentDataMixin<RenderBox> { ...@@ -199,13 +199,13 @@ class StackParentData extends ContainerBoxParentDataMixin<RenderBox> {
} }
} }
/// Whether overflowing children should be clipped, or their overflows be /// Whether overflowing children should be clipped, or their overflow be
/// visible. /// visible.
enum Overflow { enum Overflow {
/// Children's overflows will be visible. /// Overflowing children will be visible.
visible, visible,
/// Children's overflows will be clipped. /// Overflowing children will be clipped to the bounds of their parent.
clip clip,
} }
/// Implements the stack layout algorithm /// Implements the stack layout algorithm
......
...@@ -1077,9 +1077,9 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget { ...@@ -1077,9 +1077,9 @@ class SizedOverflowBox extends SingleChildRenderObjectWidget {
/// A widget that lays the child out as if it was in the tree, but without painting anything, /// A widget that lays the child out as if it was in the tree, but without painting anything,
/// without making the child available for hit testing, and without taking any /// without making the child available for hit testing, and without taking any
/// room in the parent. /// room in the parent.
class OffStage extends SingleChildRenderObjectWidget { class Offstage extends SingleChildRenderObjectWidget {
/// Creates a widget that visually hides its child. /// Creates a widget that visually hides its child.
OffStage({ Key key, this.offstage: true, Widget child }) Offstage({ Key key, this.offstage: true, Widget child })
: super(key: key, child: child) { : super(key: key, child: child) {
assert(offstage != null); assert(offstage != null);
} }
...@@ -1094,10 +1094,10 @@ class OffStage extends SingleChildRenderObjectWidget { ...@@ -1094,10 +1094,10 @@ class OffStage extends SingleChildRenderObjectWidget {
final bool offstage; final bool offstage;
@override @override
RenderOffStage createRenderObject(BuildContext context) => new RenderOffStage(offstage: offstage); RenderOffstage createRenderObject(BuildContext context) => new RenderOffstage(offstage: offstage);
@override @override
void updateRenderObject(BuildContext context, RenderOffStage renderObject) { void updateRenderObject(BuildContext context, RenderOffstage renderObject) {
renderObject.offstage = offstage; renderObject.offstage = offstage;
} }
...@@ -1106,6 +1106,22 @@ class OffStage extends SingleChildRenderObjectWidget { ...@@ -1106,6 +1106,22 @@ class OffStage extends SingleChildRenderObjectWidget {
super.debugFillDescription(description); super.debugFillDescription(description);
description.add('offstage: $offstage'); description.add('offstage: $offstage');
} }
@override
_OffstageElement createElement() => new _OffstageElement(this);
}
class _OffstageElement extends SingleChildRenderObjectElement {
_OffstageElement(Offstage widget) : super(widget);
@override
Offstage get widget => super.widget;
@override
void visitChildrenForSemantics(ElementVisitor visitor) {
if (!widget.offstage)
super.visitChildrenForSemantics(visitor);
}
} }
/// A widget that attempts to size the child to a specific aspect ratio. /// A widget that attempts to size the child to a specific aspect ratio.
...@@ -2646,6 +2662,14 @@ class IgnorePointer extends SingleChildRenderObjectWidget { ...@@ -2646,6 +2662,14 @@ class IgnorePointer extends SingleChildRenderObjectWidget {
..ignoring = ignoring ..ignoring = ignoring
..ignoringSemantics = ignoringSemantics; ..ignoringSemantics = ignoringSemantics;
} }
@override
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
description.add('ignoring: $ignoring');
if (ignoringSemantics != null)
description.add('ignoringSemantics: $ignoringSemantics');
}
} }
/// A widget that absorbs pointers during hit testing. /// A widget that absorbs pointers during hit testing.
......
...@@ -201,6 +201,63 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren ...@@ -201,6 +201,63 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
bool _buildingDirtyElements = false; bool _buildingDirtyElements = false;
/// Pump the build and rendering pipeline to generate a frame.
///
/// This method is called by [handleBeginFrame], which itself is called
/// automatically by the engine when when it is time to lay out and paint a
/// frame.
///
/// Each frame consists of the following phases:
///
/// 1. The animation phase: The [handleBeginFrame] method, which is registered
/// with [ui.window.onBeginFrame], invokes all the transient frame callbacks
/// registered with [scheduleFrameCallback] and [addFrameCallback], in
/// registration order. This includes all the [Ticker] instances that are
/// driving [AnimationController] objects, which means all of the active
/// [Animation] objects tick at this point.
///
/// [handleBeginFrame] then invokes all the persistent frame callbacks, of which
/// the most notable is this method, [beginFrame], which proceeds as follows:
///
/// 2. The build phase: All the dirty [Element]s in the widget tree are
/// rebuilt (see [State.build]). See [State.setState] for further details on
/// marking a widget dirty for building. See [BuildOwner] for more information
/// on this step.
///
/// 3. The layout phase: All the dirty [RenderObject]s in the system are laid
/// out (see [RenderObject.performLayout]). See [RenderObject.markNeedsLayout]
/// for further details on marking an object dirty for layout.
///
/// 4. The compositing bits phase: The compositing bits on any dirty
/// [RenderObject] objects are updated. See
/// [RenderObject.markNeedsCompositingBitsUpdate].
///
/// 5. The paint phase: All the dirty [RenderObject]s in the system are
/// repainted (see [RenderObject.paint]). This generates the [Layer] tree. See
/// [RenderObject.markNeedsPaint] for further details on marking an object
/// dirty for paint.
///
/// 6. The compositing phase: The layer tree is turned into a [ui.Scene] and
/// sent to the GPU.
///
/// 7. The semantics phase: All the dirty [RenderObject]s in the system have
/// their semantics updated (see [RenderObject.semanticAnnotator]). This
/// generates the [SemanticsNode] tree. See
/// [RenderObject.markNeedsSemanticsUpdate] for further details on marking an
/// object dirty for semantics.
///
/// For more details on steps 3-7, see [PipelineOwner].
///
/// 8. The finalization phase in the widgets layer: The widgets tree is
/// finalized. This causes [State.dispose] to be invoked on any objects that
/// were removed from the widgets tree this frame. See
/// [BuildOwner.finalizeTree] for more details.
///
/// 9. The finalization phase in the scheduler layer: After [beginFrame]
/// returns, [handleBeginFrame] then invokes post-frame callbacks (registered
/// with [addPostFrameCallback].
//
// When editing the above, also update rendering/binding.dart's copy.
@override @override
void beginFrame() { void beginFrame() {
assert(!_buildingDirtyElements); assert(!_buildingDirtyElements);
...@@ -210,6 +267,7 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren ...@@ -210,6 +267,7 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
super.beginFrame(); super.beginFrame();
buildOwner.finalizeTree(); buildOwner.finalizeTree();
// TODO(ianh): Following code should not be included in release mode, only profile and debug modes. // TODO(ianh): Following code should not be included in release mode, only profile and debug modes.
// See https://github.com/dart-lang/sdk/issues/27192
if (_needToReportFirstFrame) { if (_needToReportFirstFrame) {
if (_thisFrameWasUseful) { if (_thisFrameWasUseful) {
developer.Timeline.instantSync('Widgets completed first useful frame'); developer.Timeline.instantSync('Widgets completed first useful frame');
...@@ -298,7 +356,8 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi ...@@ -298,7 +356,8 @@ class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWi
@override @override
void updateRenderObject(BuildContext context, RenderObject renderObject) { } void updateRenderObject(BuildContext context, RenderObject renderObject) { }
/// Inflate this widget and actually set the resulting [RenderObject] as the child of [container]. /// Inflate this widget and actually set the resulting [RenderObject] as the
/// child of [container].
/// ///
/// If `element` is null, this function will create a new element. Otherwise, /// If `element` is null, this function will create a new element. Otherwise,
/// the given element will be updated with this widget. /// the given element will be updated with this widget.
......
...@@ -11,6 +11,12 @@ import 'table.dart'; ...@@ -11,6 +11,12 @@ import 'table.dart';
/// Log the dirty widgets that are built each frame. /// Log the dirty widgets that are built each frame.
bool debugPrintRebuildDirtyWidgets = false; bool debugPrintRebuildDirtyWidgets = false;
/// Log when widgets with global keys are deactivated and log when they are
/// reactivated (retaken).
///
/// This can help track down framework bugs relating to the [GlobalKey] logic.
bool debugPrintGlobalKeyedWidgetLifecycle = false;
Key _firstNonUniqueKey(Iterable<Widget> widgets) { Key _firstNonUniqueKey(Iterable<Widget> widgets) {
Set<Key> keySet = new HashSet<Key>(); Set<Key> keySet = new HashSet<Key>();
for (Widget widget in widgets) { for (Widget widget in widgets) {
......
...@@ -103,8 +103,8 @@ abstract class Route<T> { ...@@ -103,8 +103,8 @@ abstract class Route<T> {
/// route), then [isCurrent] will also be true. /// route), then [isCurrent] will also be true.
/// ///
/// If a later route is entirely opaque, then the route will be active but not /// If a later route is entirely opaque, then the route will be active but not
/// rendered. In particular, it's possible for a route to be active but for /// rendered. It is even possible for the route to be active but for the stateful
/// stateful widgets within the route to not be instantiated. /// widgets within the route to not be instatiated. See [ModalRoute.maintainState].
bool get isActive { bool get isActive {
if (_navigator == null) if (_navigator == null)
return false; return false;
...@@ -118,7 +118,7 @@ class RouteSettings { ...@@ -118,7 +118,7 @@ class RouteSettings {
/// Creates data used to construct routes. /// Creates data used to construct routes.
const RouteSettings({ const RouteSettings({
this.name, this.name,
this.isInitialRoute: false this.isInitialRoute: false,
}); });
/// The name of the route (e.g., "/settings"). /// The name of the route (e.g., "/settings").
......
...@@ -20,7 +20,7 @@ const Color _kTransparent = const Color(0x00000000); ...@@ -20,7 +20,7 @@ const Color _kTransparent = const Color(0x00000000);
/// A route that displays widgets in the [Navigator]'s [Overlay]. /// A route that displays widgets in the [Navigator]'s [Overlay].
abstract class OverlayRoute<T> extends Route<T> { abstract class OverlayRoute<T> extends Route<T> {
/// Subclasses should override this getter to return the builders for the overlay. /// Subclasses should override this getter to return the builders for the overlay.
List<WidgetBuilder> get builders; Iterable<OverlayEntry> createOverlayEntries();
/// The entries this route has placed in the overlay. /// The entries this route has placed in the overlay.
@override @override
...@@ -30,8 +30,7 @@ abstract class OverlayRoute<T> extends Route<T> { ...@@ -30,8 +30,7 @@ abstract class OverlayRoute<T> extends Route<T> {
@override @override
void install(OverlayEntry insertionPoint) { void install(OverlayEntry insertionPoint) {
assert(_overlayEntries.isEmpty); assert(_overlayEntries.isEmpty);
for (WidgetBuilder builder in builders) _overlayEntries.addAll(createOverlayEntries());
_overlayEntries.add(new OverlayEntry(builder: builder));
navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint); navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
} }
...@@ -415,7 +414,7 @@ class _ModalScopeState extends State<_ModalScope> { ...@@ -415,7 +414,7 @@ class _ModalScopeState extends State<_ModalScope> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new Focus( return new Focus(
key: config.route.focusKey, key: config.route.focusKey,
child: new OffStage( child: new Offstage(
offstage: config.route.offstage, offstage: config.route.offstage,
child: new IgnorePointer( child: new IgnorePointer(
ignoring: config.route.animation?.status == AnimationStatus.reverse, ignoring: config.route.animation?.status == AnimationStatus.reverse,
...@@ -577,6 +576,13 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -577,6 +576,13 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// be transparent. /// be transparent.
Color get barrierColor; Color get barrierColor;
/// Whether the route should remain in memory when it is inactive. If this is
/// true, then the route is maintained, so that any futures it is holding from
/// the next route will properly resolve when the next route pops. If this is
/// not necessary, this can be set to false to allow the framework to entirely
/// discard the route's widget hierarchy when it is not visible.
bool get maintainState;
// The API for _ModalScope and HeroController // The API for _ModalScope and HeroController
...@@ -654,10 +660,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -654,10 +660,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
} }
@override @override
List<WidgetBuilder> get builders => <WidgetBuilder>[ Iterable<OverlayEntry> createOverlayEntries() sync* {
_buildModalBarrier, yield new OverlayEntry(builder: _buildModalBarrier);
_buildModalScope yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
]; }
@override @override
String toString() => '$runtimeType($settings, animation: $_animation)'; String toString() => '$runtimeType($settings, animation: $_animation)';
...@@ -671,6 +677,9 @@ abstract class PopupRoute<T> extends ModalRoute<T> { ...@@ -671,6 +677,9 @@ abstract class PopupRoute<T> extends ModalRoute<T> {
@override @override
bool get opaque => false; bool get opaque => false;
@override
bool get maintainState => true;
@override @override
void didChangeNext(Route<dynamic> nextRoute) { void didChangeNext(Route<dynamic> nextRoute) {
assert(nextRoute is! PageRoute<dynamic>); assert(nextRoute is! PageRoute<dynamic>);
......
...@@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart'; ...@@ -6,7 +6,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
void main() { void main() {
testWidgets('Navigator.push works within a PopupMenuButton ', (WidgetTester tester) async { testWidgets('Navigator.push works within a PopupMenuButton', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
new MaterialApp( new MaterialApp(
routes: <String, WidgetBuilder> { routes: <String, WidgetBuilder> {
...@@ -46,9 +46,9 @@ void main() { ...@@ -46,9 +46,9 @@ void main() {
expect(find.text('Next'), findsNothing); expect(find.text('Next'), findsNothing);
await tester.tap(find.text('One')); await tester.tap(find.text('One'));
await tester.pump(); // return the future
await tester.pump(); await tester.pump(); // start the navigation
await tester.pump(const Duration(seconds: 1)); // finish the menu animation await tester.pump(const Duration(seconds: 1)); // end the navigation
expect(find.text('One'), findsNothing); expect(find.text('One'), findsNothing);
expect(find.text('Next'), findsOneWidget); expect(find.text('Next'), findsOneWidget);
......
...@@ -15,7 +15,7 @@ void main() { ...@@ -15,7 +15,7 @@ void main() {
// viewport incoming constraints are tight 800x600 // viewport incoming constraints are tight 800x600
// viewport is vertical by default // viewport is vertical by default
RenderBox root = new RenderViewport( RenderBox root = new RenderViewport(
child: new RenderOffStage( child: new RenderOffstage(
child: new RenderCustomPaint( child: new RenderCustomPaint(
painter: new TestCallbackPainter( painter: new TestCallbackPainter(
onPaint: () { painted = true; } onPaint: () { painted = true; }
......
...@@ -59,20 +59,20 @@ void main() { ...@@ -59,20 +59,20 @@ void main() {
// the initial setup. // the initial setup.
expect(find.byKey(firstKey), isOnStage); expect(find.byKey(firstKey), isOnstage);
expect(find.byKey(firstKey), isInCard); expect(find.byKey(firstKey), isInCard);
expect(find.byKey(secondKey), findsNothing); expect(find.byKey(secondKey), findsNothing);
await tester.tap(find.text('two')); await tester.tap(find.text('two'));
await tester.pump(); // begin navigation await tester.pump(); // begin navigation
// at this stage, the second route is off-stage, so that we can form the // at this stage, the second route is offstage, so that we can form the
// hero party. // hero party.
expect(find.byKey(firstKey), isOnStage); expect(find.byKey(firstKey), isOnstage);
expect(find.byKey(firstKey), isInCard); expect(find.byKey(firstKey), isInCard);
expect(find.byKey(secondKey), isOffStage); expect(find.byKey(secondKey, skipOffstage: false), isOffstage);
expect(find.byKey(secondKey), isInCard); expect(find.byKey(secondKey, skipOffstage: false), isInCard);
await tester.pump(); await tester.pump();
...@@ -80,7 +80,7 @@ void main() { ...@@ -80,7 +80,7 @@ void main() {
// seeing them at t=16ms. The original page no longer contains the hero. // seeing them at t=16ms. The original page no longer contains the hero.
expect(find.byKey(firstKey), findsNothing); expect(find.byKey(firstKey), findsNothing);
expect(find.byKey(secondKey), isOnStage); expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isNotInCard); expect(find.byKey(secondKey), isNotInCard);
await tester.pump(); await tester.pump();
...@@ -88,16 +88,17 @@ void main() { ...@@ -88,16 +88,17 @@ void main() {
// t=32ms for the journey. Surely they are still at it. // t=32ms for the journey. Surely they are still at it.
expect(find.byKey(firstKey), findsNothing); expect(find.byKey(firstKey), findsNothing);
expect(find.byKey(secondKey), isOnStage); expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isNotInCard); expect(find.byKey(secondKey), isNotInCard);
await tester.pump(new Duration(seconds: 1)); await tester.pump(new Duration(seconds: 1));
// t=1.032s for the journey. The journey has ended (it ends this frame, in // t=1.032s for the journey. The journey has ended (it ends this frame, in
// fact). The hero should now be in the new page, on-stage. // fact). The hero should now be in the new page, onstage. The original
// widget will be back as well now (though not visible).
expect(find.byKey(firstKey), findsNothing); expect(find.byKey(firstKey), findsNothing);
expect(find.byKey(secondKey), isOnStage); expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isInCard); expect(find.byKey(secondKey), isInCard);
await tester.pump(); await tester.pump();
...@@ -105,7 +106,7 @@ void main() { ...@@ -105,7 +106,7 @@ void main() {
// Should not change anything. // Should not change anything.
expect(find.byKey(firstKey), findsNothing); expect(find.byKey(firstKey), findsNothing);
expect(find.byKey(secondKey), isOnStage); expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isInCard); expect(find.byKey(secondKey), isInCard);
// Now move on to view 3 // Now move on to view 3
...@@ -113,13 +114,13 @@ void main() { ...@@ -113,13 +114,13 @@ void main() {
await tester.tap(find.text('three')); await tester.tap(find.text('three'));
await tester.pump(); // begin navigation await tester.pump(); // begin navigation
// at this stage, the second route is off-stage, so that we can form the // at this stage, the second route is offstage, so that we can form the
// hero party. // hero party.
expect(find.byKey(secondKey), isOnStage); expect(find.byKey(secondKey), isOnstage);
expect(find.byKey(secondKey), isInCard); expect(find.byKey(secondKey), isInCard);
expect(find.byKey(thirdKey), isOffStage); expect(find.byKey(thirdKey, skipOffstage: false), isOffstage);
expect(find.byKey(thirdKey), isInCard); expect(find.byKey(thirdKey, skipOffstage: false), isInCard);
await tester.pump(); await tester.pump();
...@@ -127,7 +128,7 @@ void main() { ...@@ -127,7 +128,7 @@ void main() {
// seeing them at t=16ms. The original page no longer contains the hero. // seeing them at t=16ms. The original page no longer contains the hero.
expect(find.byKey(secondKey), findsNothing); expect(find.byKey(secondKey), findsNothing);
expect(find.byKey(thirdKey), isOnStage); expect(find.byKey(thirdKey), isOnstage);
expect(find.byKey(thirdKey), isNotInCard); expect(find.byKey(thirdKey), isNotInCard);
await tester.pump(); await tester.pump();
...@@ -135,16 +136,16 @@ void main() { ...@@ -135,16 +136,16 @@ void main() {
// t=32ms for the journey. Surely they are still at it. // t=32ms for the journey. Surely they are still at it.
expect(find.byKey(secondKey), findsNothing); expect(find.byKey(secondKey), findsNothing);
expect(find.byKey(thirdKey), isOnStage); expect(find.byKey(thirdKey), isOnstage);
expect(find.byKey(thirdKey), isNotInCard); expect(find.byKey(thirdKey), isNotInCard);
await tester.pump(new Duration(seconds: 1)); await tester.pump(new Duration(seconds: 1));
// t=1.032s for the journey. The journey has ended (it ends this frame, in // t=1.032s for the journey. The journey has ended (it ends this frame, in
// fact). The hero should now be in the new page, on-stage. // fact). The hero should now be in the new page, onstage.
expect(find.byKey(secondKey), findsNothing); expect(find.byKey(secondKey), findsNothing);
expect(find.byKey(thirdKey), isOnStage); expect(find.byKey(thirdKey), isOnstage);
expect(find.byKey(thirdKey), isInCard); expect(find.byKey(thirdKey), isInCard);
await tester.pump(); await tester.pump();
...@@ -152,7 +153,7 @@ void main() { ...@@ -152,7 +153,7 @@ void main() {
// Should not change anything. // Should not change anything.
expect(find.byKey(secondKey), findsNothing); expect(find.byKey(secondKey), findsNothing);
expect(find.byKey(thirdKey), isOnStage); expect(find.byKey(thirdKey), isOnstage);
expect(find.byKey(thirdKey), isInCard); expect(find.byKey(thirdKey), isInCard);
}); });
...@@ -187,8 +188,8 @@ void main() { ...@@ -187,8 +188,8 @@ void main() {
final Duration duration = const Duration(milliseconds: 300); final Duration duration = const Duration(milliseconds: 300);
final Curve curve = Curves.fastOutSlowIn; final Curve curve = Curves.fastOutSlowIn;
final double initialHeight = tester.getSize(find.byKey(firstKey)).height; final double initialHeight = tester.getSize(find.byKey(firstKey, skipOffstage: false)).height;
final double finalHeight = tester.getSize(find.byKey(secondKey)).height; final double finalHeight = tester.getSize(find.byKey(secondKey, skipOffstage: false)).height;
final double deltaHeight = finalHeight - initialHeight; final double deltaHeight = finalHeight - initialHeight;
final double epsilon = 0.001; final double epsilon = 0.001;
...@@ -267,18 +268,18 @@ void main() { ...@@ -267,18 +268,18 @@ void main() {
navigator.pushNamed('/next'); navigator.pushNamed('/next');
expect(log, isEmpty); expect(log, isEmpty);
await tester.tap(find.text('foo')); await tester.tap(find.text('foo', skipOffstage: false));
expect(log, isEmpty); expect(log, isEmpty);
await tester.pump(new Duration(milliseconds: 10)); await tester.pump(new Duration(milliseconds: 10));
await tester.tap(find.text('foo')); await tester.tap(find.text('foo', skipOffstage: false));
expect(log, isEmpty); expect(log, isEmpty);
await tester.tap(find.text('bar')); await tester.tap(find.text('bar', skipOffstage: false));
expect(log, isEmpty); expect(log, isEmpty);
await tester.pump(new Duration(milliseconds: 10)); await tester.pump(new Duration(milliseconds: 10));
expect(find.text('foo'), findsNothing); expect(find.text('foo'), findsNothing);
await tester.tap(find.text('bar')); await tester.tap(find.text('bar', skipOffstage: false));
expect(log, isEmpty); expect(log, isEmpty);
await tester.pump(new Duration(seconds: 1)); await tester.pump(new Duration(seconds: 1));
......
...@@ -69,33 +69,51 @@ class ThirdWidget extends StatelessWidget { ...@@ -69,33 +69,51 @@ class ThirdWidget extends StatelessWidget {
void main() { void main() {
testWidgets('Can navigator navigate to and from a stateful widget', (WidgetTester tester) async { testWidgets('Can navigator navigate to and from a stateful widget', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/': (BuildContext context) => new FirstWidget(), '/': (BuildContext context) => new FirstWidget(), // X
'/second': (BuildContext context) => new SecondWidget(), '/second': (BuildContext context) => new SecondWidget(), // Y
}; };
await tester.pumpWidget(new MaterialApp(routes: routes)); await tester.pumpWidget(new MaterialApp(routes: routes));
expect(find.text('X'), findsOneWidget); expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsNothing); expect(find.text('Y', skipOffstage: false), findsNothing);
await tester.tap(find.text('X')); await tester.tap(find.text('X'));
await tester.pump(const Duration(milliseconds: 10)); await tester.pump();
expect(find.text('X'), findsOneWidget);
expect(find.text('Y', skipOffstage: false), isOffstage);
await tester.pump(const Duration(milliseconds: 10));
expect(find.text('X'), findsOneWidget); expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsOneWidget); expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(find.text('X'), findsNothing);
expect(find.text('X', skipOffstage: false), findsOneWidget);
expect(find.text('Y'), findsOneWidget);
await tester.tap(find.text('Y')); await tester.tap(find.text('Y'));
expect(find.text('X'), findsNothing);
expect(find.text('Y'), findsOneWidget);
await tester.pump();
expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 10)); await tester.pump(const Duration(milliseconds: 10));
await tester.pump(const Duration(milliseconds: 10)); expect(find.text('X'), findsOneWidget);
await tester.pump(const Duration(milliseconds: 10)); expect(find.text('Y'), findsOneWidget);
await tester.pump(const Duration(seconds: 1));
await tester.pump(const Duration(seconds: 1));
expect(find.text('X'), findsOneWidget); expect(find.text('X'), findsOneWidget);
expect(find.text('Y'), findsNothing); expect(find.text('Y', skipOffstage: false), findsNothing);
}); });
testWidgets('Navigator.of fails gracefully when not found in context', (WidgetTester tester) async { testWidgets('Navigator.of fails gracefully when not found in context', (WidgetTester tester) async {
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:flutter_test/flutter_test.dart' hide TypeMatcher; import 'package:flutter_test/flutter_test.dart' hide TypeMatcher;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class TestTransition extends AnimatedWidget { class TestTransition extends AnimatedWidget {
TestTransition({ TestTransition({
...@@ -26,7 +27,7 @@ class TestTransition extends AnimatedWidget { ...@@ -26,7 +27,7 @@ class TestTransition extends AnimatedWidget {
} }
class TestRoute<T> extends PageRoute<T> { class TestRoute<T> extends PageRoute<T> {
TestRoute({ this.child, RouteSettings settings}) : super(settings: settings); TestRoute({ this.child, RouteSettings settings }) : super(settings: settings);
final Widget child; final Widget child;
...@@ -36,6 +37,9 @@ class TestRoute<T> extends PageRoute<T> { ...@@ -36,6 +37,9 @@ class TestRoute<T> extends PageRoute<T> {
@override @override
Color get barrierColor => null; Color get barrierColor => null;
@override
bool get maintainState => false;
@override @override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) { Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
return child; return child;
...@@ -50,21 +54,21 @@ void main() { ...@@ -50,21 +54,21 @@ void main() {
GlobalKey insideKey = new GlobalKey(); GlobalKey insideKey = new GlobalKey();
String state() { String state({ bool skipOffstage: true }) {
String result = ''; String result = '';
if (tester.any(find.text('A'))) if (tester.any(find.text('A', skipOffstage: skipOffstage)))
result += 'A'; result += 'A';
if (tester.any(find.text('B'))) if (tester.any(find.text('B', skipOffstage: skipOffstage)))
result += 'B'; result += 'B';
if (tester.any(find.text('C'))) if (tester.any(find.text('C', skipOffstage: skipOffstage)))
result += 'C'; result += 'C';
if (tester.any(find.text('D'))) if (tester.any(find.text('D', skipOffstage: skipOffstage)))
result += 'D'; result += 'D';
if (tester.any(find.text('E'))) if (tester.any(find.text('E', skipOffstage: skipOffstage)))
result += 'E'; result += 'E';
if (tester.any(find.text('F'))) if (tester.any(find.text('F', skipOffstage: skipOffstage)))
result += 'F'; result += 'F';
if (tester.any(find.text('G'))) if (tester.any(find.text('G', skipOffstage: skipOffstage)))
result += 'G'; result += 'G';
return result; return result;
} }
...@@ -112,7 +116,8 @@ void main() { ...@@ -112,7 +116,8 @@ void main() {
navigator.pushNamed('/2'); navigator.pushNamed('/2');
expect(state(), equals('BC')); // transition 1->2 is not yet built expect(state(), equals('BC')); // transition 1->2 is not yet built
await tester.pump(); await tester.pump();
expect(state(), equals('BCE')); // transition 1->2 is at 0.0 expect(state(), equals('BC')); // transition 1->2 is at 0.0
expect(state(skipOffstage: false), equals('BCE')); // E is offstage
await tester.pump(kFourTenthsOfTheTransitionDuration); await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCE')); // transition 1->2 is at 0.4 expect(state(), equals('BCE')); // transition 1->2 is at 0.4
...@@ -122,7 +127,7 @@ void main() { ...@@ -122,7 +127,7 @@ void main() {
await tester.pump(kFourTenthsOfTheTransitionDuration); await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('E')); // transition 1->2 is at 1.0 expect(state(), equals('E')); // transition 1->2 is at 1.0
expect(state(skipOffstage: false), equals('E')); // B and C are gone, the route is inactive with maintainState=false
navigator.pop(); navigator.pop();
expect(state(), equals('E')); // transition 1<-2 is at 1.0, just reversed expect(state(), equals('E')); // transition 1<-2 is at 1.0, just reversed
...@@ -135,10 +140,12 @@ void main() { ...@@ -135,10 +140,12 @@ void main() {
navigator.pushNamed('/3'); navigator.pushNamed('/3');
expect(state(), equals('BDE')); // transition 1<-2 is at 0.6 expect(state(), equals('BDE')); // transition 1<-2 is at 0.6
await tester.pump(); await tester.pump();
expect(state(), equals('BDEF')); // transition 1<-2 is at 0.6, 1->3 is at 0.0 expect(state(), equals('BDE')); // transition 1<-2 is at 0.6, 1->3 is at 0.0
expect(state(skipOffstage: false), equals('BDEF')); // F is offstage since we're at 0.0
await tester.pump(kFourTenthsOfTheTransitionDuration); await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCEF')); // transition 1<-2 is at 0.2, 1->3 is at 0.4 expect(state(), equals('BCEF')); // transition 1<-2 is at 0.2, 1->3 is at 0.4
expect(state(skipOffstage: false), equals('BCEF')); // nothing secret going on here
await tester.pump(kFourTenthsOfTheTransitionDuration); await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BDF')); // transition 1<-2 is done, 1->3 is at 0.8 expect(state(), equals('BDF')); // transition 1<-2 is done, 1->3 is at 0.8
...@@ -157,7 +164,8 @@ void main() { ...@@ -157,7 +164,8 @@ void main() {
navigator.pushNamed('/4'); navigator.pushNamed('/4');
expect(state(), equals('BCF')); // transition 1<-3 is at 0.2, 1->4 is not yet built expect(state(), equals('BCF')); // transition 1<-3 is at 0.2, 1->4 is not yet built
await tester.pump(); await tester.pump();
expect(state(), equals('BCFG')); // transition 1<-3 is at 0.2, 1->4 is at 0.0 expect(state(), equals('BCF')); // transition 1<-3 is at 0.2, 1->4 is at 0.0
expect(state(skipOffstage: false), equals('BCFG')); // G is offstage
await tester.pump(kFourTenthsOfTheTransitionDuration); await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('BCG')); // transition 1<-3 is done, 1->4 is at 0.4 expect(state(), equals('BCG')); // transition 1<-3 is done, 1->4 is at 0.4
...@@ -167,6 +175,7 @@ void main() { ...@@ -167,6 +175,7 @@ void main() {
await tester.pump(kFourTenthsOfTheTransitionDuration); await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(state(), equals('G')); // transition 1->4 is done expect(state(), equals('G')); // transition 1->4 is done
expect(state(skipOffstage: false), equals('G')); // route 1 is not around any more
}); });
} }
...@@ -7,7 +7,9 @@ import 'package:flutter/material.dart'; ...@@ -7,7 +7,9 @@ import 'package:flutter/material.dart';
class TestOverlayRoute extends OverlayRoute<Null> { class TestOverlayRoute extends OverlayRoute<Null> {
@override @override
List<WidgetBuilder> get builders => <WidgetBuilder>[ _build ]; Iterable<OverlayEntry> createOverlayEntries() sync* {
yield new OverlayEntry(builder: _build);
}
Widget _build(BuildContext context) => new Text('Overlay'); Widget _build(BuildContext context) => new Text('Overlay');
} }
...@@ -22,7 +24,7 @@ void main() { ...@@ -22,7 +24,7 @@ void main() {
await tester.pumpWidget(new MaterialApp(routes: routes)); await tester.pumpWidget(new MaterialApp(routes: routes));
expect(find.text('Home'), isOnStage); expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), findsNothing); expect(find.text('Settings'), findsNothing);
expect(find.text('Overlay'), findsNothing); expect(find.text('Overlay'), findsNothing);
...@@ -32,20 +34,20 @@ void main() { ...@@ -32,20 +34,20 @@ void main() {
await tester.pump(); await tester.pump();
expect(find.text('Home'), isOnStage); expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), isOffStage); expect(find.text('Settings', skipOffstage: false), isOffstage);
expect(find.text('Overlay'), findsNothing); expect(find.text('Overlay'), findsNothing);
await tester.pump(const Duration(milliseconds: 16)); await tester.pump(const Duration(milliseconds: 16));
expect(find.text('Home'), isOnStage); expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), isOnStage); expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing); expect(find.text('Overlay'), findsNothing);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), findsNothing); expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage); expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing); expect(find.text('Overlay'), findsNothing);
Navigator.push(containerKey2.currentContext, new TestOverlayRoute()); Navigator.push(containerKey2.currentContext, new TestOverlayRoute());
...@@ -53,40 +55,40 @@ void main() { ...@@ -53,40 +55,40 @@ void main() {
await tester.pump(); await tester.pump();
expect(find.text('Home'), findsNothing); expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage); expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), isOnStage); expect(find.text('Overlay'), isOnstage);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), findsNothing); expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage); expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), isOnStage); expect(find.text('Overlay'), isOnstage);
expect(Navigator.canPop(containerKey2.currentContext), isTrue); expect(Navigator.canPop(containerKey2.currentContext), isTrue);
Navigator.pop(containerKey2.currentContext); Navigator.pop(containerKey2.currentContext);
await tester.pump(); await tester.pump();
expect(find.text('Home'), findsNothing); expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage); expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing); expect(find.text('Overlay'), findsNothing);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), findsNothing); expect(find.text('Home'), findsNothing);
expect(find.text('Settings'), isOnStage); expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing); expect(find.text('Overlay'), findsNothing);
expect(Navigator.canPop(containerKey2.currentContext), isTrue); expect(Navigator.canPop(containerKey2.currentContext), isTrue);
Navigator.pop(containerKey2.currentContext); Navigator.pop(containerKey2.currentContext);
await tester.pump(); await tester.pump();
expect(find.text('Home'), isOnStage); expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), isOnStage); expect(find.text('Settings'), isOnstage);
expect(find.text('Overlay'), findsNothing); expect(find.text('Overlay'), findsNothing);
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(find.text('Home'), isOnStage); expect(find.text('Home'), isOnstage);
expect(find.text('Settings'), findsNothing); expect(find.text('Settings'), findsNothing);
expect(find.text('Overlay'), findsNothing); expect(find.text('Overlay'), findsNothing);
......
...@@ -2,10 +2,13 @@ ...@@ -2,10 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ThePositiveNumbers extends StatelessWidget { class ThePositiveNumbers extends StatelessWidget {
ThePositiveNumbers({ @required this.from });
final int from;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return new ScrollableLazyList( return new ScrollableLazyList(
...@@ -13,104 +16,110 @@ class ThePositiveNumbers extends StatelessWidget { ...@@ -13,104 +16,110 @@ class ThePositiveNumbers extends StatelessWidget {
itemBuilder: (BuildContext context, int start, int count) { itemBuilder: (BuildContext context, int start, int count) {
List<Widget> result = new List<Widget>(); List<Widget> result = new List<Widget>();
for (int index = start; index < start + count; index += 1) for (int index = start; index < start + count; index += 1)
result.add(new Text('$index', key: new ValueKey<int>(index))); result.add(new Text('${index + from}', key: new ValueKey<int>(index)));
return result; return result;
} }
); );
} }
} }
void main() { Future<Null> performTest(WidgetTester tester, bool maintainState) async {
testWidgets('whether we remember our scroll position', (WidgetTester tester) async { GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>();
GlobalKey<NavigatorState> navigatorKey = new GlobalKey<NavigatorState>(); await tester.pumpWidget(new Navigator(
await tester.pumpWidget(new Navigator( key: navigatorKey,
key: navigatorKey, onGenerateRoute: (RouteSettings settings) {
onGenerateRoute: (RouteSettings settings) { if (settings.name == '/') {
if (settings.name == '/') { return new MaterialPageRoute<Null>(
return new MaterialPageRoute<Null>( settings: settings,
settings: settings, builder: (_) => new Container(child: new ThePositiveNumbers(from: 0)),
builder: (_) => new Container(child: new ThePositiveNumbers()) maintainState: maintainState,
); );
} else if (settings.name == '/second') { } else if (settings.name == '/second') {
return new MaterialPageRoute<Null>( return new MaterialPageRoute<Null>(
settings: settings, settings: settings,
builder: (_) => new Container(child: new ThePositiveNumbers()) builder: (_) => new Container(child: new ThePositiveNumbers(from: 10000)),
); maintainState: maintainState,
} );
return null;
} }
)); return null;
}
));
// we're 600 pixels high, each item is 100 pixels high, scroll position is // we're 600 pixels high, each item is 100 pixels high, scroll position is
// zero, so we should have exactly 6 items, 0..5. // zero, so we should have exactly 6 items, 0..5.
expect(find.text('0'), findsOneWidget); expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsOneWidget); expect(find.text('1'), findsOneWidget);
expect(find.text('2'), findsOneWidget); expect(find.text('2'), findsOneWidget);
expect(find.text('3'), findsOneWidget); expect(find.text('3'), findsOneWidget);
expect(find.text('4'), findsOneWidget); expect(find.text('4'), findsOneWidget);
expect(find.text('5'), findsOneWidget); expect(find.text('5'), findsOneWidget);
expect(find.text('6'), findsNothing); expect(find.text('6'), findsNothing);
expect(find.text('10'), findsNothing); expect(find.text('10'), findsNothing);
expect(find.text('100'), findsNothing); expect(find.text('100'), findsNothing);
ScrollableState targetState = tester.state(find.byType(Scrollable)); ScrollableState targetState = tester.state(find.byType(Scrollable));
targetState.scrollTo(1000.0); targetState.scrollTo(1000.0);
await tester.pump(new Duration(seconds: 1)); await tester.pump(new Duration(seconds: 1));
// we're 600 pixels high, each item is 100 pixels high, scroll position is // we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15. // 1000, so we should have exactly 6 items, 10..15.
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
expect(find.text('8'), findsNothing); expect(find.text('8'), findsNothing);
expect(find.text('9'), findsNothing); expect(find.text('9'), findsNothing);
expect(find.text('10'), findsOneWidget); expect(find.text('10'), findsOneWidget);
expect(find.text('11'), findsOneWidget); expect(find.text('11'), findsOneWidget);
expect(find.text('12'), findsOneWidget); expect(find.text('12'), findsOneWidget);
expect(find.text('13'), findsOneWidget); expect(find.text('13'), findsOneWidget);
expect(find.text('14'), findsOneWidget); expect(find.text('14'), findsOneWidget);
expect(find.text('15'), findsOneWidget); expect(find.text('15'), findsOneWidget);
expect(find.text('16'), findsNothing); expect(find.text('16'), findsNothing);
expect(find.text('100'), findsNothing); expect(find.text('100'), findsNothing);
navigatorKey.currentState.pushNamed('/second'); navigatorKey.currentState.pushNamed('/second');
await tester.pump(); // navigating always takes two frames await tester.pump(); // navigating always takes two frames, one to start...
await tester.pump(new Duration(seconds: 1)); await tester.pump(new Duration(seconds: 1)); // ...and one to end the transition
// same as the first list again // the second list is now visible, starting at 10000
expect(find.text('0'), findsOneWidget); expect(find.text('10000'), findsOneWidget);
expect(find.text('1'), findsOneWidget); expect(find.text('10001'), findsOneWidget);
expect(find.text('2'), findsOneWidget); expect(find.text('10002'), findsOneWidget);
expect(find.text('3'), findsOneWidget); expect(find.text('10003'), findsOneWidget);
expect(find.text('4'), findsOneWidget); expect(find.text('10004'), findsOneWidget);
expect(find.text('5'), findsOneWidget); expect(find.text('10005'), findsOneWidget);
expect(find.text('6'), findsNothing); expect(find.text('10006'), findsNothing);
expect(find.text('10'), findsNothing); expect(find.text('10010'), findsNothing);
expect(find.text('100'), findsNothing); expect(find.text('10100'), findsNothing);
navigatorKey.currentState.pop(); navigatorKey.currentState.pop();
await tester.pump(); // navigating always takes two frames await tester.pump(); // again, navigating always takes two frames
// Ensure we don't clamp the scroll offset even during the navigation. // Ensure we don't clamp the scroll offset even during the navigation.
// https://github.com/flutter/flutter/issues/4883 // https://github.com/flutter/flutter/issues/4883
LazyListViewport viewport = tester.firstWidget(find.byType(LazyListViewport)); LazyListViewport viewport = tester.firstWidget(find.byType(LazyListViewport));
expect(viewport.scrollOffset, equals(1000.0)); expect(viewport.scrollOffset, equals(1000.0));
await tester.pump(new Duration(seconds: 1)); await tester.pump(new Duration(seconds: 1));
// we're 600 pixels high, each item is 100 pixels high, scroll position is // we're 600 pixels high, each item is 100 pixels high, scroll position is
// 1000, so we should have exactly 6 items, 10..15. // 1000, so we should have exactly 6 items, 10..15.
expect(find.text('0'), findsNothing); expect(find.text('0'), findsNothing);
expect(find.text('8'), findsNothing); expect(find.text('8'), findsNothing);
expect(find.text('9'), findsNothing); expect(find.text('9'), findsNothing);
expect(find.text('10'), findsOneWidget); expect(find.text('10'), findsOneWidget);
expect(find.text('11'), findsOneWidget); expect(find.text('11'), findsOneWidget);
expect(find.text('12'), findsOneWidget); expect(find.text('12'), findsOneWidget);
expect(find.text('13'), findsOneWidget); expect(find.text('13'), findsOneWidget);
expect(find.text('14'), findsOneWidget); expect(find.text('14'), findsOneWidget);
expect(find.text('15'), findsOneWidget); expect(find.text('15'), findsOneWidget);
expect(find.text('16'), findsNothing); expect(find.text('16'), findsNothing);
expect(find.text('100'), findsNothing); expect(find.text('100'), findsNothing);
}
void main() {
testWidgets('whether we remember our scroll position', (WidgetTester tester) async {
await performTest(tester, true);
await performTest(tester, false);
}); });
} }
...@@ -327,4 +327,45 @@ void main() { ...@@ -327,4 +327,45 @@ void main() {
await tester.pump(); await tester.pump();
expect(log, isEmpty); expect(log, isEmpty);
}); });
testWidgets('Reparenting with multiple moves', (WidgetTester tester) async {
final GlobalKey key1 = new GlobalKey();
final GlobalKey key2 = new GlobalKey();
final GlobalKey key3 = new GlobalKey();
await tester.pumpWidget(
new Row(
children: <Widget>[
new StateMarker(
key: key1,
child: new StateMarker(
key: key2,
child: new StateMarker(
key: key3,
child: new StateMarker(child: new Container(width: 100.0))
)
)
)
]
)
);
await tester.pumpWidget(
new Row(
children: <Widget>[
new StateMarker(
key: key2,
child: new StateMarker(child: new Container(width: 100.0))
),
new StateMarker(
key: key1,
child: new StateMarker(
key: key3,
child: new StateMarker(child: new Container(width: 100.0))
)
),
]
)
);
});
} }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:meta/meta.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
/// Provides an iterable that efficiently returns all the elements /// Provides an iterable that efficiently returns all the elements
...@@ -15,13 +17,18 @@ import 'package:flutter/widgets.dart'; ...@@ -15,13 +17,18 @@ import 'package:flutter/widgets.dart';
/// The same applies to any iterable obtained indirectly through this /// The same applies to any iterable obtained indirectly through this
/// one, for example the results of calling `where` on this iterable /// one, for example the results of calling `where` on this iterable
/// are also cached. /// are also cached.
Iterable<Element> collectAllElementsFrom(Element rootElement) { Iterable<Element> collectAllElementsFrom(Element rootElement, {
return new CachingIterable<Element>(new _DepthFirstChildIterator(rootElement)); @required bool skipOffstage
}) {
return new CachingIterable<Element>(new _DepthFirstChildIterator(rootElement, skipOffstage));
} }
class _DepthFirstChildIterator implements Iterator<Element> { class _DepthFirstChildIterator implements Iterator<Element> {
_DepthFirstChildIterator(Element rootElement) _DepthFirstChildIterator(Element rootElement, bool skipOffstage)
: _stack = _reverseChildrenOf(rootElement).toList(); : skipOffstage = skipOffstage,
_stack = _reverseChildrenOf(rootElement, skipOffstage).toList();
final bool skipOffstage;
Element _current; Element _current;
...@@ -37,14 +44,18 @@ class _DepthFirstChildIterator implements Iterator<Element> { ...@@ -37,14 +44,18 @@ class _DepthFirstChildIterator implements Iterator<Element> {
_current = _stack.removeLast(); _current = _stack.removeLast();
// Stack children in reverse order to traverse first branch first // Stack children in reverse order to traverse first branch first
_stack.addAll(_reverseChildrenOf(_current)); _stack.addAll(_reverseChildrenOf(_current, skipOffstage));
return true; return true;
} }
static Iterable<Element> _reverseChildrenOf(Element element) { static Iterable<Element> _reverseChildrenOf(Element element, bool skipOffstage) {
final List<Element> children = <Element>[]; final List<Element> children = <Element>[];
element.visitChildren(children.add); if (skipOffstage) {
element.visitChildrenForSemantics(children.add);
} else {
element.visitChildren(children.add);
}
return children.reversed; return children.reversed;
} }
} }
...@@ -85,7 +85,7 @@ class WidgetController { ...@@ -85,7 +85,7 @@ class WidgetController {
/// using [Iterator.moveNext]. /// using [Iterator.moveNext].
Iterable<Element> get allElements { Iterable<Element> get allElements {
TestAsyncUtils.guardSync(); TestAsyncUtils.guardSync();
return collectAllElementsFrom(binding.renderViewElement); return collectAllElementsFrom(binding.renderViewElement, skipOffstage: false);
} }
/// The matching element in the widget tree. /// The matching element in the widget tree.
......
...@@ -27,7 +27,10 @@ class CommonFinders { ...@@ -27,7 +27,10 @@ class CommonFinders {
/// Example: /// Example:
/// ///
/// expect(tester, hasWidget(find.text('Back'))); /// expect(tester, hasWidget(find.text('Back')));
Finder text(String text) => new _TextFinder(text); ///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder text(String text, { bool skipOffstage: true }) => new _TextFinder(text, skipOffstage: skipOffstage);
/// Looks for widgets that contain a [Text] descendant with `text` /// Looks for widgets that contain a [Text] descendant with `text`
/// in it. /// in it.
...@@ -41,8 +44,11 @@ class CommonFinders { ...@@ -41,8 +44,11 @@ class CommonFinders {
/// ///
/// // You can find and tap on it like this: /// // You can find and tap on it like this:
/// tester.tap(find.widgetWithText(Button, 'Update')); /// tester.tap(find.widgetWithText(Button, 'Update'));
Finder widgetWithText(Type widgetType, String text) { ///
return new _WidgetWithTextFinder(widgetType, text); /// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder widgetWithText(Type widgetType, String text, { bool skipOffstage: true }) {
return new _WidgetWithTextFinder(widgetType, text, skipOffstage: skipOffstage);
} }
/// Finds widgets by searching for one with a particular [Key]. /// Finds widgets by searching for one with a particular [Key].
...@@ -50,7 +56,10 @@ class CommonFinders { ...@@ -50,7 +56,10 @@ class CommonFinders {
/// Example: /// Example:
/// ///
/// expect(tester, hasWidget(find.byKey(backKey))); /// expect(tester, hasWidget(find.byKey(backKey)));
Finder byKey(Key key) => new _KeyFinder(key); ///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byKey(Key key, { bool skipOffstage: true }) => new _KeyFinder(key, skipOffstage: skipOffstage);
/// Finds widgets by searching for widgets with a particular type. /// Finds widgets by searching for widgets with a particular type.
/// ///
...@@ -63,7 +72,10 @@ class CommonFinders { ...@@ -63,7 +72,10 @@ class CommonFinders {
/// Example: /// Example:
/// ///
/// expect(tester, hasWidget(find.byType(IconButton))); /// expect(tester, hasWidget(find.byType(IconButton)));
Finder byType(Type type) => new _WidgetTypeFinder(type); ///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byType(Type type, { bool skipOffstage: true }) => new _WidgetTypeFinder(type, skipOffstage: skipOffstage);
/// Finds widgets by searching for elements with a particular type. /// Finds widgets by searching for elements with a particular type.
/// ///
...@@ -76,7 +88,10 @@ class CommonFinders { ...@@ -76,7 +88,10 @@ class CommonFinders {
/// Example: /// Example:
/// ///
/// expect(tester, hasWidget(find.byElementType(SingleChildRenderObjectElement))); /// expect(tester, hasWidget(find.byElementType(SingleChildRenderObjectElement)));
Finder byElementType(Type type) => new _ElementTypeFinder(type); ///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byElementType(Type type, { bool skipOffstage: true }) => new _ElementTypeFinder(type, skipOffstage: skipOffstage);
/// Finds widgets whose current widget is the instance given by the /// Finds widgets whose current widget is the instance given by the
/// argument. /// argument.
...@@ -90,7 +105,10 @@ class CommonFinders { ...@@ -90,7 +105,10 @@ class CommonFinders {
/// ///
/// // You can find and tap on it like this: /// // You can find and tap on it like this:
/// tester.tap(find.byConfig(myButton)); /// tester.tap(find.byConfig(myButton));
Finder byConfig(Widget config) => new _ConfigFinder(config); ///
/// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byConfig(Widget config, { bool skipOffstage: true }) => new _ConfigFinder(config, skipOffstage: skipOffstage);
/// Finds widgets using a widget predicate. /// Finds widgets using a widget predicate.
/// ///
...@@ -99,8 +117,11 @@ class CommonFinders { ...@@ -99,8 +117,11 @@ class CommonFinders {
/// expect(tester, hasWidget(find.byWidgetPredicate( /// expect(tester, hasWidget(find.byWidgetPredicate(
/// (Widget widget) => widget is Tooltip && widget.message == 'Back' /// (Widget widget) => widget is Tooltip && widget.message == 'Back'
/// ))); /// )));
Finder byWidgetPredicate(WidgetPredicate predicate) { ///
return new _WidgetPredicateFinder(predicate); /// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byWidgetPredicate(WidgetPredicate predicate, { bool skipOffstage: true }) {
return new _WidgetPredicateFinder(predicate, skipOffstage: skipOffstage);
} }
/// Finds widgets using an element predicate. /// Finds widgets using an element predicate.
...@@ -113,14 +134,21 @@ class CommonFinders { ...@@ -113,14 +134,21 @@ class CommonFinders {
/// // (contrast with byElementType, which only returns exact matches) /// // (contrast with byElementType, which only returns exact matches)
/// (Element element) => element is SingleChildRenderObjectElement /// (Element element) => element is SingleChildRenderObjectElement
/// ))); /// )));
Finder byElementPredicate(ElementPredicate predicate) { ///
return new _ElementPredicateFinder(predicate); /// If the `skipOffstage` argument is true (the default), then this skips
/// nodes that are [Offstage] or that are from inactive [Route]s.
Finder byElementPredicate(ElementPredicate predicate, { bool skipOffstage: true }) {
return new _ElementPredicateFinder(predicate, skipOffstage: skipOffstage);
} }
} }
/// Searches a widget tree and returns nodes that match a particular /// Searches a widget tree and returns nodes that match a particular
/// pattern. /// pattern.
abstract class Finder { abstract class Finder {
/// Initialises a Finder. Used by subclasses to initialize the [skipOffstage]
/// property.
Finder({ this.skipOffstage: true });
/// Describes what the finder is looking for. The description should be /// Describes what the finder is looking for. The description should be
/// a brief English noun phrase describing the finder's pattern. /// a brief English noun phrase describing the finder's pattern.
String get description; String get description;
...@@ -134,11 +162,19 @@ abstract class Finder { ...@@ -134,11 +162,19 @@ abstract class Finder {
/// function, consider extending [MatchFinder] instead. /// function, consider extending [MatchFinder] instead.
Iterable<Element> apply(Iterable<Element> candidates); Iterable<Element> apply(Iterable<Element> candidates);
// Right now this is hard-coded to just grab the elements from the binding. /// Whether this finder skips nodes that are offstage.
// ///
// One could imagine a world where CommonFinders and Finder can be configured /// If this is true, then the elements are walked using
// to work from a specific subtree, but we'll implement that when it's needed. /// [Element.visitChildrenForSemantics]. This skips offstage children of
static Iterable<Element> get _allElements => collectAllElementsFrom(WidgetsBinding.instance.renderViewElement); /// [Offstage] widgets, as well as children of inactive [Route]s.
final bool skipOffstage;
Iterable<Element> get _allElements {
return collectAllElementsFrom(
WidgetsBinding.instance.renderViewElement,
skipOffstage: skipOffstage
);
}
Iterable<Element> _cachedResult; Iterable<Element> _cachedResult;
...@@ -171,21 +207,26 @@ abstract class Finder { ...@@ -171,21 +207,26 @@ abstract class Finder {
@override @override
String toString() { String toString() {
final String additional = skipOffstage ? ' (ignoring offstage widgets)' : '';
final List<Element> widgets = evaluate().toList(); final List<Element> widgets = evaluate().toList();
final int count = widgets.length; final int count = widgets.length;
if (count == 0) if (count == 0)
return 'zero widgets with $description'; return 'zero widgets with $description$additional';
if (count == 1) if (count == 1)
return 'exactly one widget with $description: ${widgets.single}'; return 'exactly one widget with $description$additional: ${widgets.single}';
if (count < 4) if (count < 4)
return '$count widgets with $description: $widgets'; return '$count widgets with $description$additional: $widgets';
return '$count widgets with $description: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...'; return '$count widgets with $description$additional: ${widgets[0]}, ${widgets[1]}, ${widgets[2]}, ...';
} }
} }
/// Searches a widget tree and returns nodes that match a particular /// Searches a widget tree and returns nodes that match a particular
/// pattern. /// pattern.
abstract class MatchFinder extends Finder { abstract class MatchFinder extends Finder {
/// Initialises a predicate-based Finder. Used by subclasses to initialize the
/// [skipOffstage] property.
MatchFinder({ bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
/// Returns true if the given element matches the pattern. /// Returns true if the given element matches the pattern.
/// ///
/// When implementing your own MatchFinder, this is the main method to override. /// When implementing your own MatchFinder, this is the main method to override.
...@@ -198,7 +239,7 @@ abstract class MatchFinder extends Finder { ...@@ -198,7 +239,7 @@ abstract class MatchFinder extends Finder {
} }
class _TextFinder extends MatchFinder { class _TextFinder extends MatchFinder {
_TextFinder(this.text); _TextFinder(this.text, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final String text; final String text;
...@@ -215,7 +256,7 @@ class _TextFinder extends MatchFinder { ...@@ -215,7 +256,7 @@ class _TextFinder extends MatchFinder {
} }
class _WidgetWithTextFinder extends Finder { class _WidgetWithTextFinder extends Finder {
_WidgetWithTextFinder(this.widgetType, this.text); _WidgetWithTextFinder(this.widgetType, this.text, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Type widgetType; final Type widgetType;
final String text; final String text;
...@@ -249,7 +290,7 @@ class _WidgetWithTextFinder extends Finder { ...@@ -249,7 +290,7 @@ class _WidgetWithTextFinder extends Finder {
} }
class _KeyFinder extends MatchFinder { class _KeyFinder extends MatchFinder {
_KeyFinder(this.key); _KeyFinder(this.key, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Key key; final Key key;
...@@ -263,7 +304,7 @@ class _KeyFinder extends MatchFinder { ...@@ -263,7 +304,7 @@ class _KeyFinder extends MatchFinder {
} }
class _WidgetTypeFinder extends MatchFinder { class _WidgetTypeFinder extends MatchFinder {
_WidgetTypeFinder(this.widgetType); _WidgetTypeFinder(this.widgetType, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Type widgetType; final Type widgetType;
...@@ -277,7 +318,7 @@ class _WidgetTypeFinder extends MatchFinder { ...@@ -277,7 +318,7 @@ class _WidgetTypeFinder extends MatchFinder {
} }
class _ElementTypeFinder extends MatchFinder { class _ElementTypeFinder extends MatchFinder {
_ElementTypeFinder(this.elementType); _ElementTypeFinder(this.elementType, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Type elementType; final Type elementType;
...@@ -291,7 +332,7 @@ class _ElementTypeFinder extends MatchFinder { ...@@ -291,7 +332,7 @@ class _ElementTypeFinder extends MatchFinder {
} }
class _ConfigFinder extends MatchFinder { class _ConfigFinder extends MatchFinder {
_ConfigFinder(this.config); _ConfigFinder(this.config, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final Widget config; final Widget config;
...@@ -305,7 +346,7 @@ class _ConfigFinder extends MatchFinder { ...@@ -305,7 +346,7 @@ class _ConfigFinder extends MatchFinder {
} }
class _WidgetPredicateFinder extends MatchFinder { class _WidgetPredicateFinder extends MatchFinder {
_WidgetPredicateFinder(this.predicate); _WidgetPredicateFinder(this.predicate, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final WidgetPredicate predicate; final WidgetPredicate predicate;
...@@ -319,7 +360,7 @@ class _WidgetPredicateFinder extends MatchFinder { ...@@ -319,7 +360,7 @@ class _WidgetPredicateFinder extends MatchFinder {
} }
class _ElementPredicateFinder extends MatchFinder { class _ElementPredicateFinder extends MatchFinder {
_ElementPredicateFinder(this.predicate); _ElementPredicateFinder(this.predicate, { bool skipOffstage: true }) : super(skipOffstage: skipOffstage);
final ElementPredicate predicate; final ElementPredicate predicate;
......
...@@ -36,12 +36,19 @@ const Matcher findsOneWidget = const _FindsWidgetMatcher(1, 1); ...@@ -36,12 +36,19 @@ const Matcher findsOneWidget = const _FindsWidgetMatcher(1, 1);
Matcher findsNWidgets(int n) => new _FindsWidgetMatcher(n, n); Matcher findsNWidgets(int n) => new _FindsWidgetMatcher(n, n);
/// Asserts that the [Finder] locates the a single widget that has at /// Asserts that the [Finder] locates the a single widget that has at
/// least one [OffStage] widget ancestor. /// least one [Offstage] widget ancestor.
const Matcher isOffStage = const _IsOffStage(); ///
/// It's important to use a full finder, since by default finders exclude
/// offstage widgets.
///
/// Example:
///
/// expect(find.text('Save', skipOffstage: false), isOffstage);
const Matcher isOffstage = const _IsOffstage();
/// Asserts that the [Finder] locates the a single widget that has no /// Asserts that the [Finder] locates the a single widget that has no
/// [OffStage] widget ancestors. /// [Offstage] widget ancestors.
const Matcher isOnStage = const _IsOnStage(); const Matcher isOnstage = const _IsOnstage();
/// Asserts that the [Finder] locates the a single widget that has at /// Asserts that the [Finder] locates the a single widget that has at
/// least one [Card] widget ancestor. /// least one [Card] widget ancestor.
...@@ -54,7 +61,8 @@ const Matcher isNotInCard = const _IsNotInCard(); ...@@ -54,7 +61,8 @@ const Matcher isNotInCard = const _IsNotInCard();
/// Asserts that an object's toString() is a plausible one-line description. /// Asserts that an object's toString() is a plausible one-line description.
/// ///
/// Specifically, this matcher checks that the string does not contains newline /// Specifically, this matcher checks that the string does not contains newline
/// characters and does not have leading or trailing whitespace. /// characters, and does not have leading or trailing whitespace, and is not
/// empty.
const Matcher hasOneLineDescription = const _HasOneLineDescription(); const Matcher hasOneLineDescription = const _HasOneLineDescription();
class _FindsWidgetMatcher extends Matcher { class _FindsWidgetMatcher extends Matcher {
...@@ -66,18 +74,17 @@ class _FindsWidgetMatcher extends Matcher { ...@@ -66,18 +74,17 @@ class _FindsWidgetMatcher extends Matcher {
@override @override
bool matches(Finder finder, Map<dynamic, dynamic> matchState) { bool matches(Finder finder, Map<dynamic, dynamic> matchState) {
assert(min != null || max != null); assert(min != null || max != null);
assert(min == null || max == null || min <= max);
matchState[Finder] = finder; matchState[Finder] = finder;
int count = 0;
Iterator<Element> iterator = finder.evaluate().iterator;
if (min != null) { if (min != null) {
int count = 0;
Iterator<Element> iterator = finder.evaluate().iterator;
while (count < min && iterator.moveNext()) while (count < min && iterator.moveNext())
count += 1; count += 1;
if (count < min) if (count < min)
return false; return false;
} }
if (max != null) { if (max != null) {
int count = 0;
Iterator<Element> iterator = finder.evaluate().iterator;
while (count <= max && iterator.moveNext()) while (count <= max && iterator.moveNext())
count += 1; count += 1;
if (count > max) if (count > max)
...@@ -137,9 +144,11 @@ class _FindsWidgetMatcher extends Matcher { ...@@ -137,9 +144,11 @@ class _FindsWidgetMatcher extends Matcher {
} }
bool _hasAncestorMatching(Finder finder, bool predicate(Widget widget)) { bool _hasAncestorMatching(Finder finder, bool predicate(Widget widget)) {
expect(finder, findsOneWidget); Iterable<Element> nodes = finder.evaluate();
if (nodes.length != 1)
return false;
bool result = false; bool result = false;
finder.evaluate().single.visitAncestorElements((Element ancestor) { nodes.single.visitAncestorElements((Element ancestor) {
if (predicate(ancestor.widget)) { if (predicate(ancestor.widget)) {
result = true; result = true;
return false; return false;
...@@ -153,16 +162,15 @@ bool _hasAncestorOfType(Finder finder, Type targetType) { ...@@ -153,16 +162,15 @@ bool _hasAncestorOfType(Finder finder, Type targetType) {
return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType); return _hasAncestorMatching(finder, (Widget widget) => widget.runtimeType == targetType);
} }
class _IsOffStage extends Matcher { class _IsOffstage extends Matcher {
const _IsOffStage(); const _IsOffstage();
@override @override
bool matches(Finder finder, Map<dynamic, dynamic> matchState) { bool matches(Finder finder, Map<dynamic, dynamic> matchState) {
return _hasAncestorMatching(finder, (Widget widget) { return _hasAncestorMatching(finder, (Widget widget) {
if (widget.runtimeType != OffStage) if (widget is Offstage)
return false; return widget.offstage;
OffStage offstage = widget; return false;
return offstage.offstage;
}); });
} }
...@@ -170,18 +178,19 @@ class _IsOffStage extends Matcher { ...@@ -170,18 +178,19 @@ class _IsOffStage extends Matcher {
Description describe(Description description) => description.add('offstage'); Description describe(Description description) => description.add('offstage');
} }
class _IsOnStage extends Matcher { class _IsOnstage extends Matcher {
const _IsOnStage(); const _IsOnstage();
@override @override
bool matches(Finder finder, Map<dynamic, dynamic> matchState) { bool matches(Finder finder, Map<dynamic, dynamic> matchState) {
expect(finder, findsOneWidget); Iterable<Element> nodes = finder.evaluate();
if (nodes.length != 1)
return false;
bool result = true; bool result = true;
finder.evaluate().single.visitAncestorElements((Element ancestor) { nodes.single.visitAncestorElements((Element ancestor) {
Widget widget = ancestor.widget; Widget widget = ancestor.widget;
if (widget.runtimeType == OffStage) { if (widget is Offstage) {
OffStage offstage = widget; result = !widget.offstage;
result = !offstage.offstage;
return false; return false;
} }
return true; return true;
...@@ -219,9 +228,9 @@ class _HasOneLineDescription extends Matcher { ...@@ -219,9 +228,9 @@ class _HasOneLineDescription extends Matcher {
@override @override
bool matches(Object object, Map<dynamic, dynamic> matchState) { bool matches(Object object, Map<dynamic, dynamic> matchState) {
String description = object.toString(); String description = object.toString();
return description.isNotEmpty && return description.isNotEmpty
!description.contains('\n') && && !description.contains('\n')
description.trim() == description; && description.trim() == description;
} }
@override @override
......
...@@ -204,7 +204,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher { ...@@ -204,7 +204,7 @@ class WidgetTester extends WidgetController implements HitTestDispatcher {
)?.target; )?.target;
if (innerTarget == null) if (innerTarget == null)
return null; return null;
final Element innerTargetElement = collectAllElementsFrom(binding.renderViewElement) final Element innerTargetElement = collectAllElementsFrom(binding.renderViewElement, skipOffstage: true)
.lastWhere((Element element) => element.renderObject == innerTarget); .lastWhere((Element element) => element.renderObject == innerTarget);
final List<Element> candidates = <Element>[]; final List<Element> candidates = <Element>[];
innerTargetElement.visitAncestorElements((Element element) { innerTargetElement.visitAncestorElements((Element element) {
......
...@@ -15,7 +15,7 @@ void main() { ...@@ -15,7 +15,7 @@ void main() {
testWidgets('fails with a descriptive message', (WidgetTester tester) async { testWidgets('fails with a descriptive message', (WidgetTester tester) async {
TestFailure failure; TestFailure failure;
try { try {
expect(find.text('foo'), findsOneWidget); expect(find.text('foo', skipOffstage: false), findsOneWidget);
} catch(e) { } catch(e) {
failure = e; failure = e;
} }
...@@ -38,7 +38,7 @@ void main() { ...@@ -38,7 +38,7 @@ void main() {
TestFailure failure; TestFailure failure;
try { try {
expect(find.text('foo'), findsNothing); expect(find.text('foo', skipOffstage: false), findsNothing);
} catch(e) { } catch(e) {
failure = e; failure = e;
} }
...@@ -50,6 +50,24 @@ void main() { ...@@ -50,6 +50,24 @@ void main() {
expect(message, contains('Actual: ?:<exactly one widget with text "foo": Text("foo")>\n')); expect(message, contains('Actual: ?:<exactly one widget with text "foo": Text("foo")>\n'));
expect(message, contains('Which: means one was found but none were expected\n')); expect(message, contains('Which: means one was found but none were expected\n'));
}); });
testWidgets('fails with a descriptive message when skipping', (WidgetTester tester) async {
await tester.pumpWidget(new Text('foo'));
TestFailure failure;
try {
expect(find.text('foo'), findsNothing);
} catch(e) {
failure = e;
}
expect(failure, isNotNull);
String message = failure.message;
expect(message, contains('Expected: no matching nodes in the widget tree\n'));
expect(message, contains('Actual: ?:<exactly one widget with text "foo" (ignoring offstage widgets): Text("foo")>\n'));
expect(message, contains('Which: means one was found but none were expected\n'));
});
}); });
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment