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