test_pointer.dart 18.5 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
        break;
      case PointerDeviceKind.stylus:
      case PointerDeviceKind.invertedStylus:
      case PointerDeviceKind.touch:
36
      case PointerDeviceKind.trackpad:
37
      case PointerDeviceKind.unknown:
38
        _device = device ?? 0;
39 40 41 42 43 44 45 46 47
        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;
48
  late int _device;
49

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

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

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

64 65 66 67 68 69 70 71
  /// 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;

72 73 74 75 76 77 78 79
  /// 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;

80 81 82
  /// The position of the last event sent by this object.
  ///
  /// If no event has ever been sent by this object, returns null.
83 84
  Offset? get location => _location;
  Offset? _location;
85

86 87 88 89 90 91 92

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

93 94
  /// If a custom event is created outside of this class, this function is used
  /// to set the [isDown].
95 96 97
  bool setDownInfo(
    PointerEvent event,
    Offset newLocation, {
98
    int? buttons,
99
  }) {
100
    _location = newLocation;
101
    if (buttons != null) {
102
      _buttons = buttons;
103
    }
104 105 106 107 108 109 110 111 112 113
    switch (event.runtimeType) {
      case PointerDownEvent:
        assert(!isDown);
        _isDown = true;
        break;
      case PointerUpEvent:
      case PointerCancelEvent:
        assert(isDown);
        _isDown = false;
        break;
114 115
      default:
        break;
116 117 118 119
    }
    return isDown;
  }

120 121
  /// Create a [PointerDownEvent] at the given location.
  ///
122 123
  /// By default, the time stamp on the event is [Duration.zero]. You can give a
  /// specific time stamp by passing the `timeStamp` argument.
124 125 126 127 128 129
  ///
  /// 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,
130
    int? buttons,
131
  }) {
132
    assert(!isDown);
133
    assert(!isPanZoomActive);
134 135
    _isDown = true;
    _location = newLocation;
136
    if (buttons != null) {
137
      _buttons = buttons;
138
    }
139
    return PointerDownEvent(
Ian Hickson's avatar
Ian Hickson committed
140
      timeStamp: timeStamp,
141
      kind: kind,
142
      device: _device,
143
      pointer: pointer,
144
      position: location!,
145
      buttons: _buttons,
146 147 148
    );
  }

149 150
  /// Create a [PointerMoveEvent] to the given location.
  ///
151 152 153 154 155
  /// 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.
156 157 158 159 160 161
  ///
  /// 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,
162
    int? buttons,
163
  }) {
164 165 166 167 168
    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.');
169
    assert(!isPanZoomActive);
170
    final Offset delta = newLocation - location!;
171
    _location = newLocation;
172
    if (buttons != null) {
173
      _buttons = buttons;
174
    }
175
    return PointerMoveEvent(
Ian Hickson's avatar
Ian Hickson committed
176
      timeStamp: timeStamp,
177
      kind: kind,
178
      device: _device,
179
      pointer: pointer,
Ian Hickson's avatar
Ian Hickson committed
180
      position: newLocation,
181
      delta: delta,
182
      buttons: _buttons,
183 184 185
    );
  }

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

205 206
  /// Create a [PointerCancelEvent].
  ///
207 208
  /// By default, the time stamp on the event is [Duration.zero]. You can give a
  /// specific time stamp by passing the `timeStamp` argument.
209 210
  ///
  /// The object is no longer usable after this method has been called.
211
  PointerCancelEvent cancel({ Duration timeStamp = Duration.zero }) {
212
    assert(isDown);
213
    _isDown = false;
214
    return PointerCancelEvent(
Ian Hickson's avatar
Ian Hickson committed
215
      timeStamp: timeStamp,
216
      kind: kind,
217
      device: _device,
218
      pointer: pointer,
219
      position: location!,
220 221 222
    );
  }

223 224 225 226 227 228 229
  /// 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,
230
    Offset? location,
231
  }) {
232
    _location = location ?? _location;
233 234 235 236
    return PointerAddedEvent(
      timeStamp: timeStamp,
      kind: kind,
      device: _device,
237
      position: _location ?? Offset.zero,
238 239 240
    );
  }

241 242
  /// Create a [PointerRemovedEvent] with the [PointerDeviceKind] the pointer
  /// was created with.
243 244 245 246 247
  ///
  /// 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,
248
    Offset? location,
249
  }) {
250
    _location = location ?? _location;
251 252 253 254
    return PointerRemovedEvent(
      timeStamp: timeStamp,
      kind: kind,
      device: _device,
255
      pointer: pointer,
256
      position: _location ?? Offset.zero,
257 258 259
    );
  }

260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
  /// 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.');
275
    final Offset delta = location != null ? newLocation - location! : Offset.zero;
276 277 278 279
    _location = newLocation;
    return PointerHoverEvent(
      timeStamp: timeStamp,
      kind: kind,
280
      device: _device,
281
      pointer: pointer,
282
      position: newLocation,
283
      delta: delta,
284 285
    );
  }
286 287 288 289 290 291 292 293 294 295 296

  /// 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");
297
    assert(location != null);
298 299 300
    return PointerScrollEvent(
      timeStamp: timeStamp,
      kind: kind,
301
      device: _device,
302
      position: location!,
303 304 305
      scrollDelta: scrollDelta,
    );
  }
306

307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
  /// 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!
    );
  }

324 325 326 327 328 329 330 331 332 333
  /// 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);
334
    assert(kind == PointerDeviceKind.trackpad);
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
    _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);
