blob: 77bd8bdd17f2f397aa98afbbfecc665b4dddbe7e [file] [log] [blame]
// Copyright 2014 The Flutter 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/foundation.dart';
import 'package:flutter/gestures.dart';
import 'test_async_utils.dart';
export 'dart:ui' show Offset;
/// 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.
///
/// Multiple [TestPointer]s created with the same pointer identifier will
/// interfere with each other if they are used in parallel.
TestPointer([
this.pointer = 1,
this.kind = PointerDeviceKind.touch,
int? device,
int buttons = kPrimaryButton,
]) : _buttons = buttons {
switch (kind) {
case PointerDeviceKind.mouse:
_device = device ?? 1;
break;
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.unknown:
_device = device ?? 0;
break;
}
}
/// The device identifier used for events generated by this object.
///
/// Set when the object is constructed. Defaults to 1 if the [kind] is
/// [PointerDeviceKind.mouse], and 0 otherwise.
int get device => _device;
late int _device;
/// The pointer identifier used for events generated by this object.
///
/// Set when the object is constructed. Defaults to 1.
final int pointer;
/// The kind of pointing device to simulate. Defaults to
/// [PointerDeviceKind.touch].
final PointerDeviceKind kind;
/// The kind of buttons to simulate on Down and Move events. Defaults to
/// [kPrimaryButton].
int get buttons => _buttons;
int _buttons;
/// 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.
Offset? get location => _location;
Offset? _location;
/// If a custom event is created outside of this class, this function is used
/// to set the [isDown].
bool setDownInfo(
PointerEvent event,
Offset newLocation, {
int? buttons,
}) {
_location = newLocation;
if (buttons != null)
_buttons = buttons;
switch (event.runtimeType) {
case PointerDownEvent:
assert(!isDown);
_isDown = true;
break;
case PointerUpEvent:
case PointerCancelEvent:
assert(isDown);
_isDown = false;
break;
default:
break;
}
return isDown;
}
/// 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.
///
/// By default, the set of buttons in the last down or move event is used.
/// You can give a specific set of buttons by passing the `buttons` argument.
PointerDownEvent down(
Offset newLocation, {
Duration timeStamp = Duration.zero,
int? buttons,
}) {
assert(!isDown);
_isDown = true;
_location = newLocation;
if (buttons != null)
_buttons = buttons;
return PointerDownEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
pointer: pointer,
position: location!,
buttons: _buttons,
);
}
/// 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.
///
/// [isDown] must be true when this is called, since move events can only
/// be generated when the pointer is down.
///
/// By default, the set of buttons in the last down or move event is used.
/// You can give a specific set of buttons by passing the `buttons` argument.
PointerMoveEvent move(
Offset newLocation, {
Duration timeStamp = Duration.zero,
int? buttons,
}) {
assert(
isDown,
'Move events can only be generated when the pointer is down. To '
'create a movement event simulating a pointer move when the pointer is '
'up, use hover() instead.');
final Offset delta = newLocation - location!;
_location = newLocation;
if (buttons != null)
_buttons = buttons;
return PointerMoveEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
pointer: pointer,
position: newLocation,
delta: delta,
buttons: _buttons,
);
}
/// 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;
return PointerUpEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
pointer: pointer,
position: location!,
);
}
/// 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;
return PointerCancelEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
pointer: pointer,
position: location!,
);
}
/// Create a [PointerAddedEvent] with the [PointerDeviceKind] the pointer was
/// created with.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerAddedEvent addPointer({
Duration timeStamp = Duration.zero,
Offset? location,
}) {
_location = location ?? _location;
return PointerAddedEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
position: _location ?? Offset.zero,
);
}
/// Create a [PointerRemovedEvent] with the [PointerDeviceKind] the pointer
/// was created with.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerRemovedEvent removePointer({
Duration timeStamp = Duration.zero,
Offset? location,
}) {
_location = location ?? _location;
return PointerRemovedEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
position: _location ?? Offset.zero,
);
}
/// Create a [PointerHoverEvent] 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.
///
/// [isDown] must be false, since hover events can't be sent when the pointer
/// is up.
PointerHoverEvent hover(
Offset newLocation, {
Duration timeStamp = Duration.zero,
}) {
assert(
!isDown,
'Hover events can only be generated when the pointer is up. To '
'simulate movement when the pointer is down, use move() instead.');
final Offset delta = location != null ? newLocation - location! : Offset.zero;
_location = newLocation;
return PointerHoverEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
position: newLocation,
delta: delta,
);
}
/// Create a [PointerScrollEvent] (e.g., scroll wheel scroll; not finger-drag
/// scroll) with the given delta.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerScrollEvent scroll(
Offset scrollDelta, {
Duration timeStamp = Duration.zero,
}) {
assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events");
assert(location != null);
return PointerScrollEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
position: location!,
scrollDelta: scrollDelta,
);
}
}
/// Signature for a callback that can dispatch events and returns a future that
/// completes when the event dispatch is complete.
typedef EventDispatcher = Future<void> Function(PointerEvent event);
/// Signature for callbacks that perform hit-testing at a given location.
typedef HitTester = HitTestResult Function(Offset 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] without dispatching any events from it.
/// The [TestGesture] can then be manipulated to perform future actions.
///
/// By default, the pointer identifier used is 1. This can be overridden by
/// providing the `pointer` argument.
///
/// A function to use for hit testing must be provided via the `hitTester`
/// argument, and a function to use for dispatching events must be provided
/// via the `dispatcher` argument.
///
/// The device `kind` defaults to [PointerDeviceKind.touch], but move events
/// when the pointer is "up" require a kind other than
/// [PointerDeviceKind.touch], like [PointerDeviceKind.mouse], for example,
/// because touch devices can't produce movement events when they are "up".
///
/// None of the arguments may be null. The `dispatcher` and `hitTester`
/// arguments are required.
TestGesture({
required EventDispatcher dispatcher,
int pointer = 1,
PointerDeviceKind kind = PointerDeviceKind.touch,
int? device,
int buttons = kPrimaryButton,
}) : _dispatcher = dispatcher,
_pointer = TestPointer(pointer, kind, device, buttons);
/// Dispatch a pointer down event at the given `downLocation`, caching the
/// hit test result.
Future<void> down(Offset downLocation, { Duration timeStamp = Duration.zero }) async {
return TestAsyncUtils.guard<void>(() async {
return _dispatcher(_pointer.down(downLocation, timeStamp: timeStamp));
});
}
/// Dispatch a pointer down event at the given `downLocation`, caching the
/// hit test result with a custom down event.
Future<void> downWithCustomEvent(Offset downLocation, PointerDownEvent event) async {
_pointer.setDownInfo(event, downLocation);
return TestAsyncUtils.guard<void>(() async {
return _dispatcher(event);
});
}
final EventDispatcher _dispatcher;
final TestPointer _pointer;
/// In a test, send a move event that moves the pointer by the given offset.
@visibleForTesting
Future<void> updateWithCustomEvent(PointerEvent event, { Duration timeStamp = Duration.zero }) {
_pointer.setDownInfo(event, event.position);
return TestAsyncUtils.guard<void>(() {
return _dispatcher(event);
});
}
/// In a test, send a pointer add event for this pointer.
Future<void> addPointer({ Duration timeStamp = Duration.zero, Offset? location }) {
return TestAsyncUtils.guard<void>(() {
return _dispatcher(_pointer.addPointer(timeStamp: timeStamp, location: location ?? _pointer.location));
});
}
/// In a test, send a pointer remove event for this pointer.
Future<void> removePointer({ Duration timeStamp = Duration.zero, Offset? location }) {
return TestAsyncUtils.guard<void>(() {
return _dispatcher(_pointer.removePointer(timeStamp: timeStamp, location: location ?? _pointer.location));
});
}
/// Send a move event moving the pointer by the given offset.
///
/// If the pointer is down, then a move event is dispatched. If the pointer is
/// up, then a hover event is dispatched.
Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
assert(_pointer.location != null);
return moveTo(_pointer.location! + offset, timeStamp: timeStamp);
}
/// Send a move event moving the pointer to the given location.
///
/// If the pointer is down, then a move event is dispatched. If the pointer is
/// up, then a hover event is dispatched.
Future<void> moveTo(Offset location, { Duration timeStamp = Duration.zero }) {
return TestAsyncUtils.guard<void>(() {
if (_pointer._isDown) {
return _dispatcher(_pointer.move(location, timeStamp: timeStamp));
} else {
return _dispatcher(_pointer.hover(location, timeStamp: timeStamp));
}
});
}
/// End the gesture by releasing the pointer.
Future<void> up({ Duration timeStamp = Duration.zero }) {
return TestAsyncUtils.guard<void>(() async {
assert(_pointer._isDown);
await _dispatcher(_pointer.up(timeStamp: timeStamp));
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).
Future<void> cancel({ Duration timeStamp = Duration.zero }) {
return TestAsyncUtils.guard<void>(() async {
assert(_pointer._isDown);
await _dispatcher(_pointer.cancel(timeStamp: timeStamp));
assert(!_pointer._isDown);
});
}
}
/// A record of input [PointerEvent] list with the timeStamp of when it is
/// injected.
///
/// The [timeDelay] is used to indicate the time when the event packet should
/// be sent.
///
/// This is a simulation of how the framework is receiving input events from
/// the engine. See [GestureBinding] and [PointerDataPacket].
class PointerEventRecord {
/// Creates a pack of [PointerEvent]s.
PointerEventRecord(this.timeDelay, this.events);
/// The time delay of when the event record should be sent.
///
/// This value is used as the time delay relative to the start of
/// [WidgetTester.handlePointerEventRecord] call.
final Duration timeDelay;
/// The event list of the record.
///
/// This can be considered as a simulation of the events expanded from the
/// [PointerDataPacket].
///
/// See [PointerEventConverter.expand].
final List<PointerEvent> events;
}