tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 })();