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