
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.
235 lines
8.7 KiB
Dart
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;
|
|
}
|
|
}
|