flutter/packages/flutter_test/lib/src/instrumentation.dart
Ian Hickson c167efca17 Minor widget_tester refactoring and docs (#3472)
This reorders some classes so that this file makes more sense, and adds
a bunch of docs. It also makes the following changes:

* Move allElements from Instrumentation to TestWidgets. (Instrumentation
  is going away.)

* Remove findElements.

* Rename byElement to byElementPredicate

* Rename byPredicate to byWidgetPredicate

* Implement _WidgetPredicateFinder so that byWidgetPredicate has good
  messages

* Fix one use of byElementPredicate to use byWidgetPredicate.
2016-04-21 16:35:46 -07:00

235 lines
8.7 KiB
Dart

// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'test_pointer.dart';
typedef Point SizeToPointFunction(Size size);
/// Helper class for flutter tests providing event dispatch.
///
/// This class provides hooks for accessing the rendering tree and dispatching
/// fake tap/drag/etc. events.
class Instrumentation {
Instrumentation({ Widgeteer binding })
: this.binding = binding ?? WidgetFlutterBinding.ensureInitialized();
final Widgeteer binding;
/// Returns a list of all the [Layer] objects in the rendering.
List<Layer> get layers => _layers(binding.renderView.layer);
// TODO(ianh): This should not be O(N) hidden behind a getter!
List<Layer> _layers(Layer layer) {
List<Layer> result = <Layer>[layer];
if (layer is ContainerLayer) {
ContainerLayer root = layer;
Layer child = root.firstChild;
while (child != null) {
result.addAll(_layers(child));
child = child.nextSibling;
}
}
return result;
}
/// Walks all the elements in the tree, in depth-first pre-order,
/// calling the given function for each one.
void walkElements(ElementVisitor visitor) {
void walk(Element element) {
visitor(element);
element.visitChildren(walk);
}
binding.renderViewElement.visitChildren(walk);
}
/// Returns the first element that for which the given predicate
/// function returns true, if any, or null if the predicate function
/// never returns true.
Element findElement(bool predicate(Element element)) {
try {
walkElements((Element element) {
if (predicate(element))
throw element;
});
} on Element catch (e) {
return e;
}
return null;
}
/// Returns the first element that corresponds to a widget with the
/// given [Key], or null if there is no such element.
Element findElementByKey(Key key) {
return findElement((Element element) => element.widget.key == key);
}
/// Returns the first element that corresponds to a [Text] widget
/// whose data is the given string, or null if there is no such
/// element.
Element findText(String text) {
return findElement((Element element) {
if (element.widget is! Text)
return false;
Text textWidget = element.widget;
return textWidget.data == text;
});
}
/// Returns the first [Widget] of the given [runtimeType], if any. Returns
/// null if there is no matching widget.
Widget findWidgetOfType(Type type) {
Element element = findElement((Element element) {
return element.widget.runtimeType == type;
});
return element?.widget;
}
/// Returns the [State] object of the first element whose state has
/// the given [runtimeType], if any. Returns null if there is no
/// matching element.
State findStateOfType(Type type) {
StatefulElement element = findElement((Element element) {
return element is StatefulElement && element.state.runtimeType == type;
});
return element?.state;
}
/// Returns the [State] object of the first element whose
/// configuration is the given widget, if any. Returns null if the
/// given configuration is not that of a stateful widget or if there
/// is no matching element.
State findStateByConfig(Widget config) {
StatefulElement element = findElement((Element element) {
return element is StatefulElement && element.state.config == config;
});
return element?.state;
}
/// Returns the point at the center of the given element.
Point getCenter(Element element) {
return _getElementPoint(element, (Size size) => size.center(Point.origin));
}
/// Returns the point at the top left of the given element.
Point getTopLeft(Element element) {
return _getElementPoint(element, (_) => Point.origin);
}
/// Returns the point at the top right of the given element. This
/// point is not inside the object's hit test area.
Point getTopRight(Element element) {
return _getElementPoint(element, (Size size) => size.topRight(Point.origin));
}
/// Returns the point at the bottom left of the given element. This
/// point is not inside the object's hit test area.
Point getBottomLeft(Element element) {
return _getElementPoint(element, (Size size) => size.bottomLeft(Point.origin));
}
/// Returns the point at the bottom right of the given element. This
/// point is not inside the object's hit test area.
Point getBottomRight(Element element) {
return _getElementPoint(element, (Size size) => size.bottomRight(Point.origin));
}
/// Returns the size of the given element. This is only valid once
/// the element's render object has been laid out at least once.
Size getSize(Element element) {
assert(element != null);
RenderBox box = element.renderObject;
assert(box != null);
return box.size;
}
Point _getElementPoint(Element element, SizeToPointFunction sizeToPoint) {
assert(element != null);
RenderBox box = element.renderObject;
assert(box != null);
return box.localToGlobal(sizeToPoint(box.size));
}
/// Dispatch a pointer down / pointer up sequence at the center of
/// the given element, assuming it is exposed. If the center of the
/// element is not exposed, this might send events to another
/// object.
void tap(Element element, { int pointer: 1 }) {
tapAt(getCenter(element), pointer: pointer);
}
/// Dispatch a pointer down / pointer up sequence at the given
/// location.
void tapAt(Point location, { int pointer: 1 }) {
HitTestResult result = _hitTest(location);
TestPointer p = new TestPointer(pointer);
binding.dispatchEvent(p.down(location), result);
binding.dispatchEvent(p.up(), result);
}
/// Attempts a fling gesture starting from the center of the given
/// element, moving the given distance, reaching the given velocity.
///
/// If the middle of the element is not exposed, this might send
/// events to another object.
void fling(Element element, Offset offset, double velocity, { int pointer: 1 }) {
flingFrom(getCenter(element), offset, velocity, pointer: pointer);
}
/// Attempts a fling gesture starting from the given location,
/// moving the given distance, reaching the given velocity.
void flingFrom(Point startLocation, Offset offset, double velocity, { int pointer: 1 }) {
assert(offset.distance > 0.0);
assert(velocity != 0.0); // velocity is pixels/second
final TestPointer p = new TestPointer(pointer);
final HitTestResult result = _hitTest(startLocation);
const int kMoveCount = 50; // Needs to be >= kHistorySize, see _LeastSquaresVelocityTrackerStrategy
final double timeStampDelta = 1000.0 * offset.distance / (kMoveCount * velocity);
double timeStamp = 0.0;
binding.dispatchEvent(p.down(startLocation, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
for(int i = 0; i <= kMoveCount; i++) {
final Point location = startLocation + Offset.lerp(Offset.zero, offset, i / kMoveCount);
binding.dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
timeStamp += timeStampDelta;
}
binding.dispatchEvent(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
}
/// Attempts to drag the given element by the given offset, by
/// starting a drag in the middle of the element.
///
/// If the middle of the element is not exposed, this might send
/// events to another object.
void scroll(Element element, Offset offset, { int pointer: 1 }) {
scrollAt(getCenter(element), offset, pointer: pointer);
}
/// Attempts a drag gesture consisting of a pointer down, a move by
/// the given offset, and a pointer up.
void scrollAt(Point startLocation, Offset offset, { int pointer: 1 }) {
Point endLocation = startLocation + offset;
TestPointer p = new TestPointer(pointer);
// Events for the entire press-drag-release gesture are dispatched
// to the widgets "hit" by the pointer down event.
HitTestResult result = _hitTest(startLocation);
binding.dispatchEvent(p.down(startLocation), result);
binding.dispatchEvent(p.move(endLocation), result);
binding.dispatchEvent(p.up(), result);
}
/// Begins a gesture at a particular point, and returns the
/// [TestGesture] object which you can use to continue the gesture.
TestGesture startGesture(Point downLocation, { int pointer: 1 }) {
return new TestGesture(downLocation, pointer: pointer);
}
HitTestResult _hitTest(Point location) {
HitTestResult result = new HitTestResult();
binding.hitTest(result, location);
return result;
}
}