• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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