Commit b7af64ee authored by Ian Hickson's avatar Ian Hickson

Refactor TestGesture (#3461)

Moves TestGesture into test_pointer.dart and makes it more
self-contained.

This is part of a general refactoring of flutter_test.

Depends on https://github.com/flutter/flutter/pull/3459
parent 3142aba4
......@@ -214,14 +214,14 @@ class FlutterDriverExtension {
HitTestResult hitTest = new HitTestResult();
prober.binding.hitTest(hitTest, startLocation);
prober.dispatchEvent(pointer.down(startLocation), hitTest);
prober.binding.dispatchEvent(pointer.down(startLocation), hitTest);
await new Future<Null>.value(); // so that down and move don't happen in the same microtask
for (int moves = 0; moves < totalMoves; moves++) {
currentLocation = currentLocation + delta;
prober.dispatchEvent(pointer.move(currentLocation), hitTest);
prober.binding.dispatchEvent(pointer.move(currentLocation), hitTest);
await new Future<Null>.delayed(pause);
}
prober.dispatchEvent(pointer.up(), hitTest);
prober.binding.dispatchEvent(pointer.up(), hitTest);
return new ScrollResult();
}
......
......@@ -24,13 +24,16 @@ enum EnginePhase {
}
class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding {
_SteppedWidgetFlutterBinding._(this.async);
final FakeAsync async;
/// Creates and initializes the binding. This constructor is
/// idempotent; calling it a second time will just return the
/// previously-created instance.
static WidgetFlutterBinding ensureInitialized() {
static WidgetFlutterBinding ensureInitialized(FakeAsync async) {
if (WidgetFlutterBinding.instance == null)
new _SteppedWidgetFlutterBinding();
new _SteppedWidgetFlutterBinding._(async);
return WidgetFlutterBinding.instance;
}
......@@ -66,6 +69,12 @@ class _SteppedWidgetFlutterBinding extends WidgetFlutterBinding {
SemanticsNode.sendSemanticsTree();
}
}
@override
void dispatchEvent(PointerEvent event, HitTestResult result) {
super.dispatchEvent(event, result);
async.flushMicrotasks();
}
}
/// Helper class for flutter tests providing fake async.
......@@ -77,7 +86,7 @@ class ElementTreeTester extends Instrumentation {
ElementTreeTester._(FakeAsync async)
: async = async,
clock = async.getClock(new DateTime.utc(2015, 1, 1)),
super(binding: _SteppedWidgetFlutterBinding.ensureInitialized()) {
super(binding: _SteppedWidgetFlutterBinding.ensureInitialized(async)) {
timeDilation = 1.0;
ui.window.onBeginFrame = null;
debugPrint = _synchronousDebugPrint;
......@@ -138,12 +147,6 @@ class ElementTreeTester extends Instrumentation {
async.flushMicrotasks();
}
@override
void dispatchEvent(PointerEvent event, HitTestResult result) {
super.dispatchEvent(event, result);
async.flushMicrotasks();
}
/// Returns the exception most recently caught by the Flutter framework.
///
/// Call this if you expect an exception during a test. If an exception is
......
......@@ -182,8 +182,8 @@ class Instrumentation {
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);
binding.dispatchEvent(p.down(location), result);
binding.dispatchEvent(p.up(), result);
}
/// Attempts a fling gesture starting from the center of the given
......@@ -205,13 +205,13 @@ class Instrumentation {
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);
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);
dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
binding.dispatchEvent(p.move(location, timeStamp: new Duration(milliseconds: timeStamp.round())), result);
timeStamp += timeStampDelta;
}
dispatchEvent(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
binding.dispatchEvent(p.up(timeStamp: new Duration(milliseconds: timeStamp.round())), result);
}
/// Attempts to drag the given element by the given offset, by
......@@ -231,18 +231,15 @@ class Instrumentation {
// 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);
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 }) {
TestPointer p = new TestPointer(pointer);
HitTestResult result = _hitTest(downLocation);
dispatchEvent(p.down(downLocation), result);
return new TestGesture._(this, result, p);
return new TestGesture(downLocation, pointer: pointer);
}
HitTestResult _hitTest(Point location) {
......@@ -250,55 +247,6 @@ class Instrumentation {
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);
}
}
class _DepthFirstChildIterable extends IterableBase<Element> {
......
......@@ -6,28 +6,59 @@ import 'package:flutter/gestures.dart';
export 'dart:ui' show Point;
/// A class for generating coherent artificial pointer events.
///
/// You can use this to manually simulate individual events, but the
/// simplest way to generate coherent gestures is to use [TestGesture].
class TestPointer {
/// Creates a [TestPointer]. By default, the pointer identifier used is 1, however
/// this can be overridden by providing an argument to the constructor.
TestPointer([ this.pointer = 1 ]);
int pointer;
bool isDown = false;
Point location;
/// The pointer identifier used for events generated by this object.
///
/// Set when the object is constructed. Defaults to 1.
final int pointer;
PointerDownEvent down(Point newLocation, { Duration timeStamp: const Duration() }) {
/// Whether the pointer simulated by this object is currently down.
///
/// A pointer is released (goes up) by calling [up] or [cancel].
///
/// Once a pointer is released, it can no longer generate events.
bool get isDown => _isDown;
bool _isDown = false;
/// The position of the last event sent by this object.
///
/// If no event has ever been sent by this object, returns null.
Point get location => _location;
Point _location;
/// Create a [PointerDownEvent] at the given location.
///
/// By default, the time stamp on the event is [Duration.ZERO]. You
/// can give a specific time stamp by passing the `timeStamp`
/// argument.
PointerDownEvent down(Point newLocation, { Duration timeStamp: Duration.ZERO }) {
assert(!isDown);
isDown = true;
location = newLocation;
return new PointerDownEvent(
_isDown = true;
_location = newLocation;
return new PointerDownEvent(
timeStamp: timeStamp,
pointer: pointer,
position: location
);
}
PointerMoveEvent move(Point newLocation, { Duration timeStamp: const Duration() }) {
/// Create a [PointerMoveEvent] to the given location.
///
/// By default, the time stamp on the event is [Duration.ZERO]. You
/// can give a specific time stamp by passing the `timeStamp`
/// argument.
PointerMoveEvent move(Point newLocation, { Duration timeStamp: Duration.ZERO }) {
assert(isDown);
Offset delta = newLocation - location;
location = newLocation;
_location = newLocation;
return new PointerMoveEvent(
timeStamp: timeStamp,
pointer: pointer,
......@@ -36,9 +67,16 @@ class TestPointer {
);
}
PointerUpEvent up({ Duration timeStamp: const Duration() }) {
/// Create a [PointerUpEvent].
///
/// By default, the time stamp on the event is [Duration.ZERO]. You
/// can give a specific time stamp by passing the `timeStamp`
/// argument.
///
/// The object is no longer usable after this method has been called.
PointerUpEvent up({ Duration timeStamp: Duration.ZERO }) {
assert(isDown);
isDown = false;
_isDown = false;
return new PointerUpEvent(
timeStamp: timeStamp,
pointer: pointer,
......@@ -46,14 +84,96 @@ class TestPointer {
);
}
PointerCancelEvent cancel({ Duration timeStamp: const Duration() }) {
/// Create a [PointerCancelEvent].
///
/// By default, the time stamp on the event is [Duration.ZERO]. You
/// can give a specific time stamp by passing the `timeStamp`
/// argument.
///
/// The object is no longer usable after this method has been called.
PointerCancelEvent cancel({ Duration timeStamp: Duration.ZERO }) {
assert(isDown);
isDown = false;
_isDown = false;
return new PointerCancelEvent(
timeStamp: timeStamp,
pointer: pointer,
position: location
);
}
}
/// A class for performing gestures in tests.
///
/// The simplest way to create a [TestGesture] is to call
/// [WidgetTester.startGesture].
class TestGesture {
/// Create a [TestGesture] by starting with a pointerDown at the
/// given point.
///
/// By default, the pointer ID used is 1. This can be overridden by
/// providing the `pointer` argument.
///
/// By default, the global binding is used both for hit testing and
/// for dispatching of events. The object to use for hit testing can
/// be overridden by providing `hitTestTarget`, and the object to
/// use for dispatching events can be overridden by providing an
/// `dispatcher`.
factory TestGesture(Point downLocation, {
int pointer: 1,
HitTestable target,
HitTestDispatcher dispatcher
}) {
// hit test
final HitTestResult result = new HitTestResult();
target ??= Gesturer.instance;
assert(target != null);
target.hitTest(result, downLocation);
// dispatch down event
final TestPointer testPointer = new TestPointer(pointer);
dispatcher ??= Gesturer.instance;
assert(dispatcher != null);
dispatcher.dispatchEvent(testPointer.down(downLocation), result);
// create a TestGesture
return new TestGesture._(dispatcher, result, testPointer);
}
const TestGesture._(this._dispatcher, this._result, this._pointer);
final HitTestDispatcher _dispatcher;
final HitTestResult _result;
final TestPointer _pointer;
/// Send a move event moving the pointer by the given offset.
void moveBy(Offset offset) {
assert(_pointer._isDown);
moveTo(_pointer.location + offset);
}
/// Send a move event moving the pointer to the given location.
void moveTo(Point location) {
assert(_pointer._isDown);
_dispatcher.dispatchEvent(_pointer.move(location), _result);
}
/// End the gesture by releasing the pointer.
///
/// The object is no longer usable after this method has been called.
void up() {
assert(_pointer._isDown);
_dispatcher.dispatchEvent(_pointer.up(), _result);
assert(!_pointer._isDown);
}
/// 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).
///
/// The object is no longer usable after this method has been called.
void cancel() {
assert(_pointer._isDown);
_dispatcher.dispatchEvent(_pointer.cancel(), _result);
assert(!_pointer._isDown);
}
}
......@@ -8,7 +8,7 @@ import 'package:quiver/testing/async.dart';
import 'package:test/test.dart';
import 'element_tree_tester.dart';
import 'instrumentation.dart';
import 'test_pointer.dart';
/// Runs the [callback] inside the Flutter test environment.
///
......@@ -81,10 +81,8 @@ class WidgetTester {
}
/// Sends an [event] at [result] location.
///
/// See [ElementTreeTester.dispatchEvent] for details.
void dispatchEvent(PointerEvent event, HitTestResult result) {
elementTreeTester.dispatchEvent(event, result);
binding.dispatchEvent(event, result);
}
/// Returns the exception most recently caught by the Flutter framework.
......
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