test_pointer.dart 14.3 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
Adam Barth's avatar
Adam Barth committed
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'package:flutter/foundation.dart';
6
import 'package:flutter/gestures.dart';
7

8 9
import 'test_async_utils.dart';

10
export 'dart:ui' show Offset;
11

12 13
/// A class for generating coherent artificial pointer events.
///
14 15
/// You can use this to manually simulate individual events, but the simplest
/// way to generate coherent gestures is to use [TestGesture].
16
class TestPointer {
17 18 19 20 21 22
  /// 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.
23 24 25
  TestPointer([
    this.pointer = 1,
    this.kind = PointerDeviceKind.touch,
26
    int? device,
27
    int buttons = kPrimaryButton,
28
  ]) : _buttons = buttons {
29 30
    switch (kind) {
      case PointerDeviceKind.mouse:
31
        _device = device ?? 1;
32 33 34 35 36
        break;
      case PointerDeviceKind.stylus:
      case PointerDeviceKind.invertedStylus:
      case PointerDeviceKind.touch:
      case PointerDeviceKind.unknown:
37
        _device = device ?? 0;
38 39 40 41 42 43 44 45 46
        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;
47
  late int _device;
48

49 50 51 52
  /// The pointer identifier used for events generated by this object.
  ///
  /// Set when the object is constructed. Defaults to 1.
  final int pointer;
53

54
  /// The kind of pointing device to simulate. Defaults to
55 56 57
  /// [PointerDeviceKind.touch].
  final PointerDeviceKind kind;

58 59 60 61 62
  /// The kind of buttons to simulate on Down and Move events. Defaults to
  /// [kPrimaryButton].
  int get buttons => _buttons;
  int _buttons;

63 64 65 66 67 68 69 70 71 72 73
  /// 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.
74 75
  Offset? get location => _location;
  Offset? _location;
76

77 78
  /// If a custom event is created outside of this class, this function is used
  /// to set the [isDown].
79 80 81
  bool setDownInfo(
    PointerEvent event,
    Offset newLocation, {
82
    int? buttons,
83
  }) {
84
    _location = newLocation;
85 86
    if (buttons != null)
      _buttons = buttons;
87 88 89 90 91 92 93 94 95 96
    switch (event.runtimeType) {
      case PointerDownEvent:
        assert(!isDown);
        _isDown = true;
        break;
      case PointerUpEvent:
      case PointerCancelEvent:
        assert(isDown);
        _isDown = false;
        break;
97 98
      default:
        break;
99 100 101 102
    }
    return isDown;
  }

103 104
  /// Create a [PointerDownEvent] at the given location.
  ///
105 106
  /// By default, the time stamp on the event is [Duration.zero]. You can give a
  /// specific time stamp by passing the `timeStamp` argument.
107 108 109 110 111 112
  ///
  /// 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,
113
    int? buttons,
114
  }) {
115
    assert(!isDown);
116 117
    _isDown = true;
    _location = newLocation;
118 119
    if (buttons != null)
      _buttons = buttons;
120
    return PointerDownEvent(
Ian Hickson's avatar
Ian Hickson committed
121
      timeStamp: timeStamp,
122
      kind: kind,
123
      device: _device,
124
      pointer: pointer,
125
      position: location!,
126
      buttons: _buttons,
127 128 129
    );
  }

130 131
  /// Create a [PointerMoveEvent] to the given location.
  ///
132 133 134 135 136
  /// 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.
137 138 139 140 141 142
  ///
  /// 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,
143
    int? buttons,
144
  }) {
145 146 147 148 149
    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.');
150
    final Offset delta = newLocation - location!;
151
    _location = newLocation;
152 153
    if (buttons != null)
      _buttons = buttons;
154
    return PointerMoveEvent(
Ian Hickson's avatar
Ian Hickson committed
155
      timeStamp: timeStamp,
156
      kind: kind,
157
      device: _device,
158
      pointer: pointer,
Ian Hickson's avatar
Ian Hickson committed
159
      position: newLocation,
160
      delta: delta,
161
      buttons: _buttons,
162 163 164
    );
  }

