1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
// 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;
case PointerDeviceKind.stylus:
case PointerDeviceKind.invertedStylus:
case PointerDeviceKind.touch:
case PointerDeviceKind.trackpad:
case PointerDeviceKind.unknown:
_device = device ?? 0;
}
}
/// 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;
/// Whether the pointer simulated by this object currently has
/// an active pan/zoom gesture.
///
/// A pan/zoom gesture begins when [panZoomStart] is called, and
/// ends when [panZoomEnd] is called.
bool get isPanZoomActive => _isPanZoomActive;
bool _isPanZoomActive = 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;
/// The pan offset of the last pointer pan/zoom event sent by this object.
///
/// If no pan/zoom event has ever been sent by this object, returns null.
Offset? get pan => _pan;
Offset? _pan;
/// 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 const (PointerDownEvent):
assert(!isDown);
_isDown = true;
case const (PointerUpEvent):
case const (PointerCancelEvent):
assert(isDown);
_isDown = false;
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);
assert(!isPanZoomActive);
_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.');
assert(!isPanZoomActive);
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(!isPanZoomActive);
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,
pointer: pointer,
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,
pointer: pointer,
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,
);
}
/// Create a [PointerScrollInertiaCancelEvent] (e.g., user resting their finger on the trackpad).
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerScrollInertiaCancelEvent scrollInertiaCancel({
Duration timeStamp = Duration.zero,
}) {
assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events");
assert(location != null);
return PointerScrollInertiaCancelEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
position: location!
);
}
/// Create a [PointerScaleEvent] (e.g., legacy pinch-to-zoom).
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerScaleEvent scale(
double scale, {
Duration timeStamp = Duration.zero,
}) {
assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events");
assert(location != null);
return PointerScaleEvent(
timeStamp: timeStamp,
kind: kind,
device: _device,
position: location!,
scale: scale,
);
}
/// Create a [PointerPanZoomStartEvent] (e.g., trackpad scroll; not scroll wheel
/// or 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.
PointerPanZoomStartEvent panZoomStart(
Offset location, {
Duration timeStamp = Duration.zero
}) {
assert(!isPanZoomActive);
assert(kind == PointerDeviceKind.trackpad);
_location = location;
_pan = Offset.zero;
_isPanZoomActive = true;
return PointerPanZoomStartEvent(
timeStamp: timeStamp,
device: _device,
pointer: pointer,
position: location,
);
}
/// Create a [PointerPanZoomUpdateEvent] to update the active pan/zoom sequence
/// on this pointer with updated pan, scale, and/or rotation values.
///
/// [rotation] is in units of radians.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerPanZoomUpdateEvent panZoomUpdate(
Offset location, {
Offset pan = Offset.zero,
double scale = 1,
double rotation = 0,
Duration timeStamp = Duration.zero,
}) {
assert(isPanZoomActive);
assert(kind == PointerDeviceKind.trackpad);
_location = location;
final Offset panDelta = pan - _pan!;
_pan = pan;
return PointerPanZoomUpdateEvent(
timeStamp: timeStamp,
device: _device,
pointer: pointer,
position: location,
pan: pan,
panDelta: panDelta,
scale: scale,
rotation: rotation,
);
}
/// Create a [PointerPanZoomEndEvent] to end the active pan/zoom sequence
/// on this pointer.
///
/// By default, the time stamp on the event is [Duration.zero]. You can give a
/// specific time stamp by passing the `timeStamp` argument.
PointerPanZoomEndEvent panZoomEnd({
Duration timeStamp = Duration.zero
}) {
assert(isPanZoomActive);
assert(kind == PointerDeviceKind.trackpad);
_isPanZoomActive = false;
_pan = null;
return PointerPanZoomEndEvent(
timeStamp: timeStamp,
device: _device,
pointer: pointer,
position: location!,
);
}
}
/// 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 {
assert(_pointer.kind != PointerDeviceKind.trackpad, 'Trackpads are expected to send panZoomStart events, not down events.');
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 {
assert(_pointer.kind != PointerDeviceKind.trackpad, 'Trackpads are expected to send panZoomStart events, not down events');
_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.
///
/// See also:
/// * [WidgetController.drag], a method to simulate a drag.
/// * [WidgetController.timedDrag], a method to simulate the drag of a given widget in a given duration.
/// It sends move events at a given frequency and it is useful when there are listeners involved.
/// * [WidgetController.fling], a method to simulate a fling.
Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
assert(_pointer.location != null);
if (_pointer.isPanZoomActive) {
return panZoomUpdate(
_pointer.location!,
pan: (_pointer.pan ?? Offset.zero) + offset,
timeStamp: timeStamp
);
} else {
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.
///
/// See also:
/// * [WidgetController.drag], a method to simulate a drag.
/// * [WidgetController.timedDrag], a method to simulate the drag of a given widget in a given duration.
/// It sends move events at a given frequency and it is useful when there are listeners involved.
/// * [WidgetController.fling], a method to simulate a fling.
Future<void> moveTo(Offset location, { Duration timeStamp = Duration.zero }) {
assert(_pointer.kind != PointerDeviceKind.trackpad);
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. For trackpad pointers this
/// will send a panZoomEnd event instead of an up event.
Future<void> up({ Duration timeStamp = Duration.zero }) {
return TestAsyncUtils.guard<void>(() async {
if (_pointer.kind == PointerDeviceKind.trackpad) {
assert(_pointer._isPanZoomActive);
await _dispatcher(_pointer.panZoomEnd(timeStamp: timeStamp));
assert(!_pointer._isPanZoomActive);
} else {
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 }) {
assert(_pointer.kind != PointerDeviceKind.trackpad, 'Trackpads do not send cancel events.');
return TestAsyncUtils.guard<void>(() async {
assert(_pointer._isDown);
await _dispatcher(_pointer.cancel(timeStamp: timeStamp));
assert(!_pointer._isDown);
});
}
/// Dispatch a pointer pan zoom start event at the given `location`, caching the
/// hit test result.
Future<void> panZoomStart(Offset location, { Duration timeStamp = Duration.zero }) async {
assert(_pointer.kind == PointerDeviceKind.trackpad, 'Only trackpads can send PointerPanZoom events.');
return TestAsyncUtils.guard<void>(() async {
return _dispatcher(_pointer.panZoomStart(location, timeStamp: timeStamp));
});
}
/// Dispatch a pointer pan zoom update event at the given `location`, caching the
/// hit test result.
Future<void> panZoomUpdate(Offset location, {
Offset pan = Offset.zero,
double scale = 1,
double rotation = 0,
Duration timeStamp = Duration.zero
}) async {
assert(_pointer.kind == PointerDeviceKind.trackpad, 'Only trackpads can send PointerPanZoom events.');
return TestAsyncUtils.guard<void>(() async {
return _dispatcher(_pointer.panZoomUpdate(location,
pan: pan,
scale: scale,
rotation: rotation,
timeStamp: timeStamp
));
});
}
/// Dispatch a pointer pan zoom end event, caching the hit test result.
Future<void> panZoomEnd({
Duration timeStamp = Duration.zero
}) async {
assert(_pointer.kind == PointerDeviceKind.trackpad, 'Only trackpads can send PointerPanZoom events.');
return TestAsyncUtils.guard<void>(() async {
return _dispatcher(_pointer.panZoomEnd(
timeStamp: timeStamp
));
});
}
}
/// 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;
}