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 * @param {String} key - Key to press 283 * @param {String?} sourceName - Named key source to use or null for the default key source 284 * @returns {Actions} 285 */ 286 keyDown: function(key, {sourceName=null}={}) { 287 let source = this.getSource("key", sourceName); 288 source.keyDown(this, key); 289 return this; 290 }, 291 292 /** 293 * Create a keyDown event for the current default key source 294 * 295 * @param {String} key - Key to release 296 * @param {String?} sourceName - Named key source to use or null for the default key source 297 * @returns {Actions} 298 */ 299 keyUp: function(key, {sourceName=null}={}) { 300 let source = this.getSource("key", sourceName); 301 source.keyUp(this, key); 302 return this; 303 }, 304 305 /** 306 * Create a pointerDown event for the current default pointer source 307 * 308 * @param {String} button - Button to press 309 * @param {String?} sourceName - Named pointer source to use or null for the default 310 * pointer source 311 * @returns {Actions} 312 */ 313 pointerDown: function({button=this.ButtonType.LEFT, sourceName=null, 314 width, height, pressure, tangentialPressure, 315 tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) { 316 let source = this.getSource("pointer", sourceName); 317 source.pointerDown(this, button, width, height, pressure, tangentialPressure, 318 tiltX, tiltY, twist, altitudeAngle, azimuthAngle); 319 return this; 320 }, 321 322 /** 323 * Create a pointerUp event for the current default pointer source 324 * 325 * @param {String} button - Button to release 326 * @param {String?} sourceName - Named pointer source to use or null for the default pointer 327 * source 328 * @returns {Actions} 329 */ 330 pointerUp: function({button=this.ButtonType.LEFT, sourceName=null}={}) { 331 let source = this.getSource("pointer", sourceName); 332 source.pointerUp(this, button); 333 return this; 334 }, 335 336 /** 337 * Create a move event for the current default pointer source 338 * 339 * @param {Number} x - Destination x coordinate 340 * @param {Number} y - Destination y coordinate 341 * @param {String|Element} origin - Origin of the coordinate system. 342 * Either "pointer", "viewport" or an Element 343 * @param {Number?} duration - Time in ms for the move 344 * @param {String?} sourceName - Named pointer source to use or null for the default pointer 345 * source 346 * @returns {Actions} 347 */ 348 pointerMove: function(x, y, 349 {origin="viewport", duration, sourceName=null, 350 width, height, pressure, tangentialPressure, 351 tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) { 352 let source = this.getSource("pointer", sourceName); 353 source.pointerMove(this, x, y, duration, origin, width, height, pressure, 354 tangentialPressure, tiltX, tiltY, twist, altitudeAngle, 355 azimuthAngle); 356 return this; 357 }, 358 359 /** 360 * Create a scroll event for the current default wheel source 361 * 362 * @param {Number} x - mouse cursor x coordinate 363 * @param {Number} y - mouse cursor y coordinate 364 * @param {Number} deltaX - scroll delta value along the x-axis in pixels 365 * @param {Number} deltaY - scroll delta value along the y-axis in pixels 366 * @param {String|Element} origin - Origin of the coordinate system. 367 * Either "viewport" or an Element 368 * @param {Number?} duration - Time in ms for the scroll 369 * @param {String?} sourceName - Named wheel source to use or null for the 370 * default wheel source 371 * @returns {Actions} 372 */ 373 scroll: function(x, y, deltaX, deltaY, 374 {origin="viewport", duration, sourceName=null}={}) { 375 let source = this.getSource("wheel", sourceName); 376 source.scroll(this, x, y, deltaX, deltaY, duration, origin); 377 return this; 378 }, 379 }; 380 381 function GeneralSource() { 382 this.actions = new Map(); 383 } 384 385 GeneralSource.prototype = { 386 serialize: function(tickCount, defaultTickDuration) { 387 let actions = []; 388 let data = {"type": "none", "actions": actions}; 389 for (let i=0; i<tickCount; i++) { 390 if (this.actions.has(i)) { 391 actions.push(this.actions.get(i)); 392 } else { 393 actions.push({"type": "pause", duration: defaultTickDuration}); 394 } 395 } 396 return data; 397 }, 398 399 addPause: function(actions, duration) { 400 let tick = actions.tickIdx; 401 if (this.actions.has(tick)) { 402 throw new Error(`Already have a pause action for the current tick`); 403 } 404 this.actions.set(tick, {type: "pause", duration: duration}); 405 }, 406 }; 407 408 function KeySource() { 409 this.actions = new Map(); 410 } 411 412 KeySource.prototype = { 413 serialize: function(tickCount) { 414 if (!this.actions.size) { 415 return undefined; 416 } 417 let actions = []; 418 let data = {"type": "key", "actions": actions}; 419 for (let i=0; i<tickCount; i++) { 420 if (this.actions.has(i)) { 421 actions.push(this.actions.get(i)); 422 } else { 423 actions.push({"type": "pause"}); 424 } 425 } 426 return data; 427 }, 428 429 keyDown: function(actions, key) { 430 let tick = actions.tickIdx; 431 if (this.actions.has(tick)) { 432 tick = actions.addTick().tickIdx; 433 } 434 this.actions.set(tick, {type: "keyDown", value: key}); 435 }, 436 437 keyUp: 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: "keyUp", value: key}); 443 }, 444 445 addPause: function(actions, duration) { 446 let tick = actions.tickIdx; 447 if (this.actions.has(tick)) { 448 tick = actions.addTick().tickIdx; 449 } 450 this.actions.set(tick, {type: "pause", duration: duration}); 451 }, 452 }; 453 454 function PointerSource(parameters={pointerType: "mouse"}) { 455 let pointerType = parameters.pointerType || "mouse"; 456 if (!["mouse", "pen", "touch"].includes(pointerType)) { 457 throw new Error(`Invalid pointerType ${pointerType}`); 458 } 459 this.type = pointerType; 460 this.actions = new Map(); 461 } 462 463 function setPointerProperties(action, width, height, pressure, tangentialPressure, 464 tiltX, tiltY, twist, altitudeAngle, azimuthAngle) { 465 if (width) { 466 action.width = width; 467 } 468 if (height) { 469 action.height = height; 470 } 471 if (pressure) { 472 action.pressure = pressure; 473 } 474 if (tangentialPressure) { 475 action.tangentialPressure = tangentialPressure; 476 } 477 if (tiltX) { 478 action.tiltX = tiltX; 479 } 480 if (tiltY) { 481 action.tiltY = tiltY; 482 } 483 if (twist) { 484 action.twist = twist; 485 } 486 if (altitudeAngle) { 487 action.altitudeAngle = altitudeAngle; 488 } 489 if (azimuthAngle) { 490 action.azimuthAngle = azimuthAngle; 491 } 492 return action; 493 } 494 495 PointerSource.prototype = { 496 serialize: function(tickCount) { 497 if (!this.actions.size) { 498 return undefined; 499 } 500 let actions = []; 501 let data = {"type": "pointer", "actions": actions, "parameters": {"pointerType": this.type}}; 502 for (let i=0; i<tickCount; i++) { 503 if (this.actions.has(i)) { 504 actions.push(this.actions.get(i)); 505 } else { 506 actions.push({"type": "pause"}); 507 } 508 } 509 return data; 510 }, 511 512 pointerDown: function(actions, button, width, height, pressure, tangentialPressure, 513 tiltX, tiltY, twist, altitudeAngle, azimuthAngle) { 514 let tick = actions.tickIdx; 515 if (this.actions.has(tick)) { 516 tick = actions.addTick().tickIdx; 517 } 518 let actionProperties = setPointerProperties({type: "pointerDown", button}, width, height, 519 pressure, tangentialPressure, tiltX, tiltY, 520 twist, altitudeAngle, azimuthAngle); 521 this.actions.set(tick, actionProperties); 522 }, 523 524 pointerUp: function(actions, button) { 525 let tick = actions.tickIdx; 526 if (this.actions.has(tick)) { 527 tick = actions.addTick().tickIdx; 528 } 529 this.actions.set(tick, {type: "pointerUp", button}); 530 }, 531 532 pointerMove: function(actions, x, y, duration, origin, width, height, pressure, 533 tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle) { 534 let tick = actions.tickIdx; 535 if (this.actions.has(tick)) { 536 tick = actions.addTick().tickIdx; 537 } 538 let moveAction = {type: "pointerMove", x, y, origin}; 539 if (duration) { 540 moveAction.duration = duration; 541 } 542 let actionProperties = setPointerProperties(moveAction, width, height, pressure, 543 tangentialPressure, tiltX, tiltY, twist, 544 altitudeAngle, azimuthAngle); 545 this.actions.set(tick, actionProperties); 546 }, 547 548 addPause: function(actions, duration) { 549 let tick = actions.tickIdx; 550 if (this.actions.has(tick)) { 551 tick = actions.addTick().tickIdx; 552 } 553 this.actions.set(tick, {type: "pause", duration: duration}); 554 }, 555 }; 556 557 function WheelSource() { 558 this.actions = new Map(); 559 } 560 561 WheelSource.prototype = { 562 serialize: function(tickCount) { 563 if (!this.actions.size) { 564 return undefined; 565 } 566 let actions = []; 567 let data = {"type": "wheel", "actions": actions}; 568 for (let i=0; i<tickCount; i++) { 569 if (this.actions.has(i)) { 570 actions.push(this.actions.get(i)); 571 } else { 572 actions.push({"type": "pause"}); 573 } 574 } 575 return data; 576 }, 577 578 scroll: function(actions, x, y, deltaX, deltaY, duration, origin) { 579 let tick = actions.tickIdx; 580 if (this.actions.has(tick)) { 581 tick = actions.addTick().tickIdx; 582 } 583 this.actions.set(tick, {type: "scroll", x, y, deltaX, deltaY, origin}); 584 if (duration) { 585 this.actions.get(tick).duration = duration; 586 } 587 }, 588 589 addPause: function(actions, duration) { 590 let tick = actions.tickIdx; 591 if (this.actions.has(tick)) { 592 tick = actions.addTick().tickIdx; 593 } 594 this.actions.set(tick, {type: "pause", duration: duration}); 595 }, 596 }; 597 598 test_driver.Actions = Actions; 599})(); 600