165 166
  /// Create a [PointerUpEvent].
  ///
167 168
  /// By default, the time stamp on the event is [Duration.zero]. You can give a
  /// specific time stamp by passing the `timeStamp` argument.
169 170
  ///
  /// The object is no longer usable after this method has been called.
171
  PointerUpEvent up({ Duration timeStamp = Duration.zero }) {
172
    assert(isDown);
173
    _isDown = false;
174
    return PointerUpEvent(
Ian Hickson's avatar
Ian Hickson committed
175
      timeStamp: timeStamp,
176
      kind: kind,
177
      device: _device,
178
      pointer: pointer,
179
      position: location!,
180 181 182
    );
  }

183 184
  /// Create a [PointerCancelEvent].
  ///
185 186
  /// By default, the time stamp on the event is [Duration.zero]. You can give a
  /// specific time stamp by passing the `timeStamp` argument.
187 188
  ///
  /// The object is no longer usable after this method has been called.
189
  PointerCancelEvent cancel({ Duration timeStamp = Duration.zero }) {
190
    assert(isDown);
191
    _isDown = false;
192
    return PointerCancelEvent(
Ian Hickson's avatar
Ian Hickson committed
193
      timeStamp: timeStamp,
194
      kind: kind,
195
      device: _device,
196
      pointer: pointer,
197
      position: location!,
198 199 200
    );
  }

201 202 203 204 205 206 207
  /// 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,
208
    Offset? location,
209
  }) {
210
    _location = location ?? _location;
211 212 213 214
    return PointerAddedEvent(
      timeStamp: timeStamp,
      kind: kind,
      device: _device,
215
      position: _location ?? Offset.zero,
216 217 218
    );
  }

219 220
  /// Create a [PointerRemovedEvent] with the [PointerDeviceKind] the pointer
  /// was created with.
221 222 223 224 225
  ///
  /// 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,
226
    Offset? location,
227
  }) {
228
    _location = location ?? _location;
229 230 231 232
    return PointerRemovedEvent(
      timeStamp: timeStamp,
      kind: kind,
      device: _device,
233
      position: _location ?? Offset.zero,
234 235 236
    );
  }

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
  /// 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.');
252
    final Offset delta = location != null ? newLocation - location! : Offset.zero;
253 254 255 256
    _location = newLocation;
    return PointerHoverEvent(
      timeStamp: timeStamp,
      kind: kind,
257
      device: _device,
258
      position: newLocation,
259
      delta: delta,
260 261
    );
  }
262 263 264 265 266 267 268 269 270 271 272

  /// 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");
273
    assert(location != null);
274 275 276
    return PointerScrollEvent(
      timeStamp: timeStamp,
      kind: kind,
277
      device: _device,
278
      position: location!,
279 280 281
      scrollDelta: scrollDelta,
    );
  }
282 283
}

284
/// Signature for a callback that can dispatch events and returns a future that
285
/// completes when the event dispatch is complete.
286
typedef EventDispatcher = Future<void> Function(PointerEvent event);
287 288

/// Signature for callbacks that perform hit-testing at a given location.
289
typedef HitTester = HitTestResult Function(Offset location);
290

291 292 293 294 295
/// A class for performing gestures in tests.
///
/// The simplest way to create a [TestGesture] is to call
/// [WidgetTester.startGesture].
class TestGesture {
296 297
  /// Create a [TestGesture] without dispatching any events from it.
  /// The [TestGesture] can then be manipulated to perform future actions.
298
  ///
299
  /// By default, the pointer identifier used is 1. This can be overridden by
300 301
  /// providing the `pointer` argument.
  ///
302 303
  /// 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
304
  /// via the `dispatcher` argument.
305
  ///
306 307 308 309
  /// 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".
310
  ///
311 312 313
  /// None of the arguments may be null. The `dispatcher` and `hitTester`
  /// arguments are required.
  TestGesture({
314
    required EventDispatcher dispatcher,
315 316
    int pointer = 1,
    PointerDeviceKind kind = PointerDeviceKind.touch,
317
    int? device,
318
    int buttons = kPrimaryButton,
319
  }) : _dispatcher = dispatcher,
320
       _pointer = TestPointer(pointer, kind, device, buttons);
321 322 323

