blob: 19c9ce68ecd2fe1f589e76a863a97840530c251f [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 'dart:collection';
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 all elements ordered in a depth-first traversal fashion.
///
/// The returned iterable is lazy. It does not walk the entire element tree
/// immediately, but rather a chunk at a time as the iteration progresses
/// using [Iterator.moveNext].
Iterable<Element> get allElements {
return new _DepthFirstChildIterable(binding.renderViewElement);
}
/// Returns all elements that satisfy [predicate].
Iterable<Element> findElements(bool predicate(Element element)) {
return allElements.where(predicate);
}
/// 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;
}
}
class _DepthFirstChildIterable extends IterableBase<Element> {
_DepthFirstChildIterable(this.rootElement);
Element rootElement;
@override
Iterator<Element> get iterator => new _DepthFirstChildIterator(rootElement);
}
class _DepthFirstChildIterator implements Iterator<Element> {
_DepthFirstChildIterator(Element rootElement)
: _stack = _reverseChildrenOf(rootElement).toList();
Element _current;
final List<Element> _stack;
@override
Element get current => _current;
@override
bool moveNext() {
if (_stack.isEmpty)
return false;
_current = _stack.removeLast();
// Stack children in reverse order to traverse first branch first
_stack.addAll(_reverseChildrenOf(_current));
return true;
}
static Iterable<Element> _reverseChildrenOf(Element element) {
List<Element> children = <Element>[];
element.visitChildren(children.add);
return children.reversed;
}
}