361
    assert(kind == PointerDeviceKind.trackpad);
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
    _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);
386
    assert(kind == PointerDeviceKind.trackpad);
387 388 389 390 391 392 393 394 395
    _isPanZoomActive = false;
    _pan = null;
    return PointerPanZoomEndEvent(
      timeStamp: timeStamp,
      device: _device,
      pointer: pointer,
      position: location!,
    );
  }
396 397
}

398
/// Signature for a callback that can dispatch events and returns a future that
399
/// completes when the event dispatch is complete.
400
typedef EventDispatcher = Future<void> Function(PointerEvent event);
401 402

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

405 406 407 408 409
/// A class for performing gestures in tests.
///
/// The simplest way to create a [TestGesture] is to call
/// [WidgetTester.startGesture].
class TestGesture {
410 411
  /// Create a [TestGesture] without dispatching any events from it.
  /// The [TestGesture] can then be manipulated to perform future actions.
412
  ///
413
  /// By default, the pointer identifier used is 1. This can be overridden by
414 415
  /// providing the `pointer` argument.
  ///
416 417
  /// 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
418
  /// via the `dispatcher` argument.
419
  ///
420 421 422 423
  /// 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".
424
  ///
425 426 427
  /// None of the arguments may be null. The `dispatcher` and `hitTester`
  /// arguments are required.
  TestGesture({
428
    required EventDispatcher dispatcher,
429 430
    int pointer = 1,
    PointerDeviceKind kind = PointerDeviceKind.touch,
431
    int? device,
432
    int buttons = kPrimaryButton,
433
  }) : _dispatcher = dispatcher,
434
       _pointer = TestPointer(pointer, kind, device, buttons);
435 436 437

  /// Dispatch a pointer down event at the given `downLocation`, caching the
  /// hit test result.
438
  Future<void> down(Offset downLocation, { Duration timeStamp = Duration.zero }) async {
439
    return TestAsyncUtils.guard<void>(() async {
440
      return _dispatcher(_pointer.down(downLocation, timeStamp: timeStamp));
441 442 443
    });
  }

444 445
  /// Dispatch a pointer down event at the given `downLocation`, caching the
  /// hit test result with a custom down event.
446 447
  Future<void> downWithCustomEvent(Offset downLocation, PointerDownEvent event) async {
    _pointer.setDownInfo(event, downLocation);
448
    return TestAsyncUtils.guard<void>(() async {
449
      return _dispatcher(event);
450 451 452
    });
  }

453
  final EventDispatcher _dispatcher;
454 455
  final TestPointer _pointer;

456 457
  /// In a test, send a move event that moves the pointer by the given offset.
  @visibleForTesting
458
  Future<void> updateWithCustomEvent(PointerEvent event, { Duration timeStamp = Duration.zero }) {
459 460
    _pointer.setDownInfo(event, event.position);
    return TestAsyncUtils.guard<void>(() {
461
      return _dispatcher(event);
462 463 464
    });
  }

465
  /// In a test, send a pointer add event for this pointer.
466
  Future<void> addPointer({ Duration timeStamp = Duration.zero, Offset? location }) {
467
    return TestAsyncUtils.guard<void>(() {
468
      return _dispatcher(_pointer.addPointer(timeStamp: timeStamp, location: location ?? _pointer.location));
469 470 471 472
    });
  }

  /// In a test, send a pointer remove event for this pointer.
473
  Future<void> removePointer({ Duration timeStamp = Duration.zero, Offset? location }) {
474
    return TestAsyncUtils.guard<void>(() {
475
      return _dispatcher(_pointer.removePointer(timeStamp: timeStamp, location: location ?? _pointer.location));
476 477 478
    });
  }

479
  /// Send a move event moving the pointer by the given offset.
480 481
  ///
  /// If the pointer is down, then a move event is dispatched. If the pointer is
482
  /// up, then a hover event is dispatched.
483 484 485 486 487 488
  ///
  /// 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.
489
  Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
490 491
    assert(_pointer.location != null);
    return moveTo(_pointer.location! + offset, timeStamp: timeStamp);
492 493 494
  }

  /// Send a move event moving the pointer to the given location.
495 496
  ///
  /// If the pointer is down, then a move event is dispatched. If the pointer is
497
  /// up, then a hover event is dispatched.
498 499 500 501 502 503
  ///
  /// 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.
504
  Future<void> moveTo(Offset location, { Duration timeStamp = Duration.zero }) {
505
    return TestAsyncUtils.guard<void>(() {
506
      if (_pointer._isDown) {
507
        return _dispatcher(_pointer.move(location, timeStamp: timeStamp));
508
      } else {
509
        return _dispatcher(_pointer.hover(location, timeStamp: timeStamp));
510
      }
511
    });
512 513 514
  }

  /// End the gesture by releasing the pointer.
515
  Future<void> up({ Duration timeStamp = Duration.zero }) {
516
    return TestAsyncUtils.guard<void>(() async {
517
      assert(_pointer._isDown);
518
      await _dispatcher(_pointer.up(timeStamp: timeStamp));
519 520
      assert(!_pointer._isDown);
    });
521
  }
522

523 524 525
  /// 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).
526
  Future<void> cancel({ Duration timeStamp = Duration.zero }) {
527
    return TestAsyncUtils.guard<void>(() async {
528
      assert(_pointer._isDown);
529
      await _dispatcher(_pointer.cancel(timeStamp: timeStamp));
530 531
      assert(!_pointer._isDown);
    });
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

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