  /// Dispatch a pointer down event at the given `downLocation`, caching the
  /// hit test result.
324
  Future<void> down(Offset downLocation, { Duration timeStamp = Duration.zero }) async {
325
    return TestAsyncUtils.guard<void>(() async {
326
      return _dispatcher(_pointer.down(downLocation, timeStamp: timeStamp));
327 328 329
    });
  }

330 331
  /// Dispatch a pointer down event at the given `downLocation`, caching the
  /// hit test result with a custom down event.
332 333
  Future<void> downWithCustomEvent(Offset downLocation, PointerDownEvent event) async {
    _pointer.setDownInfo(event, downLocation);
334
    return TestAsyncUtils.guard<void>(() async {
335
      return _dispatcher(event);
336 337 338
    });
  }

339
  final EventDispatcher _dispatcher;
340 341
  final TestPointer _pointer;

342 343
  /// In a test, send a move event that moves the pointer by the given offset.
  @visibleForTesting
344
  Future<void> updateWithCustomEvent(PointerEvent event, { Duration timeStamp = Duration.zero }) {
345 346
    _pointer.setDownInfo(event, event.position);
    return TestAsyncUtils.guard<void>(() {
347
      return _dispatcher(event);
348 349 350
    });
  }

351
  /// In a test, send a pointer add event for this pointer.
352
  Future<void> addPointer({ Duration timeStamp = Duration.zero, Offset? location }) {
353
    return TestAsyncUtils.guard<void>(() {
354
      return _dispatcher(_pointer.addPointer(timeStamp: timeStamp, location: location ?? _pointer.location));
355 356 357 358
    });
  }

  /// In a test, send a pointer remove event for this pointer.
359
  Future<void> removePointer({ Duration timeStamp = Duration.zero, Offset? location }) {
360
    return TestAsyncUtils.guard<void>(() {
361
      return _dispatcher(_pointer.removePointer(timeStamp: timeStamp, location: location ?? _pointer.location));
362 363 364
    });
  }

365
  /// Send a move event moving the pointer by the given offset.
366 367
  ///
  /// If the pointer is down, then a move event is dispatched. If the pointer is
368
  /// up, then a hover event is dispatched.
369
  Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
370 371
    assert(_pointer.location != null);
    return moveTo(_pointer.location! + offset, timeStamp: timeStamp);
372 373 374
  }

  /// Send a move event moving the pointer to the given location.
375 376
  ///
  /// If the pointer is down, then a move event is dispatched. If the pointer is
377
  /// up, then a hover event is dispatched.
378
  Future<void> moveTo(Offset location, { Duration timeStamp = Duration.zero }) {
379
    return TestAsyncUtils.guard<void>(() {
380
      if (_pointer._isDown) {
381
        return _dispatcher(_pointer.move(location, timeStamp: timeStamp));
382
      } else {
383
        return _dispatcher(_pointer.hover(location, timeStamp: timeStamp));
384
      }
385
    });
386 387 388
  }

  /// End the gesture by releasing the pointer.
389
  Future<void> up({ Duration timeStamp = Duration.zero }) {
390
    return TestAsyncUtils.guard<void>(() async {
391
      assert(_pointer._isDown);
392
      await _dispatcher(_pointer.up(timeStamp: timeStamp));
393 394
      assert(!_pointer._isDown);
    });
395
  }
396

397 398 399
  /// 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).
400
  Future<void> cancel({ Duration timeStamp = Duration.zero }) {
401
    return TestAsyncUtils.guard<void>(() async {
402
      assert(_pointer._isDown);
403
      await _dispatcher(_pointer.cancel(timeStamp: timeStamp));
404 405
      assert(!_pointer._isDown);
    });
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

/// 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;
}