test_pointer.dart 20.2 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 334 335 336 337 338 339 340 341 342
  /// 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,
    );
  }

343 344 345 346 347 348 349 350 351 352
  /// 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);
353
    assert(kind == PointerDeviceKind.trackpad);
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
    _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);
380
    assert(kind == PointerDeviceKind.trackpad);
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
    _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);
405
    assert(kind == PointerDeviceKind.trackpad);
406 407 408 409 410 411 412 413 414
    _isPanZoomActive = false;
    _pan = null;
    return PointerPanZoomEndEvent(
      timeStamp: timeStamp,
      device: _device,
      pointer: pointer,
      position: location!,
    );
  }
415 416
}

417
/// Signature for a callback that can dispatch events and returns a future that
418
/// completes when the event dispatch is complete.
419
typedef EventDispatcher = Future<void> Function(PointerEvent event);
420 421

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

424 425 426 427 428
/// A class for performing gestures in tests.
///
/// The simplest way to create a [TestGesture] is to call
/// [WidgetTester.startGesture].
class TestGesture {
429 430
  /// Create a [TestGesture] without dispatching any events from it.
  /// The [TestGesture] can then be manipulated to perform future actions.
431
  ///
432
  /// By default, the pointer identifier used is 1. This can be overridden by
433 434
  /// providing the `pointer` argument.
  ///
435 436
  /// 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
437
  /// via the `dispatcher` argument.
438
  ///
439 440 441 442
  /// 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".
443
  ///
444 445 446
  /// None of the arguments may be null. The `dispatcher` and `hitTester`
  /// arguments are required.
  TestGesture({
447
    required EventDispatcher dispatcher,
448 449
    int pointer = 1,
    PointerDeviceKind kind = PointerDeviceKind.touch,
450
    int? device,
451
    int buttons = kPrimaryButton,
452
  }) : _dispatcher = dispatcher,
453
       _pointer = TestPointer(pointer, kind, device, buttons);
454 455 456

  /// Dispatch a pointer down event at the given `downLocation`, caching the
  /// hit test result.
457
  Future<void> down(Offset downLocation, { Duration timeStamp = Duration.zero }) async {
458
    return TestAsyncUtils.guard<void>(() async {
459
      return _dispatcher(_pointer.down(downLocation, timeStamp: timeStamp));
460 461 462
    });
  }

463 464
  /// Dispatch a pointer down event at the given `downLocation`, caching the
  /// hit test result with a custom down event.
465 466
  Future<void> downWithCustomEvent(Offset downLocation, PointerDownEvent event) async {
    _pointer.setDownInfo(event, downLocation);
467
    return TestAsyncUtils.guard<void>(() async {
468
      return _dispatcher(event);
469 470 471
    });
  }

472
  final EventDispatcher _dispatcher;
473 474
  final TestPointer _pointer;

475 476
  /// In a test, send a move event that moves the pointer by the given offset.
  @visibleForTesting
477
  Future<void> updateWithCustomEvent(PointerEvent event, { Duration timeStamp = Duration.zero }) {
478 479
    _pointer.setDownInfo(event, event.position);
    return TestAsyncUtils.guard<void>(() {
480
      return _dispatcher(event);
481 482 483
    });
  }

484
  /// In a test, send a pointer add event for this pointer.
485
  Future<void> addPointer({ Duration timeStamp = Duration.zero, Offset? location }) {
486
    return TestAsyncUtils.guard<void>(() {
487
      return _dispatcher(_pointer.addPointer(timeStamp: timeStamp, location: location ?? _pointer.location));
488 489 490 491
    });
  }

  /// In a test, send a pointer remove event for this pointer.
492
  Future<void> removePointer({ Duration timeStamp = Duration.zero, Offset? location }) {
493
    return TestAsyncUtils.guard<void>(() {
494
      return _dispatcher(_pointer.removePointer(timeStamp: timeStamp, location: location ?? _pointer.location));
495 496 497
    });
  }

498
  /// Send a move event moving the pointer by the given offset.
499 500
  ///
  /// If the pointer is down, then a move event is dispatched. If the pointer is
501
  /// up, then a hover event is dispatched.
502 503 504 505 506 507
  ///
  /// 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.
508
  Future<void> moveBy(Offset offset, { Duration timeStamp = Duration.zero }) {
509 510
    assert(_pointer.location != null);
    return moveTo(_pointer.location! + offset, timeStamp: timeStamp);
511 512 513
  }

  /// Send a move event moving the pointer to the given location.
514 515
  ///
  /// If the pointer is down, then a move event is dispatched. If the pointer is
516
  /// up, then a hover event is dispatched.
517 518 519 520 521 522
  ///
  /// 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.
523
  Future<void> moveTo(Offset location, { Duration timeStamp = Duration.zero }) {
524
    return TestAsyncUtils.guard<void>(() {
525
      if (_pointer._isDown) {
526
        return _dispatcher(_pointer.move(location, timeStamp: timeStamp));
527
      } else {
528
        return _dispatcher(_pointer.hover(location, timeStamp: timeStamp));
529
      }
530
    });
531 532 533
  }

  /// End the gesture by releasing the pointer.
534
  Future<void> up({ Duration timeStamp = Duration.zero }) {
535
    return TestAsyncUtils.guard<void>(() async {
536
      assert(_pointer._isDown);
537
      await _dispatcher(_pointer.up(timeStamp: timeStamp));
538 539
      assert(!_pointer._isDown);
    });
540
  }
541

542 543 544
  /// 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).
545
  Future<void> cancel({ Duration timeStamp = Duration.zero }) {
546
    return TestAsyncUtils.guard<void>(() async {
547
      assert(_pointer._isDown);
548
      await _dispatcher(_pointer.cancel(timeStamp: timeStamp));
549 550
      assert(!_pointer._isDown);
    });
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

  /// 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 {
    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 {
    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 {
    return TestAsyncUtils.guard<void>(() async {
      return _dispatcher(_pointer.panZoomEnd(
        timeStamp: timeStamp
      ));
    });
  }
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

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