• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2015 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:flutter/foundation.dart';
8import 'package:flutter/gestures.dart';
9
10import 'test_async_utils.dart';
11
12export 'dart:ui' show Offset;
13
14/// A class for generating coherent artificial pointer events.
15///
16/// You can use this to manually simulate individual events, but the simplest
17/// way to generate coherent gestures is to use [TestGesture].
18class TestPointer {
19  /// Creates a [TestPointer]. By default, the pointer identifier used is 1,
20  /// however this can be overridden by providing an argument to the
21  /// constructor.
22  ///
23  /// Multiple [TestPointer]s created with the same pointer identifier will
24  /// interfere with each other if they are used in parallel.
25  TestPointer([
26    this.pointer = 1,
27    this.kind = PointerDeviceKind.touch,
28    this._device,
29    int buttons = kPrimaryButton,
30  ])
31      : assert(kind != null),
32        assert(pointer != null),
33        assert(buttons != null),
34        _buttons = buttons {
35    switch (kind) {
36      case PointerDeviceKind.mouse:
37        _device ??= 1;
38        break;
39      case PointerDeviceKind.stylus:
40      case PointerDeviceKind.invertedStylus:
41      case PointerDeviceKind.touch:
42      case PointerDeviceKind.unknown:
43        _device ??= 0;
44        break;
45    }
46  }
47
48  /// The device identifier used for events generated by this object.
49  ///
50  /// Set when the object is constructed. Defaults to 1 if the [kind] is
51  /// [PointerDeviceKind.mouse], and 0 otherwise.
52  int get device => _device;
53  int _device;
54
55  /// The pointer identifier used for events generated by this object.
56  ///
57  /// Set when the object is constructed. Defaults to 1.
58  final int pointer;
59
60  /// The kind of pointer device to simulate. Defaults to
61  /// [PointerDeviceKind.touch].
62  final PointerDeviceKind kind;
63
64  /// The kind of buttons to simulate on Down and Move events. Defaults to
65  /// [kPrimaryButton].
66  int get buttons => _buttons;
67  int _buttons;
68
69  /// Whether the pointer simulated by this object is currently down.
70  ///
71  /// A pointer is released (goes up) by calling [up] or [cancel].
72  ///
73  /// Once a pointer is released, it can no longer generate events.
74  bool get isDown => _isDown;
75  bool _isDown = false;
76
77  /// The position of the last event sent by this object.
78  ///
79  /// If no event has ever been sent by this object, returns null.
80  Offset get location => _location;
81  Offset _location;
82
83  /// If a custom event is created outside of this class, this function is used
84  /// to set the [isDown].
85  bool setDownInfo(
86    PointerEvent event,
87    Offset newLocation, {
88    int buttons,
89  }) {
90    _location = newLocation;
91    if (buttons != null)
92      _buttons = buttons;
93    switch (event.runtimeType) {
94      case PointerDownEvent:
95        assert(!isDown);
96        _isDown = true;
97        break;
98      case PointerUpEvent:
99      case PointerCancelEvent:
100        assert(isDown);
101        _isDown = false;
102        break;
103      default:
104        break;
105    }
106    return isDown;
107  }
108
109  /// Create a [PointerDownEvent] at the given location.
110  ///
111  /// By default, the time stamp on the event is [Duration.zero]. You can give a
112  /// specific time stamp by passing the `timeStamp` argument.
113  ///
114  /// By default, the set of buttons in the last down or move event is used.
115  /// You can give a specific set of buttons by passing the `buttons` argument.
116  PointerDownEvent down(
117    Offset newLocation, {
118    Duration timeStamp = Duration.zero,
119    int buttons,
120  }) {
121    assert(!isDown);
122    _isDown = true;
123    _location = newLocation;
124    if (buttons != null)
125      _buttons = buttons;
126    return PointerDownEvent(
127      timeStamp: timeStamp,
128      kind: kind,
129      device: _device,
130      pointer: pointer,
131      position: location,
132      buttons: _buttons,
133    );
134  }
135
136  /// Create a [PointerMoveEvent] to the given location.
137  ///
138  /// By default, the time stamp on the event is [Duration.zero]. You can give a
139  /// specific time stamp by passing the `timeStamp` argument.
140  ///
141  /// [isDown] must be true when this is called, since move events can only
142  /// be generated when the pointer is down.
143  ///
144  /// By default, the set of buttons in the last down or move event is used.
145  /// You can give a specific set of buttons by passing the `buttons` argument.
146  PointerMoveEvent move(
147    Offset newLocation, {
148    Duration timeStamp = Duration.zero,
149    int buttons,
150  }) {
151    assert(
152        isDown,
153        'Move events can only be generated when the pointer is down. To '
154        'create a movement event simulating a pointer move when the pointer is '
155        'up, use hover() instead.');
156    final Offset delta = newLocation - location;
157    _location = newLocation;
158    if (buttons != null)
159      _buttons = buttons;
160    return PointerMoveEvent(
161      timeStamp: timeStamp,
162      kind: kind,
163      device: _device,
164      pointer: pointer,
165      position: newLocation,
166      delta: delta,
167      buttons: _buttons,
168    );
169  }
170
171  /// Create a [PointerUpEvent].
172  ///
173  /// By default, the time stamp on the event is [Duration.zero]. You can give a
174  /// specific time stamp by passing the `timeStamp` argument.
175  ///
176  /// The object is no longer usable after this method has been called.
177  PointerUpEvent up({ Duration timeStamp = Duration.zero }) {
178    assert(isDown);
179    _isDown = false;
180    return PointerUpEvent(
181      timeStamp: timeStamp,
182      kind: kind,
183      device: _device,
184      pointer: pointer,
185      position: location,
186    );
187  }
188
189  /// Create a [PointerCancelEvent].
190  ///
191  /// By default, the time stamp on the event is [Duration.zero]. You can give a
192  /// specific time stamp by passing the `timeStamp` argument.
193  ///
194  /// The object is no longer usable after this method has been called.
195  PointerCancelEvent cancel({ Duration timeStamp = Duration.zero }) {
196    assert(isDown);
197    _isDown = false;
198    return PointerCancelEvent(
199      timeStamp: timeStamp,
200      kind: kind,
201      device: _device,
202      pointer: pointer,
203      position: location,
204    );
205  }
206
207  /// Create a [PointerAddedEvent] with the [PointerDeviceKind] the pointer was
208  /// created with.
209  ///
210  /// By default, the time stamp on the event is [Duration.zero]. You can give a
211  /// specific time stamp by passing the `timeStamp` argument.
212  PointerAddedEvent addPointer({
213    Duration timeStamp = Duration.zero,
214    Offset location,
215  }) {
216    assert(timeStamp != null);
217    _location = location ?? _location;
218    return PointerAddedEvent(
219      timeStamp: timeStamp,
220      kind: kind,
221      device: _device,
222      position: _location ?? Offset.zero,
223    );
224  }
225
226  /// Create a [PointerRemovedEvent] with the [PointerDeviceKind] the pointer
227  /// was created with.
228  ///
229  /// By default, the time stamp on the event is [Duration.zero]. You can give a
230  /// specific time stamp by passing the `timeStamp` argument.
231  PointerRemovedEvent removePointer({
232    Duration timeStamp = Duration.zero,
233    Offset location,
234  }) {
235    assert(timeStamp != null);
236    _location = location ?? _location;
237    return PointerRemovedEvent(
238      timeStamp: timeStamp,
239      kind: kind,
240      device: _device,
241      position: _location ?? Offset.zero,
242    );
243  }
244
245  /// Create a [PointerHoverEvent] to the given location.
246  ///
247  /// By default, the time stamp on the event is [Duration.zero]. You can give a
248  /// specific time stamp by passing the `timeStamp` argument.
249  ///
250  /// [isDown] must be false, since hover events can't be sent when the pointer
251  /// is up.
252  PointerHoverEvent hover(
253    Offset newLocation, {
254    Duration timeStamp = Duration.zero,
255  }) {
256    assert(newLocation != null);
257    assert(timeStamp != null);
258    assert(
259        !isDown,
260        'Hover events can only be generated when the pointer is up. To '
261        'simulate movement when the pointer is down, use move() instead.');
262    assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate hover events");
263    final Offset delta = location != null ? newLocation - location : Offset.zero;
264    _location = newLocation;
265    return PointerHoverEvent(
266      timeStamp: timeStamp,
267      kind: kind,
268      device: _device,
269      position: newLocation,
270      delta: delta,
271    );
272  }
273
274  /// Create a [PointerScrollEvent] (e.g., scroll wheel scroll; not finger-drag
275  /// scroll) with the given delta.
276  ///
277  /// By default, the time stamp on the event is [Duration.zero]. You can give a
278  /// specific time stamp by passing the `timeStamp` argument.
279  PointerScrollEvent scroll(
280    Offset scrollDelta, {
281    Duration timeStamp = Duration.zero,
282  }) {
283    assert(scrollDelta != null);
284    assert(timeStamp != null);
285    assert(kind != PointerDeviceKind.touch, "Touch pointers can't generate pointer signal events");
286    return PointerScrollEvent(
287      timeStamp: timeStamp,
288      kind: kind,
289      device: _device,
290      position: location,
291      scrollDelta: scrollDelta,
292    );
293  }
294}
295
296/// Signature for a callback that can dispatch events and returns a future that
297/// completes when the event dispatch is complete.
298typedef EventDispatcher = Future<void> Function(PointerEvent event, HitTestResult result);
299
300/// Signature for callbacks that perform hit-testing at a given location.
301typedef HitTester = HitTestResult Function(Offset location);
302
303/// A class for performing gestures in tests.
304///
305/// The simplest way to create a [TestGesture] is to call
306/// [WidgetTester.startGesture].
307class TestGesture {
308  /// Create a [TestGesture] without dispatching any events from it.
309  /// The [TestGesture] can then be manipulated to perform future actions.
310  ///
311  /// By default, the pointer identifier used is 1. This can be overridden by
312  /// providing the `pointer` argument.
313  ///
314  /// A function to use for hit testing must be provided via the `hitTester`
315  /// argument, and a function to use for dispatching events must be provided
316  /// via the `dispatcher` argument.
317  ///
318  /// The device `kind` defaults to [PointerDeviceKind.touch], but move events
319  /// when the pointer is "up" require a kind other than
320  /// [PointerDeviceKind.touch], like [PointerDeviceKind.mouse], for example,
321  /// because touch devices can't produce movement events when they are "up".
322  ///
323  /// None of the arguments may be null. The `dispatcher` and `hitTester`
324  /// arguments are required.
325  TestGesture({
326    @required EventDispatcher dispatcher,
327    @required HitTester hitTester,
328    int pointer = 1,
329    PointerDeviceKind kind = PointerDeviceKind.touch,
330    int device,
331    int buttons = kPrimaryButton,
332  }) : assert(dispatcher != null),
333       assert(hitTester != null),
334       assert(pointer != null),
335       assert(kind != null),
336       assert(buttons != null),
337       _dispatcher = dispatcher,
338       _hitTester = hitTester,
339       _pointer = TestPointer(pointer, kind, device, buttons),
340       _result = null;
341
342  /// Dispatch a pointer down event at the given `downLocation`, caching the
343  /// hit test result.
344  Future<void> down(Offset downLocation) async {
345    return TestAsyncUtils.guard<void>(() async {
346      _result = _hitTester(downLocation);
347      return _dispatcher(_pointer.down(downLocation), _result);
348    });
349  }
350
351  /// Dispatch a pointer down event at the given `downLocation`, caching the
352  /// hit test result with a custom down event.
353  Future<void> downWithCustomEvent(Offset downLocation, PointerDownEvent event) async {
354    _pointer.setDownInfo(event, downLocation);
355    return TestAsyncUtils.guard<void>(() async {
356      _result = _hitTester(downLocation);
357      return _dispatcher(event, _result);
358    });
359  }
360
361  final EventDispatcher _dispatcher;
362  final HitTester _hitTester;
363  final TestPointer _pointer;
364  HitTestResult _result;
365
366  /// In a test, send a move event that moves the pointer by the given offset.
367  @visibleForTesting
368  Future<void> updateWithCustomEvent(PointerEvent event, { Duration timeStamp = Duration.zero }) {
369    _pointer.setDownInfo(event, event.position);
370    return TestAsyncUtils.guard<void>(() {
371      return _dispatcher(event, _result);
372    });
373  }
374
375  /// In a test, send a pointer add event for this pointer.
376  Future<void> addPointer({ Duration timeStamp = Duration.zero }) {
377    return TestAsyncUtils.guard<void>(() {
378      return _dispatcher(_pointer.addPointer(timeStamp: timeStamp, location: _pointer.location), null);
379    });
380  }
381
382  /// In a test, send a pointer remove event for this pointer.
383  Future<void> removePointer({ Duration timeStamp = Duration.zero}) {
384    return TestAsyncUtils.guard<void>(() {
385      return _dispatcher(_pointer.removePointer(timeStamp: timeStamp, location: _pointer.location), null);
386    });
387  }
388
389  /// Send a move event moving the pointer by the given offset.
390  ///
391  /// If the pointer is down, then a move event is dispatched. If the pointer is
392  /// up, then a hover event is dispatched. Touch devices are not able to send
393  /// hover events.
394  Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
395    return moveTo(_pointer.location + offset, timeStamp: timeStamp);
396  }
397
398  /// Send a move event moving the pointer to the given location.
399  ///
400  /// If the pointer is down, then a move event is dispatched. If the pointer is
401  /// up, then a hover event is dispatched. Touch devices are not able to send
402  /// hover events.
403  Future<void> moveTo(Offset location, { Duration timeStamp = Duration.zero }) {
404    return TestAsyncUtils.guard<void>(() {
405      if (_pointer._isDown) {
406        assert(_result != null,
407            'Move events with the pointer down must be preceded by a down '
408            'event that captures a hit test result.');
409        return _dispatcher(_pointer.move(location, timeStamp: timeStamp), _result);
410      } else {
411        assert(_pointer.kind != PointerDeviceKind.touch,
412            'Touch device move events can only be sent if the pointer is down.');
413        return _dispatcher(_pointer.hover(location, timeStamp: timeStamp), null);
414      }
415    });
416  }
417
418  /// End the gesture by releasing the pointer.
419  Future<void> up() {
420    return TestAsyncUtils.guard<void>(() async {
421      assert(_pointer._isDown);
422      await _dispatcher(_pointer.up(), _result);
423      assert(!_pointer._isDown);
424      _result = null;
425    });
426  }
427
428  /// End the gesture by canceling the pointer (as would happen if the
429  /// system showed a modal dialog on top of the Flutter application,
430  /// for instance).
431  Future<void> cancel() {
432    return TestAsyncUtils.guard<void>(() async {
433      assert(_pointer._isDown);
434      await _dispatcher(_pointer.cancel(), _result);
435      assert(!_pointer._isDown);
436      _result = null;
437    });
438  }
439}
440