1(function() { 2 let sourceNameIdx = 0; 3 4 /** 5 * Builder for creating a sequence of actions 6 */ 7 function Actions() { 8 this.sourceTypes = new Map([["key", KeySource], 9 ["pointer", PointerSource], 10 ["none", GeneralSource]]); 11 this.sources = new Map(); 12 this.sourceOrder = []; 13 for (let sourceType of this.sourceTypes.keys()) { 14 this.sources.set(sourceType, new Map()); 15 } 16 this.currentSources = new Map(); 17 for (let sourceType of this.sourceTypes.keys()) { 18 this.currentSources.set(sourceType, null); 19 } 20 this.createSource("none"); 21 this.tickIdx = 0; 22 } 23 24 Actions.prototype = { 25 ButtonType: { 26 LEFT: 0, 27 MIDDLE: 1, 28 RIGHT: 2, 29 BACK: 3, 30 FORWARD: 4, 31 }, 32 33 /** 34 * Generate the action sequence suitable for passing to 35 * test_driver.action_sequence 36 * 37 * @returns {Array} Array of WebDriver-compatible actions sequences 38 */ 39 serialize: function() { 40 let actions = []; 41 for (let [sourceType, sourceName] of this.sourceOrder) { 42 let source = this.sources.get(sourceType).get(sourceName); 43 let serialized = source.serialize(this.tickIdx + 1); 44 if (serialized) { 45 serialized.id = sourceName; 46 actions.push(serialized); 47 } 48 } 49 return actions; 50 }, 51 52 /** 53 * Generate and send the action sequence 54 * 55 * @returns {Promise} fulfilled after the sequence is executed, 56 * rejected if any actions fail. 57 */ 58 send: function() { 59 let actions; 60 try { 61 actions = this.serialize(); 62 } catch(e) { 63 return Promise.reject(e); 64 } 65 return test_driver.action_sequence(actions); 66 }, 67 68 /** 69 * Get the action source with a particular source type and name. 70 * If no name is passed, a new source with the given type is 71 * created. 72 * 73 * @param {String} type - Source type ('none', 'key', or 'pointer') 74 * @param {String?} name - Name of the source 75 * @returns {Source} Source object for that source. 76 */ 77 getSource: function(type, name) { 78 if (!this.sources.has(type)) { 79 throw new Error(`${type} is not a valid action type`); 80 } 81 if (name === null || name === undefined) { 82 name = this.currentSources.get(type); 83 } 84 if (name === null || name === undefined) { 85 return this.createSource(type, null); 86 } 87 return this.sources.get(type).get(name); 88 }, 89 90 setSource: function(type, name) { 91 if (!this.sources.has(type)) { 92 throw new Error(`${type} is not a valid action type`); 93 } 94 if (!this.sources.get(type).has(name)) { 95 throw new Error(`${name} is not a valid source for ${type}`); 96 } 97 this.currentSources.set(type, name); 98 return this; 99 }, 100 101 /** 102 * Add a new key input source with the given name 103 * 104 * @param {String} name - Name of the key source 105 * @param {Bool} set - Set source as the default key source 106 * @returns {Actions} 107 */ 108 addKeyboard: function(name, set=true) { 109 this.createSource("key", name); 110 if (set) { 111 this.setKeyboard(name); 112 } 113 return this; 114 }, 115 116 /** 117 * Set the current default key source 118 * 119 * @param {String} name - Name of the key source 120 * @returns {Actions} 121 */ 122 setKeyboard: function(name) { 123 this.setSource("key", name); 124 return this; 125 }, 126 127 /** 128 * Add a new pointer input source with the given name 129 * 130 * @param {String} type - Name of the key source 131 * @param {String} pointerType - Type of pointing device 132 * @param {Bool} set - Set source as the default key source 133 * @returns {Actions} 134 */ 135 addPointer: function(name, pointerType="mouse", set=true) { 136 this.createSource("pointer", name, {pointerType: pointerType}); 137 if (set) { 138 this.setPointer(name); 139 } 140 return this; 141 }, 142 143 /** 144 * Set the current default pointer source 145 * 146 * @param {String} name - Name of the pointer source 147 * @returns {Actions} 148 */ 149 setPointer: function(name) { 150 this.setSource("pointer", name); 151 return this; 152 }, 153 154 createSource: function(type, name, parameters={}) { 155 if (!this.sources.has(type)) { 156 throw new Error(`${type} is not a valid action type`); 157 } 158 let sourceNames = new Set(); 159 for (let [_, name] of this.sourceOrder) { 160 sourceNames.add(name); 161 } 162 if (!name) { 163 do { 164 name = "" + sourceNameIdx++; 165 } while (sourceNames.has(name)) 166 } else { 167 if (sourceNames.has(name)) { 168 throw new Error(`Alreay have a source of type ${type} named ${name}.`); 169 } 170 } 171 this.sources.get(type).set(name, new (this.sourceTypes.get(type))(parameters)); 172 this.currentSources.set(type, name); 173 this.sourceOrder.push([type, name]); 174 return this.sources.get(type).get(name); 175 }, 176 177 /** 178 * Insert a new actions tick 179 * 180 * @param {Number?} duration - Minimum length of the tick in ms. 181 * @returns {Actions} 182 */ 183 addTick: function(duration) { 184 this.tickIdx += 1; 185 if (duration) { 186 this.pause(duration); 187 } 188 return this; 189 }, 190 191 /** 192 * Add a pause to the current tick 193 * 194 * @param {Number?} duration - Minimum length of the tick in ms. 195 * @returns {Actions} 196 */ 197 pause: function(duration) { 198 this.getSource("none").addPause(this, duration); 199 return this; 200 }, 201 202 /** 203 * Create a keyDown event for the current default key source 204 * 205 * @param {String} key - Key to press 206 * @param {String?} sourceName - Named key source to use or null for the default key source 207 * @returns {Actions} 208 */ 209 keyDown: function(key, {sourceName=null}={}) { 210 let source = this.getSource("key", sourceName); 211 source.keyDown(this, key); 212 return this; 213 }, 214 215 /** 216 * Create a keyDown event for the current default key source 217 * 218 * @param {String} key - Key to release 219 * @param {String?} sourceName - Named key source to use or null for the default key source 220 * @returns {Actions} 221 */ 222 keyUp: function(key, {sourceName=null}={}) { 223 let source = this.getSource("key", sourceName); 224 source.keyUp(this, key); 225 return this; 226 }, 227 228 /** 229 * Create a pointerDown event for the current default pointer source 230 * 231 * @param {String} button - Button to press 232 * @param {String?} sourceName - Named pointer source to use or null for the default 233 * pointer source 234 * @returns {Actions} 235 */ 236 pointerDown: function({button=this.ButtonType.LEFT, sourceName=null}={}) { 237 let source = this.getSource("pointer", sourceName); 238 source.pointerDown(this, button); 239 return this; 240 }, 241 242 /** 243 * Create a pointerUp event for the current default pointer source 244 * 245 * @param {String} button - Button to release 246 * @param {String?} sourceName - Named pointer source to use or null for the default pointer 247 * source 248 * @returns {Actions} 249 */ 250 pointerUp: function({button=this.ButtonType.LEFT, sourceName=null}={}) { 251 let source = this.getSource("pointer", sourceName); 252 source.pointerUp(this, button); 253 return this; 254 }, 255 256 /** 257 * Create a move event for the current default pointer source 258 * 259 * @param {Number} x - Destination x coordinate 260 * @param {Number} y - Destination y coordinate 261 * @param {String|Element} origin - Origin of the coordinate system. 262 * Either "pointer", "viewport" or an Element 263 * @param {Number?} duration - Time in ms for the move 264 * @param {String?} sourceName - Named pointer source to use or null for the default pointer 265 * source 266 * @returns {Actions} 267 */ 268 pointerMove: function(x, y, 269 {origin="viewport", duration, sourceName=null}={}) { 270 let source = this.getSource("pointer", sourceName); 271 source.pointerMove(this, x, y, duration, origin); 272 return this; 273 }, 274 }; 275 276 function GeneralSource() { 277 this.actions = new Map(); 278 } 279 280 GeneralSource.prototype = { 281 serialize: function(tickCount) { 282 if (!this.actions.size) { 283 return undefined; 284 } 285 let actions = []; 286 let data = {"type": "none", "actions": actions}; 287 for (let i=0; i<tickCount; i++) { 288 if (this.actions.has(i)) { 289 actions.push(this.actions.get(i)); 290 } else { 291 actions.push({"type": "pause"}); 292 } 293 } 294 return data; 295 }, 296 297 addPause: function(actions, duration) { 298 let tick = actions.tickIdx; 299 if (this.actions.has(tick)) { 300 throw new Error(`Already have a pause action for the current tick`); 301 } 302 this.actions.set(tick, {type: "pause", duration: duration}); 303 }, 304 }; 305 306 function KeySource() { 307 this.actions = new Map(); 308 } 309 310 KeySource.prototype = { 311 serialize: function(tickCount) { 312 if (!this.actions.size) { 313 return undefined; 314 } 315 let actions = []; 316 let data = {"type": "key", "actions": actions}; 317 for (let i=0; i<tickCount; i++) { 318 if (this.actions.has(i)) { 319 actions.push(this.actions.get(i)); 320 } else { 321 actions.push({"type": "pause"}); 322 } 323 } 324 return data; 325 }, 326 327 keyDown: function(actions, key) { 328 let tick = actions.tickIdx; 329 if (this.actions.has(tick)) { 330 tick = actions.addTick().tickIdx; 331 } 332 this.actions.set(tick, {type: "keyDown", value: key}); 333 }, 334 335 keyUp: function(actions, key) { 336 let tick = actions.tickIdx; 337 if (this.actions.has(tick)) { 338 tick = actions.addTick().tickIdx; 339 } 340 this.actions.set(tick, {type: "keyUp", value: key}); 341 }, 342 }; 343 344 function PointerSource(parameters={pointerType: "mouse"}) { 345 let pointerType = parameters.pointerType || "mouse"; 346 if (!["mouse", "pen", "touch"].includes(pointerType)) { 347 throw new Error(`Invalid pointerType ${pointerType}`); 348 } 349 this.type = pointerType; 350 this.actions = new Map(); 351 } 352 353 PointerSource.prototype = { 354 serialize: function(tickCount) { 355 if (!this.actions.size) { 356 return undefined; 357 } 358 let actions = []; 359 let data = {"type": "pointer", "actions": actions, "parameters": {"pointerType": this.type}}; 360 for (let i=0; i<tickCount; i++) { 361 if (this.actions.has(i)) { 362 actions.push(this.actions.get(i)); 363 } else { 364 actions.push({"type": "pause"}); 365 } 366 } 367 return data; 368 }, 369 370 pointerDown: function(actions, button) { 371 let tick = actions.tickIdx; 372 if (this.actions.has(tick)) { 373 tick = actions.addTick().tickIdx; 374 } 375 this.actions.set(tick, {type: "pointerDown", button}); 376 }, 377 378 pointerUp: function(actions, button) { 379 let tick = actions.tickIdx; 380 if (this.actions.has(tick)) { 381 tick = actions.addTick().tickIdx; 382 } 383 this.actions.set(tick, {type: "pointerUp", button}); 384 }, 385 386 pointerMove: function(actions, x, y, duration, origin) { 387 let tick = actions.tickIdx; 388 if (this.actions.has(tick)) { 389 tick = actions.addTick().tickIdx; 390 } 391 this.actions.set(tick, {type: "pointerMove", x, y, origin}); 392 if (duration) { 393 this.actions.get(tick).duration = duration; 394 } 395 }, 396 }; 397 398 test_driver.Actions = Actions; 399})(); 400