testdriver-actions.js (19165B)
1 (function() { 2 let sourceNameIdx = 0; 3 4 /** 5 * @class 6 * Builder for creating a sequence of actions 7 * 8 * 9 * The actions are dispatched once 10 * :js:func:`test_driver.Actions.send` is called. This returns a 11 * promise which resolves once the actions are complete. 12 * 13 * The other methods on :js:class:`test_driver.Actions` object are 14 * used to build the sequence of actions that will be sent. These 15 * return the `Actions` object itself, so the actions sequence can 16 * be constructed by chaining method calls. 17 * 18 * Internally :js:func:`test_driver.Actions.send` invokes 19 * :js:func:`test_driver.action_sequence`. 20 * 21 * @example 22 * let text_box = document.getElementById("text"); 23 * 24 * let actions = new test_driver.Actions() 25 * .pointerMove(0, 0, {origin: text_box}) 26 * .pointerDown() 27 * .pointerUp() 28 * .addTick() 29 * .keyDown("p") 30 * .keyUp("p"); 31 * 32 * await actions.send(); 33 * 34 * @param {number} [defaultTickDuration] - The default duration of a 35 * tick. Be default this is set ot 16ms, which is one frame time 36 * based on 60Hz display. 37 */ 38 function Actions(defaultTickDuration=16) { 39 this.sourceTypes = new Map([["key", KeySource], 40 ["pointer", PointerSource], 41 ["wheel", WheelSource], 42 ["none", GeneralSource]]); 43 this.sources = new Map(); 44 this.sourceOrder = []; 45 for (let sourceType of this.sourceTypes.keys()) { 46 this.sources.set(sourceType, new Map()); 47 } 48 this.currentSources = new Map(); 49 for (let sourceType of this.sourceTypes.keys()) { 50 this.currentSources.set(sourceType, null); 51 } 52 this.createSource("none"); 53 this.tickIdx = 0; 54 this.defaultTickDuration = defaultTickDuration; 55 this.context = null; 56 } 57 58 Actions.prototype = { 59 ButtonType: { 60 LEFT: 0, 61 MIDDLE: 1, 62 RIGHT: 2, 63 BACK: 3, 64 FORWARD: 4, 65 }, 66 67 /** 68 * Generate the action sequence suitable for passing to 69 * test_driver.action_sequence 70 * 71 * @returns {Array} Array of WebDriver-compatible actions sequences 72 */ 73 serialize: function() { 74 let actions = []; 75 for (let [sourceType, sourceName] of this.sourceOrder) { 76 let source = this.sources.get(sourceType).get(sourceName); 77 let serialized = source.serialize(this.tickIdx + 1, this.defaultTickDuration); 78 if (serialized) { 79 serialized.id = sourceName; 80 actions.push(serialized); 81 } 82 } 83 return actions; 84 }, 85 86 /** 87 * Generate and send the action sequence 88 * 89 * @returns {Promise} fulfilled after the sequence is executed, 90 * rejected if any actions fail. 91 */ 92 send: function() { 93 let actions; 94 try { 95 actions = this.serialize(); 96 } catch(e) { 97 return Promise.reject(e); 98 } 99 return test_driver.action_sequence(actions, this.context); 100 }, 101 102 /** 103 * Set the context for the actions 104 * 105 * @param {WindowProxy} context - Context in which to run the action sequence 106 */ 107 setContext: function(context) { 108 this.context = context; 109 return this; 110 }, 111 112 /** 113 * Get the action source with a particular source type and name. 114 * If no name is passed, a new source with the given type is 115 * created. 116 * 117 * @param {String} type - Source type ('none', 'key', 'pointer', or 'wheel') 118 * @param {String?} name - Name of the source 119 * @returns {Source} Source object for that source. 120 */ 121 getSource: function(type, name) { 122 if (!this.sources.has(type)) { 123 throw new Error(`${type} is not a valid action type`); 124 } 125 if (name === null || name === undefined) { 126 name = this.currentSources.get(type); 127 } 128 if (name === null || name === undefined) { 129 return this.createSource(type, null); 130 } 131 return this.sources.get(type).get(name); 132 }, 133 134 setSource: function(type, name) { 135 if (!this.sources.has(type)) { 136 throw new Error(`${type} is not a valid action type`); 137 } 138 if (!this.sources.get(type).has(name)) { 139 throw new Error(`${name} is not a valid source for ${type}`); 140 } 141 this.currentSources.set(type, name); 142 return this; 143 }, 144 145 /** 146 * Add a new key input source with the given name 147 * 148 * @param {String} name - Name of the key source 149 * @param {Bool} set - Set source as the default key source 150 * @returns {Actions} 151 */ 152 addKeyboard: function(name, set=true) { 153 this.createSource("key", name); 154 if (set) { 155 this.setKeyboard(name); 156 } 157 return this; 158 }, 159 160 /** 161 * Set the current default key source 162 * 163 * @param {String} name - Name of the key source 164 * @returns {Actions} 165 */ 166 setKeyboard: function(name) { 167 this.setSource("key", name); 168 return this; 169 }, 170 171 /** 172 * Add a new pointer input source with the given name 173 * 174 * @param {String} type - Name of the pointer source 175 * @param {String} pointerType - Type of pointing device 176 * @param {Bool} set - Set source as the default pointer source 177 * @returns {Actions} 178 */ 179 addPointer: function(name, pointerType="mouse", set=true) { 180 this.createSource("pointer", name, {pointerType: pointerType}); 181 if (set) { 182 this.setPointer(name); 183 } 184 return this; 185 }, 186 187 /** 188 * Set the current default pointer source 189 * 190 * @param {String} name - Name of the pointer source 191 * @returns {Actions} 192 */ 193 setPointer: function(name) { 194 this.setSource("pointer", name); 195 return this; 196 }, 197 198 /** 199 * Add a new wheel input source with the given name 200 * 201 * @param {String} type - Name of the wheel source 202 * @param {Bool} set - Set source as the default wheel source 203 * @returns {Actions} 204 */ 205 addWheel: function(name, set=true) { 206 this.createSource("wheel", name); 207 if (set) { 208 this.setWheel(name); 209 } 210 return this; 211 }, 212 213 /** 214 * Set the current default wheel source 215 * 216 * @param {String} name - Name of the wheel source 217 * @returns {Actions} 218 */ 219 setWheel: function(name) { 220 this.setSource("wheel", name); 221 return this; 222 }, 223 224 createSource: function(type, name, parameters={}) { 225 if (!this.sources.has(type)) { 226 throw new Error(`${type} is not a valid action type`); 227 } 228 let sourceNames = new Set(); 229 for (let [_, name] of this.sourceOrder) { 230 sourceNames.add(name); 231 } 232 if (!name) { 233 do { 234 name = "" + sourceNameIdx++; 235 } while (sourceNames.has(name)) 236 } else { 237 if (sourceNames.has(name)) { 238 throw new Error(`Alreay have a source of type ${type} named ${name}.`); 239 } 240 } 241 this.sources.get(type).set(name, new (this.sourceTypes.get(type))(parameters)); 242 this.currentSources.set(type, name); 243 this.sourceOrder.push([type, name]); 244 return this.sources.get(type).get(name); 245 }, 246 247 /** 248 * Insert a new actions tick 249 * 250 * @param {Number?} duration - Minimum length of the tick in ms. 251 * @returns {Actions} 252 */ 253 addTick: function(duration) { 254 this.tickIdx += 1; 255 if (duration) { 256 this.pause(duration); 257 } 258 return this; 259 }, 260 261 /** 262 * Add a pause to the current tick 263 * 264 * @param {Number?} duration - Minimum length of the tick in ms. 265 * @param {String} sourceType - source type 266 * @param {String?} sourceName - Named key, pointer or wheel source to use 267 * or null for the default key, pointer or 268 * wheel source 269 * @returns {Actions} 270 */ 271 pause: function(duration=0, sourceType="none", {sourceName=null}={}) { 272 if (sourceType=="none") 273 this.getSource("none").addPause(this, duration); 274 else 275 this.getSource(sourceType, sourceName).addPause(this, duration); 276 return this; 277 }, 278 279 /** 280 * Create a keyDown event for the current default key source 281 * 282 * To send special keys, send the respective key's codepoint, 283 * as defined by `WebDriver 284 * <https://w3c.github.io/webdriver/#keyboard-actions>`_. 285 * 286 * @param {String} key - Key to press 287 * @param {String?} sourceName - Named key source to use or null for the default key source 288 * @returns {Actions} 289 */ 290 keyDown: function(key, {sourceName=null}={}) { 291 let source = this.getSource("key", sourceName); 292 source.keyDown(this, key); 293 return this; 294 }, 295 296 /** 297 * Create a keyUp event for the current default key source 298 * 299 * To send special keys, send the respective key's codepoint, 300 * as defined by `WebDriver 301 * <https://w3c.github.io/webdriver/#keyboard-actions>`_. 302 * 303 * @param {String} key - Key to release 304 * @param {String?} sourceName - Named key source to use or null for the default key source 305 * @returns {Actions} 306 */ 307 keyUp: function(key, {sourceName=null}={}) { 308 let source = this.getSource("key", sourceName); 309 source.keyUp(this, key); 310 return this; 311 }, 312 313 /** 314 * Create a pointerDown event for the current default pointer source 315 * 316 * @param {String} button - Button to press 317 * @param {String?} sourceName - Named pointer source to use or null for the default 318 * pointer source 319 * @returns {Actions} 320 */ 321 pointerDown: function({button=this.ButtonType.LEFT, sourceName=null, 322 width, height, pressure, tangentialPressure, 323 tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) { 324 let source = this.getSource("pointer", sourceName); 325 source.pointerDown(this, button, width, height, pressure, tangentialPressure, 326 tiltX, tiltY, twist, altitudeAngle, azimuthAngle); 327 return this; 328 }, 329 330 /** 331 * Create a pointerUp event for the current default pointer source 332 * 333 * @param {String} button - Button to release 334 * @param {String?} sourceName - Named pointer source to use or null for the default pointer 335 * source 336 * @returns {Actions} 337 */ 338 pointerUp: function({button=this.ButtonType.LEFT, sourceName=null}={}) { 339 let source = this.getSource("pointer", sourceName); 340 source.pointerUp(this, button); 341 return this; 342 }, 343 344 /** 345 * Create a move event for the current default pointer source 346 * 347 * @param {Number} x - Destination x coordinate 348 * @param {Number} y - Destination y coordinate 349 * @param {String|Element} origin - Origin of the coordinate system. 350 * Either "pointer", "viewport" or an Element 351 * @param {Number?} duration - Time in ms for the move 352 * @param {String?} sourceName - Named pointer source to use or null for the default pointer 353 * source 354 * @returns {Actions} 355 */ 356 pointerMove: function(x, y, 357 {origin="viewport", duration, sourceName=null, 358 width, height, pressure, tangentialPressure, 359 tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) { 360 let source = this.getSource("pointer", sourceName); 361 source.pointerMove(this, x, y, duration, origin, width, height, pressure, 362 tangentialPressure, tiltX, tiltY, twist, altitudeAngle, 363 azimuthAngle); 364 return this; 365 }, 366 367 /** 368 * Create a scroll event for the current default wheel source 369 * 370 * @param {Number} x - mouse cursor x coordinate 371 * @param {Number} y - mouse cursor y coordinate 372 * @param {Number} deltaX - scroll delta value along the x-axis in pixels 373 * @param {Number} deltaY - scroll delta value along the y-axis in pixels 374 * @param {String|Element} origin - Origin of the coordinate system. 375 * Either "viewport" or an Element 376 * @param {Number?} duration - Time in ms for the scroll 377 * @param {String?} sourceName - Named wheel source to use or null for the 378 * default wheel source 379 * @returns {Actions} 380 */ 381 scroll: function(x, y, deltaX, deltaY, 382 {origin="viewport", duration, sourceName=null}={}) { 383 let source = this.getSource("wheel", sourceName); 384 source.scroll(this, x, y, deltaX, deltaY, duration, origin); 385 return this; 386 }, 387 }; 388 389 function GeneralSource() { 390 this.actions = new Map(); 391 } 392 393 GeneralSource.prototype = { 394 serialize: function(tickCount, defaultTickDuration) { 395 let actions = []; 396 let data = {"type": "none", "actions": actions}; 397 for (let i=0; i<tickCount; i++) { 398 if (this.actions.has(i)) { 399 actions.push(this.actions.get(i)); 400 } else { 401 actions.push({"type": "pause", duration: defaultTickDuration}); 402 } 403 } 404 return data; 405 }, 406 407 addPause: function(actions, duration) { 408 let tick = actions.tickIdx; 409 if (this.actions.has(tick)) { 410 throw new Error(`Already have a pause action for the current tick`); 411 } 412 this.actions.set(tick, {type: "pause", duration: duration}); 413 }, 414 }; 415 416 function KeySource() { 417 this.actions = new Map(); 418 } 419 420 KeySource.prototype = { 421 serialize: function(tickCount) { 422 if (!this.actions.size) { 423 return undefined; 424 } 425 let actions = []; 426 let data = {"type": "key", "actions": actions}; 427 for (let i=0; i<tickCount; i++) { 428 if (this.actions.has(i)) { 429 actions.push(this.actions.get(i)); 430 } else { 431 actions.push({"type": "pause"}); 432 } 433 } 434 return data; 435 }, 436 437 keyDown: function(actions, key) { 438 let tick = actions.tickIdx; 439 if (this.actions.has(tick)) { 440 tick = actions.addTick().tickIdx; 441 } 442 this.actions.set(tick, {type: "keyDown", value: key}); 443 }, 444 445 keyUp: function(actions, key) { 446 let tick = actions.tickIdx; 447 if (this.actions.has(tick)) { 448 tick = actions.addTick().tickIdx; 449 } 450 this.actions.set(tick, {type: "keyUp", value: key}); 451 }, 452 453 addPause: function(actions, duration) { 454 let tick = actions.tickIdx; 455 if (this.actions.has(tick)) { 456 tick = actions.addTick().tickIdx; 457 } 458 this.actions.set(tick, {type: "pause", duration: duration}); 459 }, 460 }; 461 462 function PointerSource(parameters={pointerType: "mouse"}) { 463 let pointerType = parameters.pointerType || "mouse"; 464 if (!["mouse", "pen", "touch"].includes(pointerType)) { 465 throw new Error(`Invalid pointerType ${pointerType}`); 466 } 467 this.type = pointerType; 468 this.actions = new Map(); 469 } 470 471 function setPointerProperties(action, width, height, pressure, tangentialPressure, 472 tiltX, tiltY, twist, altitudeAngle, azimuthAngle) { 473 if (width) { 474 action.width = width; 475 } 476 if (height) { 477 action.height = height; 478 } 479 if (pressure) { 480 action.pressure = pressure; 481 } 482 if (tangentialPressure) { 483 action.tangentialPressure = tangentialPressure; 484 } 485 if (tiltX) { 486 action.tiltX = tiltX; 487 } 488 if (tiltY) { 489 action.tiltY = tiltY; 490 } 491 if (twist) { 492 action.twist = twist; 493 } 494 if (altitudeAngle) { 495 action.altitudeAngle = altitudeAngle; 496 } 497 if (azimuthAngle) { 498 action.azimuthAngle = azimuthAngle; 499 } 500 return action; 501 } 502 503 PointerSource.prototype = { 504 serialize: function(tickCount) { 505 if (!this.actions.size) { 506 return undefined; 507 } 508 let actions = []; 509 let data = {"type": "pointer", "actions": actions, "parameters": {"pointerType": this.type}}; 510 for (let i=0; i<tickCount; i++) { 511 if (this.actions.has(i)) { 512 actions.push(this.actions.get(i)); 513 } else { 514 actions.push({"type": "pause"}); 515 } 516 } 517 return data; 518 }, 519 520 pointerDown: function(actions, button, width, height, pressure, tangentialPressure, 521 tiltX, tiltY, twist, altitudeAngle, azimuthAngle) { 522 let tick = actions.tickIdx; 523 if (this.actions.has(tick)) { 524 tick = actions.addTick().tickIdx; 525 } 526 let actionProperties = setPointerProperties({type: "pointerDown", button}, width, height, 527 pressure, tangentialPressure, tiltX, tiltY, 528 twist, altitudeAngle, azimuthAngle); 529 this.actions.set(tick, actionProperties); 530 }, 531 532 pointerUp: function(actions, button) { 533 let tick = actions.tickIdx; 534 if (this.actions.has(tick)) { 535 tick = actions.addTick().tickIdx; 536 } 537 this.actions.set(tick, {type: "pointerUp", button}); 538 }, 539 540 pointerMove: function(actions, x, y, duration, origin, width, height, pressure, 541 tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle) { 542 let tick = actions.tickIdx; 543 if (this.actions.has(tick)) { 544 tick = actions.addTick().tickIdx; 545 } 546 let moveAction = {type: "pointerMove", x, y, origin}; 547 if (duration) { 548 moveAction.duration = duration; 549 } 550 let actionProperties = setPointerProperties(moveAction, width, height, pressure, 551 tangentialPressure, tiltX, tiltY, twist, 552 altitudeAngle, azimuthAngle); 553 this.actions.set(tick, actionProperties); 554 }, 555 556 addPause: function(actions, duration) { 557 let tick = actions.tickIdx; 558 if (this.actions.has(tick)) { 559 tick = actions.addTick().tickIdx; 560 } 561 this.actions.set(tick, {type: "pause", duration: duration}); 562 }, 563 }; 564 565 function WheelSource() { 566 this.actions = new Map(); 567 } 568 569 WheelSource.prototype = { 570 serialize: function(tickCount) { 571 if (!this.actions.size) { 572 return undefined; 573 } 574 let actions = []; 575 let data = {"type": "wheel", "actions": actions}; 576 for (let i=0; i<tickCount; i++) { 577 if (this.actions.has(i)) { 578 actions.push(this.actions.get(i)); 579 } else { 580 actions.push({"type": "pause"}); 581 } 582 } 583 return data; 584 }, 585 586 scroll: function(actions, x, y, deltaX, deltaY, duration, origin) { 587 let tick = actions.tickIdx; 588 if (this.actions.has(tick)) { 589 tick = actions.addTick().tickIdx; 590 } 591 this.actions.set(tick, {type: "scroll", x, y, deltaX, deltaY, origin}); 592 if (duration) { 593 this.actions.get(tick).duration = duration; 594 } 595 }, 596 597 addPause: function(actions, duration) { 598 let tick = actions.tickIdx; 599 if (this.actions.has(tick)) { 600 tick = actions.addTick().tickIdx; 601 } 602 this.actions.set(tick, {type: "pause", duration: duration}); 603 }, 604 }; 605 606 test_driver.Actions = Actions; 607 })();