blob: f41554508f01ec041aaaa0e413288e1d76df1e50 [file] [log] [blame]
// 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({ WidgetFlutterBinding binding })
: this.binding = binding ?? WidgetFlutterBinding.ensureInitialized();
final WidgetFlutterBinding 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) {
StatefulComponentElement element = findElement((Element element) {
return element is StatefulComponentElement && 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) {
StatefulComponentElement element = findElement((Element element) {
return element is StatefulComponentElement && 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);
dispatchEvent(p.down(location), result);
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;
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);
dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
timeStamp += timeStampDelta;
}
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);
dispatchEvent(p.down(startLocation), result);
dispatchEvent(p.move(endLocation), result);
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 }) {
TestPointer p = new TestPointer(pointer);
HitTestResult result = _hitTest(downLocation);
dispatchEvent(p.down(downLocation), result);
return new TestGesture._(this, result, p);
}
HitTestResult _hitTest(Point location) {
HitTestResult result = new HitTestResult();
binding.hitTest(result, location);
return result;
}
/// Sends a [PointerEvent] at a particular [HitTestResult].
///
/// Generally speaking, it is preferred to use one of the more
/// semantically meaningful ways to dispatch events in tests, in
/// particular: [tap], [tapAt], [fling], [flingFrom], [scroll],
/// [scrollAt], or [startGesture].
void dispatchEvent(PointerEvent event, HitTestResult result) {
binding.dispatchEvent(event, result);
}
}
/// A class for performing gestures in tests. To create a
/// [TestGesture], call [WidgetTester.startGesture].
class TestGesture {
TestGesture._(this._target, this._result, this.pointer);
final Instrumentation _target;
final HitTestResult _result;
final TestPointer pointer;
bool _isDown = true;
/// Send a move event moving the pointer to the given location.
void moveTo(Point location) {
assert(_isDown);
_target.dispatchEvent(pointer.move(location), _result);
}
/// Send a move event moving the pointer by the given offset.
void moveBy(Offset offset) {
assert(_isDown);
moveTo(pointer.location + offset);
}
/// End the gesture by releasing the pointer.
void up() {
assert(_isDown);
_isDown = false;
_target.dispatchEvent(pointer.up(), _result);
}
/// End the gesture by canceling the pointer (as would happen if the
/// system showed a modal dialog on top of the Flutter application,
/// for instance).
void cancel() {
assert(_isDown);
_isDown = false;
_target.dispatchEvent(pointer.cancel(), _result);
}
}