Browse Source

Tests: Import WPT accname/name/comp_embedded_control.html test

This change imports the WPT accname/name/comp_embedded_control.html
test, along with related resources files it depends on.

Note that in the wai-aria/scripts/aria-utils.js file, this changes the
get_computed_label call to use our window.internals.getComputedLabel.
sideshowbarker 8 months ago
parent
commit
120bc52f23

+ 36 - 0
Tests/LibWeb/Text/expected/wpt-import/accname/name/comp_embedded_control.txt

@@ -0,0 +1,36 @@
+Summary
+
+Harness status: OK
+
+Rerun
+
+Found 26 tests
+
+26 Pass
+Details
+Result	Test Name	MessagePass	checkbox label with embedded textfield	
+Pass	label of embedded textfield inside checkbox label	
+Pass	checkbox label with embedded select:not([size])	
+Pass	label of embedded select:not([size]) inside checkbox label	
+Pass	checkbox label with embedded select[size]	
+Pass	label of embedded select[size] inside checkbox label	
+Pass	checkbox label with embedded combobox (input[type=text])	
+Pass	label of embedded combobox (input[type=text]) inside checkbox label	
+Pass	checkbox label with embedded combobox (span)	
+Pass	label of embedded combobox (span) inside checkbox label	
+Pass	checkbox label with embedded combobox (div)	
+Pass	label of embedded combobox (div) inside checkbox label	
+Pass	checkbox label with embedded listbox>option[aria-selected=true]	
+Pass	label of embedded listbox>option[aria-selected=true] inside checkbox label	
+Pass	checkbox label with embedded input[type=range]	
+Pass	label of embedded input[type=range] inside checkbox label	
+Pass	checkbox label with embedded input[type=number]	
+Pass	label of embedded input[type=number] inside checkbox label	
+Pass	checkbox label with embedded ARIA slider (aria-valuenow)	
+Pass	label of embedded ARIA slider (aria-valuenow) inside checkbox label	
+Pass	checkbox label with embedded ARIA slider (aria-valuetext)	
+Pass	label of embedded ARIA slider (aria-valuetext) inside checkbox label	
+Pass	checkbox label with embedded ARIA spinbutton (aria-valuenow)	
+Pass	label of embedded ARIA spinbutton (aria-valuenow) inside checkbox label	
+Pass	checkbox label with embedded ARIA spinbutton (aria-valuetext)	
+Pass	label of embedded ARIA spinbutton (aria-valuetext) inside checkbox label	

+ 134 - 0
Tests/LibWeb/Text/input/wpt-import/accname/name/comp_embedded_control.html

@@ -0,0 +1,134 @@
+<!doctype html>
+<html>
+<head>
+  <title>Name Comp: Embedded Control</title>
+  <script src="../../resources/testharness.js"></script>
+  <script src="../../resources/testharnessreport.js"></script>
+  <script src="../../resources/testdriver.js"></script>
+  <script src="../../resources/testdriver-vendor.js"></script>
+  <script src="../../resources/testdriver-actions.js"></script>
+  <script src="../../wai-aria/scripts/aria-utils.js"></script>
+</head>
+<body>
+
+<p>Tests the <a href="https://w3c.github.io/accname/#comp_embedded_control">#comp_embedded_control</a> portions of the AccName <em>Name Computation</em> algorithm.</p>
+
+<!-- Textfield (textbox) -->
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded textfield" class="ex">
+  Flash the screen
+  <input value="3" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded textfield inside checkbox label" class="ex"> times
+</label>
+<br><br>
+
+<!-- HTML select:not([size]) element renders as menu button (on Mac) or single-row listbox (on Windows/Linux) -->
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded select:not([size])" class="ex">
+  Flash the screen
+  <select aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded select:not([size]) inside checkbox label" class="ex">
+    <option value="1">1</option>
+    <option value="2">2</option>
+    <option value="3" selected>3</option>
+    <option value="4">4</option>
+    <option value="5">5</option>
+  </select>
+  times
+</label>
+<br><br>
+
+<!-- HTML select[size] element renders as multi-row listbox -->
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded select[size]" class="ex">
+  Flash the screen
+  <select size=5 aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded select[size] inside checkbox label" class="ex">
+    <option value="1">1</option>
+    <option value="2">2</option>
+    <option value="3" selected>3</option>
+    <option value="4">4</option>
+    <option value="5">5</option>
+  </select>
+  times
+</label>
+<br><br>
+
+<!-- ARIA combobox examples (not interactive) -->
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded combobox (input[type=text])" class="ex">
+  Flash the screen
+  <input role="combobox" value="3" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded combobox (input[type=text]) inside checkbox label" class="ex"> times
+</label>
+<br><br>
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded combobox (span)" class="ex">
+  Flash the screen
+  <span role="combobox" tabindex="0" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded combobox (span) inside checkbox label" class="ex">3</span> times
+</label>
+<br><br>
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded combobox (div)" class="ex">
+  Flash the screen
+  <div style="display: inline-block;" role="combobox" tabindex="0" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded combobox (div) inside checkbox label" class="ex">3</div> times
+</label>
+<br><br>
+<!-- Todo: we may want another combobox example using aria-activedescendant? -->
+
+
+<!-- ARIA listbox examples (not interactive) -->
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded listbox>option[aria-selected=true]" class="ex">
+  Flash the screen
+  <ul role="listbox" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded listbox>option[aria-selected=true] inside checkbox label" class="ex" style="padding:0; list-style:none; border: solid 1px gray; width: 15em; display: inline-block;">
+    <li role="option" tabindex="-1" style="padding:0.2em 0.8em; list-style:none;">1</li>
+    <li role="option" tabindex="-1" style="padding:0.2em 0.8em; list-style:none;">2</li>
+    <li role="option" tabindex="0" aria-selected="true" style="padding:0.2em 0.8em; list-style:none; color: white; background-color: #555;">3</li>
+    <li role="option" tabindex="-1" style="padding:0.2em 0.8em; list-style:none;">4</li>
+    <li role="option" tabindex="-1" style="padding:0.2em 0.8em; list-style:none;">5</li>
+  </ul> times
+</label>
+<br><br>
+
+
+<!-- Ranges: HTML native and ARIA sliders and steppers (valuetext, valuenow, host language specific) -->
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded input[type=range]" class="ex">
+  Flash the screen
+  <input type ="range" min="1" max="5" value="3" step="1" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded input[type=range] inside checkbox label" class="ex"> times
+</label>
+<br><br>
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded input[type=number]" class="ex">
+  Flash the screen
+  <input type ="number" min="1" max="5" value="3" step="1" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded input[type=number] inside checkbox label" class="ex"> times
+</label>
+<br><br>
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded ARIA slider (aria-valuenow)" class="ex">
+  Flash the screen
+  <span tabindex="0" role="slider" aria-valuemin="1" aria-valuemax="5" aria-valuenow="3" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded ARIA slider (aria-valuenow) inside checkbox label" class="ex">3.0</span> times
+</label>
+<br><br>
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded ARIA slider (aria-valuetext)" class="ex">
+  Flash the screen
+  <span tabindex="0" role="slider" aria-valuemin="1.0" aria-valuemax="5.0" aria-valuenow="3.0" aria-valuetext="3" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded ARIA slider (aria-valuetext) inside checkbox label" class="ex">3.0</span> times
+</label>
+<br><br>
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded ARIA spinbutton (aria-valuenow)" class="ex">
+  Flash the screen
+  <span tabindex="0" role="spinbutton" aria-valuemin="1" aria-valuemax="5" aria-valuenow="3" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded ARIA spinbutton (aria-valuenow) inside checkbox label" class="ex">3.0</span> times
+</label>
+<br><br>
+<label>
+  <input type="checkbox" data-expectedlabel="Flash the screen 3 times" data-testname="checkbox label with embedded ARIA spinbutton (aria-valuetext)" class="ex">
+  Flash the screen
+  <span tabindex="0" role="spinbutton" aria-valuemin="1.0" aria-valuemax="5.0" aria-valuenow="3.0" aria-valuetext="3" aria-label="number of times" data-expectedlabel="number of times" data-testname="label of embedded ARIA spinbutton (aria-valuetext) inside checkbox label" class="ex">3.0</span> times
+</label>
+<br><br>
+
+
+<script>
+AriaUtils.verifyLabelsBySelector(".ex");
+</script>
+</body>
+</html>

+ 599 - 0
Tests/LibWeb/Text/input/wpt-import/resources/testdriver-actions.js

@@ -0,0 +1,599 @@
+(function() {
+  let sourceNameIdx = 0;
+
+  /**
+   * @class
+   * Builder for creating a sequence of actions
+   *
+   *
+   * The actions are dispatched once
+   * :js:func:`test_driver.Actions.send` is called. This returns a
+   * promise which resolves once the actions are complete.
+   *
+   * The other methods on :js:class:`test_driver.Actions` object are
+   * used to build the sequence of actions that will be sent. These
+   * return the `Actions` object itself, so the actions sequence can
+   * be constructed by chaining method calls.
+   *
+   * Internally :js:func:`test_driver.Actions.send` invokes
+   * :js:func:`test_driver.action_sequence`.
+   *
+   * @example
+   * let text_box = document.getElementById("text");
+   *
+   * let actions = new test_driver.Actions()
+   *    .pointerMove(0, 0, {origin: text_box})
+   *    .pointerDown()
+   *    .pointerUp()
+   *    .addTick()
+   *    .keyDown("p")
+   *    .keyUp("p");
+   *
+   * await actions.send();
+   *
+   * @param {number} [defaultTickDuration] - The default duration of a
+   * tick. Be default this is set ot 16ms, which is one frame time
+   * based on 60Hz display.
+   */
+  function Actions(defaultTickDuration=16) {
+    this.sourceTypes = new Map([["key", KeySource],
+                                ["pointer", PointerSource],
+                                ["wheel", WheelSource],
+                                ["none", GeneralSource]]);
+    this.sources = new Map();
+    this.sourceOrder = [];
+    for (let sourceType of this.sourceTypes.keys()) {
+      this.sources.set(sourceType, new Map());
+    }
+    this.currentSources = new Map();
+    for (let sourceType of this.sourceTypes.keys()) {
+      this.currentSources.set(sourceType, null);
+    }
+    this.createSource("none");
+    this.tickIdx = 0;
+    this.defaultTickDuration = defaultTickDuration;
+    this.context = null;
+  }
+
+  Actions.prototype = {
+    ButtonType: {
+      LEFT: 0,
+      MIDDLE: 1,
+      RIGHT: 2,
+      BACK: 3,
+      FORWARD: 4,
+    },
+
+    /**
+     * Generate the action sequence suitable for passing to
+     * test_driver.action_sequence
+     *
+     * @returns {Array} Array of WebDriver-compatible actions sequences
+     */
+    serialize: function() {
+      let actions = [];
+      for (let [sourceType, sourceName] of this.sourceOrder) {
+        let source = this.sources.get(sourceType).get(sourceName);
+        let serialized = source.serialize(this.tickIdx + 1, this.defaultTickDuration);
+        if (serialized) {
+          serialized.id = sourceName;
+          actions.push(serialized);
+        }
+      }
+      return actions;
+    },
+
+    /**
+     * Generate and send the action sequence
+     *
+     * @returns {Promise} fulfilled after the sequence is executed,
+     *                    rejected if any actions fail.
+     */
+    send: function() {
+      let actions;
+      try {
+        actions = this.serialize();
+      } catch(e) {
+        return Promise.reject(e);
+      }
+      return test_driver.action_sequence(actions, this.context);
+    },
+
+    /**
+     * Set the context for the actions
+     *
+     * @param {WindowProxy} context - Context in which to run the action sequence
+     */
+    setContext: function(context) {
+      this.context = context;
+      return this;
+    },
+
+    /**
+     * Get the action source with a particular source type and name.
+     * If no name is passed, a new source with the given type is
+     * created.
+     *
+     * @param {String} type - Source type ('none', 'key', 'pointer', or 'wheel')
+     * @param {String?} name - Name of the source
+     * @returns {Source} Source object for that source.
+     */
+    getSource: function(type, name) {
+      if (!this.sources.has(type)) {
+        throw new Error(`${type} is not a valid action type`);
+      }
+      if (name === null || name === undefined) {
+        name = this.currentSources.get(type);
+      }
+      if (name === null || name === undefined) {
+        return this.createSource(type, null);
+      }
+      return this.sources.get(type).get(name);
+    },
+
+    setSource: function(type, name) {
+      if (!this.sources.has(type)) {
+        throw new Error(`${type} is not a valid action type`);
+      }
+      if (!this.sources.get(type).has(name)) {
+        throw new Error(`${name} is not a valid source for ${type}`);
+      }
+      this.currentSources.set(type, name);
+      return this;
+    },
+
+    /**
+     * Add a new key input source with the given name
+     *
+     * @param {String} name - Name of the key source
+     * @param {Bool} set - Set source as the default key source
+     * @returns {Actions}
+     */
+    addKeyboard: function(name, set=true) {
+      this.createSource("key", name);
+      if (set) {
+        this.setKeyboard(name);
+      }
+      return this;
+    },
+
+    /**
+     * Set the current default key source
+     *
+     * @param {String} name - Name of the key source
+     * @returns {Actions}
+     */
+    setKeyboard: function(name) {
+      this.setSource("key", name);
+      return this;
+    },
+
+    /**
+     * Add a new pointer input source with the given name
+     *
+     * @param {String} type - Name of the pointer source
+     * @param {String} pointerType - Type of pointing device
+     * @param {Bool} set - Set source as the default pointer source
+     * @returns {Actions}
+     */
+    addPointer: function(name, pointerType="mouse", set=true) {
+      this.createSource("pointer", name, {pointerType: pointerType});
+      if (set) {
+        this.setPointer(name);
+      }
+      return this;
+    },
+
+    /**
+     * Set the current default pointer source
+     *
+     * @param {String} name - Name of the pointer source
+     * @returns {Actions}
+     */
+    setPointer: function(name) {
+      this.setSource("pointer", name);
+      return this;
+    },
+
+    /**
+     * Add a new wheel input source with the given name
+     *
+     * @param {String} type - Name of the wheel source
+     * @param {Bool} set - Set source as the default wheel source
+     * @returns {Actions}
+     */
+    addWheel: function(name, set=true) {
+      this.createSource("wheel", name);
+      if (set) {
+        this.setWheel(name);
+      }
+      return this;
+    },
+
+    /**
+     * Set the current default wheel source
+     *
+     * @param {String} name - Name of the wheel source
+     * @returns {Actions}
+     */
+    setWheel: function(name) {
+      this.setSource("wheel", name);
+      return this;
+    },
+
+    createSource: function(type, name, parameters={}) {
+      if (!this.sources.has(type)) {
+        throw new Error(`${type} is not a valid action type`);
+      }
+      let sourceNames = new Set();
+      for (let [_, name] of this.sourceOrder) {
+        sourceNames.add(name);
+      }
+      if (!name) {
+        do {
+          name = "" + sourceNameIdx++;
+        } while (sourceNames.has(name))
+      } else {
+        if (sourceNames.has(name)) {
+          throw new Error(`Alreay have a source of type ${type} named ${name}.`);
+        }
+      }
+      this.sources.get(type).set(name, new (this.sourceTypes.get(type))(parameters));
+      this.currentSources.set(type, name);
+      this.sourceOrder.push([type, name]);
+      return this.sources.get(type).get(name);
+    },
+
+    /**
+     * Insert a new actions tick
+     *
+     * @param {Number?} duration - Minimum length of the tick in ms.
+     * @returns {Actions}
+     */
+    addTick: function(duration) {
+      this.tickIdx += 1;
+      if (duration) {
+        this.pause(duration);
+      }
+      return this;
+    },
+
+    /**
+     * Add a pause to the current tick
+     *
+     * @param {Number?} duration - Minimum length of the tick in ms.
+     * @param {String} sourceType - source type
+     * @param {String?} sourceName - Named key, pointer or wheel source to use
+     *                               or null for the default key, pointer or
+     *                               wheel source
+     * @returns {Actions}
+     */
+    pause: function(duration=0, sourceType="none", {sourceName=null}={}) {
+      if (sourceType=="none")
+        this.getSource("none").addPause(this, duration);
+      else
+        this.getSource(sourceType, sourceName).addPause(this, duration);
+      return this;
+    },
+
+    /**
+     * Create a keyDown event for the current default key source
+     *
+     * @param {String} key - Key to press
+     * @param {String?} sourceName - Named key source to use or null for the default key source
+     * @returns {Actions}
+     */
+    keyDown: function(key, {sourceName=null}={}) {
+      let source = this.getSource("key", sourceName);
+      source.keyDown(this, key);
+      return this;
+    },
+
+    /**
+     * Create a keyDown event for the current default key source
+     *
+     * @param {String} key - Key to release
+     * @param {String?} sourceName - Named key source to use or null for the default key source
+     * @returns {Actions}
+     */
+    keyUp: function(key, {sourceName=null}={}) {
+      let source = this.getSource("key", sourceName);
+      source.keyUp(this, key);
+      return this;
+    },
+
+    /**
+     * Create a pointerDown event for the current default pointer source
+     *
+     * @param {String} button - Button to press
+     * @param {String?} sourceName - Named pointer source to use or null for the default
+     *                               pointer source
+     * @returns {Actions}
+     */
+    pointerDown: function({button=this.ButtonType.LEFT, sourceName=null,
+                           width, height, pressure, tangentialPressure,
+                           tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) {
+      let source = this.getSource("pointer", sourceName);
+      source.pointerDown(this, button, width, height, pressure, tangentialPressure,
+                         tiltX, tiltY, twist, altitudeAngle, azimuthAngle);
+      return this;
+    },
+
+    /**
+     * Create a pointerUp event for the current default pointer source
+     *
+     * @param {String} button - Button to release
+     * @param {String?} sourceName - Named pointer source to use or null for the default pointer
+     *                               source
+     * @returns {Actions}
+     */
+    pointerUp: function({button=this.ButtonType.LEFT, sourceName=null}={}) {
+      let source = this.getSource("pointer", sourceName);
+      source.pointerUp(this, button);
+      return this;
+    },
+
+    /**
+     * Create a move event for the current default pointer source
+     *
+     * @param {Number} x - Destination x coordinate
+     * @param {Number} y - Destination y coordinate
+     * @param {String|Element} origin - Origin of the coordinate system.
+     *                                  Either "pointer", "viewport" or an Element
+     * @param {Number?} duration - Time in ms for the move
+     * @param {String?} sourceName - Named pointer source to use or null for the default pointer
+     *                               source
+     * @returns {Actions}
+     */
+    pointerMove: function(x, y,
+                          {origin="viewport", duration, sourceName=null,
+                           width, height, pressure, tangentialPressure,
+                           tiltX, tiltY, twist, altitudeAngle, azimuthAngle}={}) {
+      let source = this.getSource("pointer", sourceName);
+      source.pointerMove(this, x, y, duration, origin, width, height, pressure,
+                         tangentialPressure, tiltX, tiltY, twist, altitudeAngle,
+                         azimuthAngle);
+      return this;
+    },
+
+    /**
+     * Create a scroll event for the current default wheel source
+     *
+     * @param {Number} x - mouse cursor x coordinate
+     * @param {Number} y - mouse cursor y coordinate
+     * @param {Number} deltaX - scroll delta value along the x-axis in pixels
+     * @param {Number} deltaY - scroll delta value along the y-axis in pixels
+     * @param {String|Element} origin - Origin of the coordinate system.
+     *                                  Either "viewport" or an Element
+     * @param {Number?} duration - Time in ms for the scroll
+     * @param {String?} sourceName - Named wheel source to use or null for the
+     *                               default wheel source
+     * @returns {Actions}
+     */
+    scroll: function(x, y, deltaX, deltaY,
+                     {origin="viewport", duration, sourceName=null}={}) {
+      let source = this.getSource("wheel", sourceName);
+      source.scroll(this, x, y, deltaX, deltaY, duration, origin);
+      return this;
+    },
+  };
+
+  function GeneralSource() {
+    this.actions = new Map();
+  }
+
+  GeneralSource.prototype = {
+    serialize: function(tickCount, defaultTickDuration) {
+      let actions = [];
+      let data = {"type": "none", "actions": actions};
+      for (let i=0; i<tickCount; i++) {
+        if (this.actions.has(i)) {
+          actions.push(this.actions.get(i));
+        } else {
+          actions.push({"type": "pause", duration: defaultTickDuration});
+        }
+      }
+      return data;
+    },
+
+    addPause: function(actions, duration) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        throw new Error(`Already have a pause action for the current tick`);
+      }
+      this.actions.set(tick, {type: "pause", duration: duration});
+    },
+  };
+
+  function KeySource() {
+    this.actions = new Map();
+  }
+
+  KeySource.prototype = {
+    serialize: function(tickCount) {
+      if (!this.actions.size) {
+        return undefined;
+      }
+      let actions = [];
+      let data = {"type": "key", "actions": actions};
+      for (let i=0; i<tickCount; i++) {
+        if (this.actions.has(i)) {
+          actions.push(this.actions.get(i));
+        } else {
+          actions.push({"type": "pause"});
+        }
+      }
+      return data;
+    },
+
+    keyDown: function(actions, key) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        tick = actions.addTick().tickIdx;
+      }
+      this.actions.set(tick, {type: "keyDown", value: key});
+    },
+
+    keyUp: function(actions, key) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        tick = actions.addTick().tickIdx;
+      }
+      this.actions.set(tick, {type: "keyUp", value: key});
+    },
+
+    addPause: function(actions, duration) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        tick = actions.addTick().tickIdx;
+      }
+      this.actions.set(tick, {type: "pause", duration: duration});
+    },
+  };
+
+  function PointerSource(parameters={pointerType: "mouse"}) {
+    let pointerType = parameters.pointerType || "mouse";
+    if (!["mouse", "pen", "touch"].includes(pointerType)) {
+      throw new Error(`Invalid pointerType ${pointerType}`);
+    }
+    this.type = pointerType;
+    this.actions = new Map();
+  }
+
+  function setPointerProperties(action, width, height, pressure, tangentialPressure,
+                                tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+    if (width) {
+      action.width = width;
+    }
+    if (height) {
+      action.height = height;
+    }
+    if (pressure) {
+      action.pressure = pressure;
+    }
+    if (tangentialPressure) {
+      action.tangentialPressure = tangentialPressure;
+    }
+    if (tiltX) {
+      action.tiltX = tiltX;
+    }
+    if (tiltY) {
+      action.tiltY = tiltY;
+    }
+    if (twist) {
+      action.twist = twist;
+    }
+    if (altitudeAngle) {
+      action.altitudeAngle = altitudeAngle;
+    }
+    if (azimuthAngle) {
+      action.azimuthAngle = azimuthAngle;
+    }
+    return action;
+  }
+
+  PointerSource.prototype = {
+    serialize: function(tickCount) {
+      if (!this.actions.size) {
+        return undefined;
+      }
+      let actions = [];
+      let data = {"type": "pointer", "actions": actions, "parameters": {"pointerType": this.type}};
+      for (let i=0; i<tickCount; i++) {
+        if (this.actions.has(i)) {
+          actions.push(this.actions.get(i));
+        } else {
+          actions.push({"type": "pause"});
+        }
+      }
+      return data;
+    },
+
+    pointerDown: function(actions, button, width, height, pressure, tangentialPressure,
+                          tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        tick = actions.addTick().tickIdx;
+      }
+      let actionProperties = setPointerProperties({type: "pointerDown", button}, width, height,
+                                                  pressure, tangentialPressure, tiltX, tiltY,
+                                                  twist, altitudeAngle, azimuthAngle);
+      this.actions.set(tick, actionProperties);
+    },
+
+    pointerUp: function(actions, button) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        tick = actions.addTick().tickIdx;
+      }
+      this.actions.set(tick, {type: "pointerUp", button});
+    },
+
+    pointerMove: function(actions, x, y, duration, origin, width, height, pressure,
+                          tangentialPressure, tiltX, tiltY, twist, altitudeAngle, azimuthAngle) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        tick = actions.addTick().tickIdx;
+      }
+      let moveAction = {type: "pointerMove", x, y, origin};
+      if (duration) {
+        moveAction.duration = duration;
+      }
+      let actionProperties = setPointerProperties(moveAction, width, height, pressure,
+                                                  tangentialPressure, tiltX, tiltY, twist,
+                                                  altitudeAngle, azimuthAngle);
+      this.actions.set(tick, actionProperties);
+    },
+
+    addPause: function(actions, duration) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        tick = actions.addTick().tickIdx;
+      }
+      this.actions.set(tick, {type: "pause", duration: duration});
+    },
+  };
+
+  function WheelSource() {
+    this.actions = new Map();
+  }
+
+  WheelSource.prototype = {
+    serialize: function(tickCount) {
+      if (!this.actions.size) {
+        return undefined;
+      }
+      let actions = [];
+      let data = {"type": "wheel", "actions": actions};
+      for (let i=0; i<tickCount; i++) {
+        if (this.actions.has(i)) {
+          actions.push(this.actions.get(i));
+        } else {
+          actions.push({"type": "pause"});
+        }
+      }
+      return data;
+    },
+
+    scroll: function(actions, x, y, deltaX, deltaY, duration, origin) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        tick = actions.addTick().tickIdx;
+      }
+      this.actions.set(tick, {type: "scroll", x, y, deltaX, deltaY, origin});
+      if (duration) {
+        this.actions.get(tick).duration = duration;
+      }
+    },
+
+    addPause: function(actions, duration) {
+      let tick = actions.tickIdx;
+      if (this.actions.has(tick)) {
+        tick = actions.addTick().tickIdx;
+      }
+      this.actions.set(tick, {type: "pause", duration: duration});
+    },
+  };
+
+  test_driver.Actions = Actions;
+})();

+ 1 - 0
Tests/LibWeb/Text/input/wpt-import/resources/testdriver-vendor.js

@@ -0,0 +1 @@
+// This file intentionally left blank

+ 1259 - 0
Tests/LibWeb/Text/input/wpt-import/resources/testdriver.js

@@ -0,0 +1,1259 @@
+(function() {
+    "use strict";
+    var idCounter = 0;
+    let testharness_context = null;
+
+    function getInViewCenterPoint(rect) {
+        var left = Math.max(0, rect.left);
+        var right = Math.min(window.innerWidth, rect.right);
+        var top = Math.max(0, rect.top);
+        var bottom = Math.min(window.innerHeight, rect.bottom);
+
+        var x = 0.5 * (left + right);
+        var y = 0.5 * (top + bottom);
+
+        return [x, y];
+    }
+
+    function getPointerInteractablePaintTree(element) {
+        let elementDocument = element.ownerDocument;
+        if (!elementDocument.contains(element)) {
+            return [];
+        }
+
+        var rectangles = element.getClientRects();
+
+        if (rectangles.length === 0) {
+            return [];
+        }
+
+        var centerPoint = getInViewCenterPoint(rectangles[0]);
+
+        if ("elementsFromPoint" in elementDocument) {
+            return elementDocument.elementsFromPoint(centerPoint[0], centerPoint[1]);
+        } else if ("msElementsFromPoint" in elementDocument) {
+            var rv = elementDocument.msElementsFromPoint(centerPoint[0], centerPoint[1]);
+            return Array.prototype.slice.call(rv ? rv : []);
+        } else {
+            throw new Error("document.elementsFromPoint unsupported");
+        }
+    }
+
+    function inView(element) {
+        var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+        return pointerInteractablePaintTree.indexOf(element) !== -1;
+    }
+
+
+    /**
+     * @namespace {test_driver}
+     */
+    window.test_driver = {
+        /**
+         * Set the context in which testharness.js is loaded
+         *
+         * @param {WindowProxy} context - the window containing testharness.js
+         **/
+        set_test_context: function(context) {
+          if (window.test_driver_internal.set_test_context) {
+            window.test_driver_internal.set_test_context(context);
+          }
+          testharness_context = context;
+        },
+
+        /**
+         * postMessage to the context containing testharness.js
+         *
+         * @param {Object} msg - the data to POST
+         **/
+        message_test: function(msg) {
+            let target = testharness_context;
+            if (testharness_context === null) {
+                target = window;
+            }
+            target.postMessage(msg, "*");
+        },
+
+        /**
+         * Trigger user interaction in order to grant additional privileges to
+         * a provided function.
+         *
+         * See `Tracking user activation
+         * <https://html.spec.whatwg.org/multipage/interaction.html#tracking-user-activation>`_.
+         *
+         * @example
+         * var mediaElement = document.createElement('video');
+         *
+         * test_driver.bless('initiate media playback', function () {
+         *   mediaElement.play();
+         * });
+         *
+         * @param {String} intent - a description of the action which must be
+         *                          triggered by user interaction
+         * @param {Function} action - code requiring escalated privileges
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled following user interaction and
+         *                    execution of the provided `action` function;
+         *                    rejected if interaction fails or the provided
+         *                    function throws an error
+         */
+        bless: function(intent, action, context=null) {
+            let contextDocument = context ? context.document : document;
+            var button = contextDocument.createElement("button");
+            button.innerHTML = "This test requires user interaction.<br />" +
+                "Please click here to allow " + intent + ".";
+            button.id = "wpt-test-driver-bless-" + (idCounter += 1);
+            const elem = contextDocument.body || contextDocument.documentElement;
+            elem.appendChild(button);
+
+            let wait_click = new Promise(resolve => button.addEventListener("click", resolve));
+
+            return test_driver.click(button)
+                .then(wait_click)
+                .then(function() {
+                    button.remove();
+
+                    if (typeof action === "function") {
+                        return action();
+                    }
+                    return null;
+                });
+        },
+
+        /**
+         * Triggers a user-initiated click
+         *
+         * If ``element`` isn't inside the
+         * viewport, it will be scrolled into view before the click
+         * occurs.
+         *
+         * If ``element`` is from a different browsing context, the
+         * command will be run in that context.
+         *
+         * Matches the behaviour of the `Element Click
+         * <https://w3c.github.io/webdriver/#element-click>`_
+         * WebDriver command.
+         *
+         * **Note:** If the element to be clicked does not have a
+         * unique ID, the document must not have any DOM mutations
+         * made between the function being called and the promise
+         * settling.
+         *
+         * @param {Element} element - element to be clicked
+         * @returns {Promise} fulfilled after click occurs, or rejected in
+         *                    the cases the WebDriver command errors
+         */
+        click: function(element) {
+            if (!inView(element)) {
+                element.scrollIntoView({behavior: "instant",
+                                        block: "end",
+                                        inline: "nearest"});
+            }
+
+            var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+            if (pointerInteractablePaintTree.length === 0 ||
+                !element.contains(pointerInteractablePaintTree[0])) {
+                return Promise.reject(new Error("element click intercepted error"));
+            }
+
+            var rect = element.getClientRects()[0];
+            var centerPoint = getInViewCenterPoint(rect);
+            return window.test_driver_internal.click(element,
+                                                     {x: centerPoint[0],
+                                                      y: centerPoint[1]});
+        },
+
+        /**
+         * Deletes all cookies.
+         *
+         * Matches the behaviour of the `Delete All Cookies
+         * <https://w3c.github.io/webdriver/#delete-all-cookies>`_
+         * WebDriver command.
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after cookies are deleted, or rejected in
+         *                    the cases the WebDriver command errors
+         */
+        delete_all_cookies: function(context=null) {
+            return window.test_driver_internal.delete_all_cookies(context);
+        },
+
+        /**
+         * Get details for all cookies in the current context.
+         * See https://w3c.github.io/webdriver/#get-all-cookies
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Returns an array of cookies objects as defined in the spec:
+         *                    https://w3c.github.io/webdriver/#cookies
+         */
+        get_all_cookies: function(context=null) {
+            return window.test_driver_internal.get_all_cookies(context);
+        },
+
+        /**
+         * Get details for a cookie in the current context by name if it exists.
+         * See https://w3c.github.io/webdriver/#get-named-cookie
+         *
+         * @param {String} name - The name of the cookie to get.
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Returns the matching cookie as defined in the spec:
+         *                    https://w3c.github.io/webdriver/#cookies
+         *                    Rejected if no such cookie exists.
+         */
+         get_named_cookie: async function(name, context=null) {
+            let cookie = await window.test_driver_internal.get_named_cookie(name, context);
+            if (!cookie) {
+                throw new Error("no such cookie");
+            }
+            return cookie;
+        },
+
+        /**
+         * Get Computed Label for an element.
+         *
+         * This matches the behaviour of the
+         * `Get Computed Label
+         * <https://w3c.github.io/webdriver/#dfn-get-computed-label>`_
+         * WebDriver command.
+         *
+         * @param {Element} element
+         * @returns {Promise} fulfilled after the computed label is returned, or
+         *                    rejected in the cases the WebDriver command errors
+         */
+        get_computed_label: async function(element) {
+            let label = await window.test_driver_internal.get_computed_label(element);
+            return label;
+        },
+
+        /**
+         * Get Computed Role for an element.
+         *
+         * This matches the behaviour of the
+         * `Get Computed Label
+         * <https://w3c.github.io/webdriver/#dfn-get-computed-role>`_
+         * WebDriver command.
+         *
+         * @param {Element} element
+         * @returns {Promise} fulfilled after the computed role is returned, or
+         *                    rejected in the cases the WebDriver command errors
+         */
+        get_computed_role: async function(element) {
+            let role = await window.test_driver_internal.get_computed_role(element);
+            return role;
+        },
+
+        /**
+         * Send keys to an element.
+         *
+         * If ``element`` isn't inside the
+         * viewport, it will be scrolled into view before the click
+         * occurs.
+         *
+         * If ``element`` is from a different browsing context, the
+         * command will be run in that context. The test must not depend
+         * on the ``window.name`` property being unset on the target
+         * window.
+         *
+         * To send special keys, send the respective key's codepoint,
+         * as defined by `WebDriver
+         * <https://w3c.github.io/webdriver/#keyboard-actions>`_.  For
+         * example, the "tab" key is represented as "``\uE004``".
+         *
+         * **Note:** these special-key codepoints are not necessarily
+         * what you would expect. For example, <kbd>Esc</kbd> is the
+         * invalid Unicode character ``\uE00C``, not the ``\u001B`` Escape
+         * character from ASCII.
+         *
+         * This matches the behaviour of the
+         * `Send Keys
+         * <https://w3c.github.io/webdriver/#element-send-keys>`_
+         * WebDriver command.
+         *
+         * **Note:** If the element to be clicked does not have a
+         * unique ID, the document must not have any DOM mutations
+         * made between the function being called and the promise
+         * settling.
+         *
+         * @param {Element} element - element to send keys to
+         * @param {String} keys - keys to send to the element
+         * @returns {Promise} fulfilled after keys are sent, or rejected in
+         *                    the cases the WebDriver command errors
+         */
+        send_keys: function(element, keys) {
+            if (!inView(element)) {
+                element.scrollIntoView({behavior: "instant",
+                                        block: "end",
+                                        inline: "nearest"});
+            }
+
+            return window.test_driver_internal.send_keys(element, keys);
+        },
+
+        /**
+         * Freeze the current page
+         *
+         * The freeze function transitions the page from the HIDDEN state to
+         * the FROZEN state as described in `Lifecycle API for Web Pages
+         * <https://github.com/WICG/page-lifecycle/blob/master/README.md>`_.
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the freeze request is sent, or rejected
+         *                    in case the WebDriver command errors
+         */
+        freeze: function(context=null) {
+            return window.test_driver_internal.freeze();
+        },
+
+        /**
+         * Minimizes the browser window.
+         *
+         * Matches the the behaviour of the `Minimize
+         * <https://www.w3.org/TR/webdriver/#minimize-window>`_
+         * WebDriver command
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled with the previous `WindowRect
+         *                    <https://www.w3.org/TR/webdriver/#dfn-windowrect-object>`_
+         *                    value, after the window is minimized.
+         */
+        minimize_window: function(context=null) {
+            return window.test_driver_internal.minimize_window(context);
+        },
+
+        /**
+         * Restore the window from minimized/maximized state to a given rect.
+         *
+         * Matches the behaviour of the `Set Window Rect
+         * <https://www.w3.org/TR/webdriver/#set-window-rect>`_
+         * WebDriver command
+         *
+         * @param {Object} rect - A `WindowRect
+         *                        <https://www.w3.org/TR/webdriver/#dfn-windowrect-object>`_
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the window is restored to the given rect.
+         */
+        set_window_rect: function(rect, context=null) {
+            return window.test_driver_internal.set_window_rect(rect, context);
+        },
+
+        /**
+         * Gets a rect with the size and position on the screen from the current window state.
+         *
+         * Matches the behaviour of the `Get Window Rect
+         * <https://www.w3.org/TR/webdriver/#get-window-rect>`_
+         * WebDriver command
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the window rect is returned, or rejected
+         * in cases the WebDriver command returns errors. Returns a
+         * `WindowRect <https://www.w3.org/TR/webdriver/#dfn-windowrect-object>`_
+         */
+        get_window_rect: function(context=null) {
+            return window.test_driver_internal.get_window_rect(context);
+        },
+
+        /**
+         * Send a sequence of actions
+         *
+         * This function sends a sequence of actions to perform.
+         *
+         * Matches the behaviour of the `Actions
+         * <https://w3c.github.io/webdriver/#actions>`_ feature in
+         * WebDriver.
+         *
+         * Authors are encouraged to use the
+         * :js:class:`test_driver.Actions` builder rather than
+         * invoking this API directly.
+         *
+         * @param {Array} actions - an array of actions. The format is
+         *                          the same as the actions property
+         *                          of the `Perform Actions
+         *                          <https://w3c.github.io/webdriver/#perform-actions>`_
+         *                          WebDriver command. Each element is
+         *                          an object representing an input
+         *                          source and each input source
+         *                          itself has an actions property
+         *                          detailing the behaviour of that
+         *                          source at each timestep (or
+         *                          tick). Authors are not expected to
+         *                          construct the actions sequence by
+         *                          hand, but to use the builder api
+         *                          provided in testdriver-actions.js
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the actions are performed, or rejected in
+         *                    the cases the WebDriver command errors
+         */
+        action_sequence: function(actions, context=null) {
+            return window.test_driver_internal.action_sequence(actions, context);
+        },
+
+        /**
+         * Generates a test report on the current page
+         *
+         * The generate_test_report function generates a report (to be
+         * observed by ReportingObserver) for testing purposes.
+         *
+         * Matches the `Generate Test Report
+         * <https://w3c.github.io/reporting/#generate-test-report-command>`_
+         * WebDriver command.
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the report is generated, or
+         *                    rejected if the report generation fails
+         */
+        generate_test_report: function(message, context=null) {
+            return window.test_driver_internal.generate_test_report(message, context);
+        },
+
+        /**
+         * Sets the state of a permission
+         *
+         * This function causes permission requests and queries for the status
+         * of a certain permission type (e.g. "push", or "background-fetch") to
+         * always return ``state``.
+         *
+         * Matches the `Set Permission
+         * <https://w3c.github.io/permissions/#set-permission-command>`_
+         * WebDriver command.
+         *
+         * @example
+         * await test_driver.set_permission({ name: "background-fetch" }, "denied");
+         * await test_driver.set_permission({ name: "push", userVisibleOnly: true }, "granted");
+         *
+         * @param {PermissionDescriptor} descriptor - a `PermissionDescriptor
+         *                              <https://w3c.github.io/permissions/#dom-permissiondescriptor>`_
+         *                              or derived object.
+         * @param {PermissionState} state - a `PermissionState
+         *                          <https://w3c.github.io/permissions/#dom-permissionstate>`_
+         *                          value.
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         * @returns {Promise} fulfilled after the permission is set, or rejected if setting the
+         *                    permission fails
+         */
+        set_permission: function(descriptor, state, context=null) {
+            let permission_params = {
+              descriptor,
+              state,
+            };
+            return window.test_driver_internal.set_permission(permission_params, context);
+        },
+
+        /**
+         * Creates a virtual authenticator
+         *
+         * This function creates a virtual authenticator for use with
+         * the U2F and WebAuthn APIs.
+         *
+         * Matches the `Add Virtual Authenticator
+         * <https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator>`_
+         * WebDriver command.
+         *
+         * @param {Object} config - an `Authenticator Configuration
+         *                          <https://w3c.github.io/webauthn/#authenticator-configuration>`_
+         *                          object
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the authenticator is added, or
+         *                    rejected in the cases the WebDriver command
+         *                    errors. Returns the ID of the authenticator
+         */
+        add_virtual_authenticator: function(config, context=null) {
+            return window.test_driver_internal.add_virtual_authenticator(config, context);
+        },
+
+        /**
+         * Removes a virtual authenticator
+         *
+         * This function removes a virtual authenticator that has been
+         * created by :js:func:`add_virtual_authenticator`.
+         *
+         * Matches the `Remove Virtual Authenticator
+         * <https://w3c.github.io/webauthn/#sctn-automation-remove-virtual-authenticator>`_
+         * WebDriver command.
+         *
+         * @param {String} authenticator_id - the ID of the authenticator to be
+         *                                    removed.
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the authenticator is removed, or
+         *                    rejected in the cases the WebDriver command
+         *                    errors
+         */
+        remove_virtual_authenticator: function(authenticator_id, context=null) {
+            return window.test_driver_internal.remove_virtual_authenticator(authenticator_id, context);
+        },
+
+        /**
+         * Adds a credential to a virtual authenticator
+         *
+         * Matches the `Add Credential
+         * <https://w3c.github.io/webauthn/#sctn-automation-add-credential>`_
+         * WebDriver command.
+         *
+         * @param {String} authenticator_id - the ID of the authenticator
+         * @param {Object} credential - A `Credential Parameters
+         *                              <https://w3c.github.io/webauthn/#credential-parameters>`_
+         *                              object
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the credential is added, or
+         *                    rejected in the cases the WebDriver command
+         *                    errors
+         */
+        add_credential: function(authenticator_id, credential, context=null) {
+            return window.test_driver_internal.add_credential(authenticator_id, credential, context);
+        },
+
+        /**
+         * Gets all the credentials stored in an authenticator
+         *
+         * This function retrieves all the credentials (added via the U2F API,
+         * WebAuthn, or the add_credential function) stored in a virtual
+         * authenticator
+         *
+         * Matches the `Get Credentials
+         * <https://w3c.github.io/webauthn/#sctn-automation-get-credentials>`_
+         * WebDriver command.
+         *
+         * @param {String} authenticator_id - the ID of the authenticator
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the credentials are
+         *                    returned, or rejected in the cases the
+         *                    WebDriver command errors. Returns an
+         *                    array of `Credential Parameters
+         *                    <https://w3c.github.io/webauthn/#credential-parameters>`_
+         */
+        get_credentials: function(authenticator_id, context=null) {
+            return window.test_driver_internal.get_credentials(authenticator_id, context=null);
+        },
+
+        /**
+         * Remove a credential stored in an authenticator
+         *
+         * Matches the `Remove Credential
+         * <https://w3c.github.io/webauthn/#sctn-automation-remove-credential>`_
+         * WebDriver command.
+         *
+         * @param {String} authenticator_id - the ID of the authenticator
+         * @param {String} credential_id - the ID of the credential
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the credential is removed, or
+         *                    rejected in the cases the WebDriver command
+         *                    errors.
+         */
+        remove_credential: function(authenticator_id, credential_id, context=null) {
+            return window.test_driver_internal.remove_credential(authenticator_id, credential_id, context);
+        },
+
+        /**
+         * Removes all the credentials stored in a virtual authenticator
+         *
+         * Matches the `Remove All Credentials
+         * <https://w3c.github.io/webauthn/#sctn-automation-remove-all-credentials>`_
+         * WebDriver command.
+         *
+         * @param {String} authenticator_id - the ID of the authenticator
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the credentials are removed, or
+         *                    rejected in the cases the WebDriver command
+         *                    errors.
+         */
+        remove_all_credentials: function(authenticator_id, context=null) {
+            return window.test_driver_internal.remove_all_credentials(authenticator_id, context);
+        },
+
+        /**
+         * Sets the User Verified flag on an authenticator
+         *
+         * Sets whether requests requiring user verification will succeed or
+         * fail on a given virtual authenticator
+         *
+         * Matches the `Set User Verified
+         * <https://w3c.github.io/webauthn/#sctn-automation-set-user-verified>`_
+         * WebDriver command.
+         *
+         * @param {String} authenticator_id - the ID of the authenticator
+         * @param {boolean} uv - the User Verified flag
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         */
+        set_user_verified: function(authenticator_id, uv, context=null) {
+            return window.test_driver_internal.set_user_verified(authenticator_id, uv, context);
+        },
+
+        /**
+         * Sets the storage access rule for an origin when embedded
+         * in a third-party context.
+         *
+         * Matches the `Set Storage Access
+         * <https://privacycg.github.io/storage-access/#set-storage-access-command>`_
+         * WebDriver command.
+         *
+         * @param {String} origin - A third-party origin to block or allow.
+         *                          May be "*" to indicate all origins.
+         * @param {String} embedding_origin - an embedding (first-party) origin
+         *                                    on which {origin}'s access should
+         *                                    be blocked or allowed.
+         *                                    May be "*" to indicate all origins.
+         * @param {String} state - The storage access setting.
+         *                         Must be either "allowed" or "blocked".
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the storage access rule has been
+         *                    set, or rejected if setting the rule fails.
+         */
+        set_storage_access: function(origin, embedding_origin, state, context=null) {
+            if (state !== "allowed" && state !== "blocked") {
+                throw new Error("storage access status must be 'allowed' or 'blocked'");
+            }
+            const blocked = state === "blocked";
+            return window.test_driver_internal.set_storage_access(origin, embedding_origin, blocked, context);
+        },
+
+        /**
+         * Sets the current transaction automation mode for Secure Payment
+         * Confirmation.
+         *
+         * This function places `Secure Payment
+         * Confirmation <https://w3c.github.io/secure-payment-confirmation>`_ into
+         * an automated 'autoAccept' or 'autoReject' mode, to allow testing
+         * without user interaction with the transaction UX prompt.
+         *
+         * Matches the `Set SPC Transaction Mode
+         * <https://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode>`_
+         * WebDriver command.
+         *
+         * @example
+         * await test_driver.set_spc_transaction_mode("autoaccept");
+         * test.add_cleanup(() => {
+         *   return test_driver.set_spc_transaction_mode("none");
+         * });
+         *
+         * // Assumption: `request` is a PaymentRequest with a secure-payment-confirmation
+         * // payment method.
+         * const response = await request.show();
+         *
+         * @param {String} mode - The `transaction mode
+         *                        <https://w3c.github.io/secure-payment-confirmation/#enumdef-transactionautomationmode>`_
+         *                        to set. Must be one of "``none``",
+         *                        "``autoAccept``", or
+         *                        "``autoReject``".
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the transaction mode has been set,
+         *                    or rejected if setting the mode fails.
+         */
+        set_spc_transaction_mode: function(mode, context=null) {
+          return window.test_driver_internal.set_spc_transaction_mode(mode, context);
+        },
+
+        /**
+         * Sets the current registration automation mode for Register Protocol Handlers.
+         *
+         * This function places `Register Protocol Handlers
+         * <https://html.spec.whatwg.org/multipage/system-state.html#custom-handlers>`_ into
+         * an automated 'autoAccept' or 'autoReject' mode, to allow testing
+         * without user interaction with the transaction UX prompt.
+         *
+         * Matches the `Set Register Protocol Handler Mode
+         * <https://html.spec.whatwg.org/multipage/system-state.html#set-rph-registration-mode>`_
+         * WebDriver command.
+         *
+         * @example
+         * await test_driver.set_rph_registration_mode("autoAccept");
+         * test.add_cleanup(() => {
+         *   return test_driver.set_rph_registration_mode("none");
+         * });
+         *
+         * navigator.registerProtocolHandler('web+soup', 'soup?url=%s');
+         *
+         * @param {String} mode - The `registration mode
+         *                        <https://html.spec.whatwg.org/multipage/system-state.html#registerprotocolhandler()-automation-mode>`_
+         *                        to set. Must be one of "``none``",
+         *                        "``autoAccept``", or
+         *                        "``autoReject``".
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the transaction mode has been set,
+         *                    or rejected if setting the mode fails.
+         */
+        set_rph_registration_mode: function(mode, context=null) {
+          return window.test_driver_internal.set_rph_registration_mode(mode, context);
+        },
+
+        /**
+         * Cancels the Federated Credential Management dialog
+         *
+         * Matches the `Cancel dialog
+         * <https://fedidcg.github.io/FedCM/#webdriver-canceldialog>`_
+         * WebDriver command.
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the dialog is canceled, or rejected
+         *                    in case the WebDriver command errors
+         */
+        cancel_fedcm_dialog: function(context=null) {
+            return window.test_driver_internal.cancel_fedcm_dialog(context);
+        },
+
+        /**
+         * Clicks a button on the Federated Credential Management dialog
+         *
+         * Matches the `Click dialog button
+         * <https://fedidcg.github.io/FedCM/#webdriver-clickdialogbutton>`_
+         * WebDriver command.
+         *
+         * @param {String} dialog_button - String enum representing the dialog button to click.
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the button is clicked,
+         *                    or rejected in case the WebDriver command errors
+         */
+        click_fedcm_dialog_button: function(dialog_button, context=null) {
+          return window.test_driver_internal.click_fedcm_dialog_button(dialog_button, context);
+        },
+
+        /**
+         * Selects an account from the Federated Credential Management dialog
+         *
+         * Matches the `Select account
+         * <https://fedidcg.github.io/FedCM/#webdriver-selectaccount>`_
+         * WebDriver command.
+         *
+         * @param {number} account_index - Index of the account to select.
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the account is selected,
+         *                    or rejected in case the WebDriver command errors
+         */
+        select_fedcm_account: function(account_index, context=null) {
+          return window.test_driver_internal.select_fedcm_account(account_index, context);
+        },
+
+        /**
+         * Gets the account list from the Federated Credential Management dialog
+         *
+         * Matches the `Account list
+         * <https://fedidcg.github.io/FedCM/#webdriver-accountlist>`_
+         * WebDriver command.
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} fulfilled after the account list is returned, or
+         *                    rejected in case the WebDriver command errors
+         */
+        get_fedcm_account_list: function(context=null) {
+          return window.test_driver_internal.get_fedcm_account_list(context);
+        },
+
+        /**
+         * Gets the title of the Federated Credential Management dialog
+         *
+         * Matches the `Get title
+         * <https://fedidcg.github.io/FedCM/#webdriver-gettitle>`_
+         * WebDriver command.
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the title is returned, or rejected
+         *                    in case the WebDriver command errors
+         */
+        get_fedcm_dialog_title: function(context=null) {
+          return window.test_driver_internal.get_fedcm_dialog_title(context);
+        },
+
+        /**
+         * Gets the type of the Federated Credential Management dialog
+         *
+         * Matches the `Get dialog type
+         * <https://fedidcg.github.io/FedCM/#webdriver-getdialogtype>`_
+         * WebDriver command.
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the dialog type is returned, or
+         *                    rejected in case the WebDriver command errors
+         */
+        get_fedcm_dialog_type: function(context=null) {
+          return window.test_driver_internal.get_fedcm_dialog_type(context);
+        },
+
+        /**
+         * Sets whether promise rejection delay is enabled for the Federated Credential Management dialog
+         *
+         * Matches the `Set delay enabled
+         * <https://fedidcg.github.io/FedCM/#webdriver-setdelayenabled>`_
+         * WebDriver command.
+         *
+         * @param {boolean} enabled - Whether to delay FedCM promise rejection.
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the delay has been enabled or disabled,
+         *                    or rejected in case the WebDriver command errors
+         */
+        set_fedcm_delay_enabled: function(enabled, context=null) {
+          return window.test_driver_internal.set_fedcm_delay_enabled(enabled, context);
+        },
+
+        /**
+         * Resets the Federated Credential Management dialog's cooldown
+         *
+         * Matches the `Reset cooldown
+         * <https://fedidcg.github.io/FedCM/#webdriver-resetcooldown>`_
+         * WebDriver command.
+         *
+         * @param {WindowProxy} context - Browsing context in which
+         *                                to run the call, or null for the current
+         *                                browsing context.
+         *
+         * @returns {Promise} Fulfilled after the cooldown has been reset,
+         *                    or rejected in case the WebDriver command errors
+         */
+        reset_fedcm_cooldown: function(context=null) {
+          return window.test_driver_internal.reset_fedcm_cooldown(context);
+        },
+
+        /**
+         * Creates a virtual sensor for use with the Generic Sensors APIs.
+         *
+         * Matches the `Create Virtual Sensor
+         * <https://w3c.github.io/sensors/#create-virtual-sensor-command>`_
+         * WebDriver command.
+         *
+         * Once created, a virtual sensor is available to all navigables under
+         * the same top-level traversable (i.e. all frames in the same page,
+         * regardless of origin).
+         *
+         * @param {String} sensor_type - A `virtual sensor type
+         *                               <https://w3c.github.io/sensors/#virtual-sensor-metadata-virtual-sensor-type>`_
+         *                               such as "accelerometer".
+         * @param {Object} [sensor_params={}] - Optional parameters described
+         *                                     in `Create Virtual Sensor
+         *                                     <https://w3c.github.io/sensors/#create-virtual-sensor-command>`_.
+         * @param {WindowProxy} [context=null] - Browsing context in which to
+         *                                       run the call, or null for the
+         *                                       current browsing context.
+         *
+         * @returns {Promise} Fulfilled when virtual sensor is created.
+         *                    Rejected in case the WebDriver command errors out
+         *                    (including if a virtual sensor of the same type
+         *                    already exists).
+         */
+        create_virtual_sensor: function(sensor_type, sensor_params={}, context=null) {
+          return window.test_driver_internal.create_virtual_sensor(sensor_type, sensor_params, context);
+        },
+
+        /**
+         * Causes a virtual sensor to report a new reading to any connected
+         * platform sensor.
+         *
+         * Matches the `Update Virtual Sensor Reading
+         * <https://w3c.github.io/sensors/#update-virtual-sensor-reading-command>`_
+         * WebDriver command.
+         *
+         * Note: The ``Promise`` it returns may fulfill before or after a
+         * "reading" event is fired. When using
+         * :js:func:`EventWatcher.wait_for`, it is necessary to take this into
+         * account:
+         *
+         * Note: New values may also be discarded due to the checks in `update
+         * latest reading
+         * <https://w3c.github.io/sensors/#update-latest-reading>`_.
+         *
+         * @example
+         * // Avoid races between EventWatcher and update_virtual_sensor().
+         * // This assumes you are sure this reading will be processed (see
+         * // the example below otherwise).
+         * const reading = { x: 1, y: 2, z: 3 };
+         * await Promise.all([
+         *   test_driver.update_virtual_sensor('gyroscope', reading),
+         *   watcher.wait_for('reading')
+         * ]);
+         *
+         * @example
+         * // Do not wait forever if you are not sure the reading will be
+         * // processed.
+         * const readingPromise = watcher.wait_for('reading');
+         * const timeoutPromise = new Promise(resolve => {
+         *     t.step_timeout(() => resolve('TIMEOUT', 3000))
+         * });
+         *
+         * const reading = { x: 1, y: 2, z: 3 };
+         * await test_driver.update_virtual_sensor('gyroscope', 'reading');
+         *
+         * const value =
+         *     await Promise.race([timeoutPromise, readingPromise]);
+         * if (value !== 'TIMEOUT') {
+         *   // Do something. The "reading" event was fired.
+         * }
+         *
+         * @param {String} sensor_type - A `virtual sensor type
+         *                               <https://w3c.github.io/sensors/#virtual-sensor-metadata-virtual-sensor-type>`_
+         *                               such as "accelerometer".
+         * @param {Object} reading - An Object describing a reading in a format
+         *                           dependent on ``sensor_type`` (e.g. ``{x:
+         *                           1, y: 2, z: 3}`` or ``{ illuminance: 42
+         *                           }``).
+         * @param {WindowProxy} [context=null] - Browsing context in which to
+         *                                       run the call, or null for the
+         *                                       current browsing context.
+         *
+         * @returns {Promise} Fulfilled after the reading update reaches the
+         *                    virtual sensor. Rejected in case the WebDriver
+         *                    command errors out (including if a virtual sensor
+         *                    of the given type does not exist).
+         */
+        update_virtual_sensor: function(sensor_type, reading, context=null) {
+          return window.test_driver_internal.update_virtual_sensor(sensor_type, reading, context);
+        },
+
+        /**
+         * Triggers the removal of a virtual sensor if it exists.
+         *
+         * Matches the `Delete Virtual Sensor
+         * <https://w3c.github.io/sensors/#delete-virtual-sensor-command>`_
+         * WebDriver command.
+         *
+         * @param {String} sensor_type - A `virtual sensor type
+         *                               <https://w3c.github.io/sensors/#virtual-sensor-metadata-virtual-sensor-type>`_
+         *                               such as "accelerometer".
+         * @param {WindowProxy} [context=null] - Browsing context in which to
+         *                                       run the call, or null for the
+         *                                       current browsing context.
+         *
+         * @returns {Promise} Fulfilled after the virtual sensor has been
+         *                    removed or if a sensor of the given type does not
+         *                    exist. Rejected in case the WebDriver command
+         *                    errors out.
+
+         */
+        remove_virtual_sensor: function(sensor_type, context=null) {
+          return window.test_driver_internal.remove_virtual_sensor(sensor_type, context);
+        },
+
+        /**
+         * Returns information about a virtual sensor.
+         *
+         * Matches the `Get Virtual Sensor Information
+         * <https://w3c.github.io/sensors/#get-virtual-sensor-information-command>`_
+         * WebDriver command.
+         *
+         * @param {String} sensor_type - A `virtual sensor type
+         *                               <https://w3c.github.io/sensors/#virtual-sensor-metadata-virtual-sensor-type>`_
+         *                               such as "accelerometer".
+         * @param {WindowProxy} [context=null] - Browsing context in which to
+         *                                       run the call, or null for the
+         *                                       current browsing context.
+         *
+         * @returns {Promise} Fulfilled with an Object with the properties
+         *                    described in `Get Virtual Sensor Information
+         *                    <https://w3c.github.io/sensors/#get-virtual-sensor-information-command>`_.
+         *                    Rejected in case the WebDriver command errors out
+         *                    (including if a virtual sensor of the given type
+         *                    does not exist).
+         */
+        get_virtual_sensor_information: function(sensor_type, context=null) {
+            return window.test_driver_internal.get_virtual_sensor_information(sensor_type, context);
+        },
+
+        /**
+         * Overrides device posture set by hardware.
+         *
+         * Matches the `Set device posture
+         * <https://w3c.github.io/device-posture/#set-device-posture>`_
+         * WebDriver command.
+         *
+         * @param {String} posture - A `DevicePostureType
+         *                           <https://w3c.github.io/device-posture/#dom-deviceposturetype>`_
+         *                           either "continuous" or "folded".
+         * @param {WindowProxy} [context=null] - Browsing context in which to
+         *                                       run the call, or null for the
+         *                                       current browsing context.
+         *
+         * @returns {Promise} Fulfilled when device posture is set.
+         *                    Rejected in case the WebDriver command errors out
+         *                    (including if a device posture of the given type
+         *                    does not exist).
+         */
+        set_device_posture: function(posture, context=null) {
+            return window.test_driver_internal.set_device_posture(posture, context);
+        },
+
+        /**
+         * Removes device posture override and returns device posture control
+         * back to hardware.
+         *
+         * Matches the `Clear device posture
+         * <https://w3c.github.io/device-posture/#clear-device-posture>`_
+         * WebDriver command.
+         *
+         * @param {WindowProxy} [context=null] - Browsing context in which to
+         *                                       run the call, or null for the
+         *                                       current browsing context.
+         *
+         * @returns {Promise} Fulfilled after the device posture override has
+         *                    been removed. Rejected in case the WebDriver
+         *                    command errors out.
+         */
+        clear_device_posture: function(context=null) {
+            return window.test_driver_internal.clear_device_posture(context);
+        }
+    };
+
+    window.test_driver_internal = {
+        /**
+         * This flag should be set to `true` by any code which implements the
+         * internal methods defined below for automation purposes. Doing so
+         * allows the library to signal failure immediately when an automated
+         * implementation of one of the methods is not available.
+         */
+        in_automation: false,
+
+        async click(element, coords) {
+            if (this.in_automation) {
+                throw new Error("click() is not implemented by testdriver-vendor.js");
+            }
+
+            return new Promise(function(resolve, reject) {
+                element.addEventListener("click", resolve);
+            });
+        },
+
+        async delete_all_cookies(context=null) {
+            throw new Error("delete_all_cookies() is not implemented by testdriver-vendor.js");
+        },
+
+        async get_all_cookies(context=null) {
+            throw new Error("get_all_cookies() is not implemented by testdriver-vendor.js");
+        },
+
+        async get_named_cookie(name, context=null) {
+            throw new Error("get_named_cookie() is not implemented by testdriver-vendor.js");
+        },
+
+        async send_keys(element, keys) {
+            if (this.in_automation) {
+                throw new Error("send_keys() is not implemented by testdriver-vendor.js");
+            }
+
+            return new Promise(function(resolve, reject) {
+                var seen = "";
+
+                function remove() {
+                    element.removeEventListener("keydown", onKeyDown);
+                }
+
+                function onKeyDown(event) {
+                    if (event.key.length > 1) {
+                        return;
+                    }
+
+                    seen += event.key;
+
+                    if (keys.indexOf(seen) !== 0) {
+                        reject(new Error("Unexpected key sequence: " + seen));
+                        remove();
+                    } else if (seen === keys) {
+                        resolve();
+                        remove();
+                    }
+                }
+
+                element.addEventListener("keydown", onKeyDown);
+            });
+        },
+
+        async freeze(context=null) {
+            throw new Error("freeze() is not implemented by testdriver-vendor.js");
+        },
+
+        async minimize_window(context=null) {
+            throw new Error("minimize_window() is not implemented by testdriver-vendor.js");
+        },
+
+        async set_window_rect(rect, context=null) {
+            throw new Error("set_window_rect() is not implemented by testdriver-vendor.js");
+        },
+
+        async get_window_rect(context=null) {
+            throw new Error("get_window_rect() is not implemented by testdriver-vendor.js");
+        },
+
+        async action_sequence(actions, context=null) {
+            throw new Error("action_sequence() is not implemented by testdriver-vendor.js");
+        },
+
+        async generate_test_report(message, context=null) {
+            throw new Error("generate_test_report() is not implemented by testdriver-vendor.js");
+        },
+
+        async set_permission(permission_params, context=null) {
+            throw new Error("set_permission() is not implemented by testdriver-vendor.js");
+        },
+
+        async add_virtual_authenticator(config, context=null) {
+            throw new Error("add_virtual_authenticator() is not implemented by testdriver-vendor.js");
+        },
+
+        async remove_virtual_authenticator(authenticator_id, context=null) {
+            throw new Error("remove_virtual_authenticator() is not implemented by testdriver-vendor.js");
+        },
+
+        async add_credential(authenticator_id, credential, context=null) {
+            throw new Error("add_credential() is not implemented by testdriver-vendor.js");
+        },
+
+        async get_credentials(authenticator_id, context=null) {
+            throw new Error("get_credentials() is not implemented by testdriver-vendor.js");
+        },
+
+        async remove_credential(authenticator_id, credential_id, context=null) {
+            throw new Error("remove_credential() is not implemented by testdriver-vendor.js");
+        },
+
+        async remove_all_credentials(authenticator_id, context=null) {
+            throw new Error("remove_all_credentials() is not implemented by testdriver-vendor.js");
+        },
+
+        async set_user_verified(authenticator_id, uv, context=null) {
+            throw new Error("set_user_verified() is not implemented by testdriver-vendor.js");
+        },
+
+        async set_storage_access(origin, embedding_origin, blocked, context=null) {
+            throw new Error("set_storage_access() is not implemented by testdriver-vendor.js");
+        },
+
+        async set_spc_transaction_mode(mode, context=null) {
+            throw new Error("set_spc_transaction_mode() is not implemented by testdriver-vendor.js");
+        },
+
+        set_rph_registration_mode: function(mode, context=null) {
+            return Promise.reject(new Error("unimplemented"));
+        },
+
+        async cancel_fedcm_dialog(context=null) {
+            throw new Error("cancel_fedcm_dialog() is not implemented by testdriver-vendor.js");
+        },
+
+        async click_fedcm_dialog_button(dialog_button, context=null) {
+            throw new Error("click_fedcm_dialog_button() is not implemented by testdriver-vendor.js");
+        },
+
+        async select_fedcm_account(account_index, context=null) {
+            throw new Error("select_fedcm_account() is not implemented by testdriver-vendor.js");
+        },
+
+        async get_fedcm_account_list(context=null) {
+            throw new Error("get_fedcm_account_list() is not implemented by testdriver-vendor.js");
+        },
+
+        async get_fedcm_dialog_title(context=null) {
+            throw new Error("get_fedcm_dialog_title() is not implemented by testdriver-vendor.js");
+        },
+
+        async get_fedcm_dialog_type(context=null) {
+            throw new Error("get_fedcm_dialog_type() is not implemented by testdriver-vendor.js");
+        },
+
+        async set_fedcm_delay_enabled(enabled, context=null) {
+            throw new Error("set_fedcm_delay_enabled() is not implemented by testdriver-vendor.js");
+        },
+
+        async reset_fedcm_cooldown(context=null) {
+            throw new Error("reset_fedcm_cooldown() is not implemented by testdriver-vendor.js");
+        },
+
+        async create_virtual_sensor(sensor_type, sensor_params, context=null) {
+            throw new Error("create_virtual_sensor() is not implemented by testdriver-vendor.js");
+        },
+
+        async update_virtual_sensor(sensor_type, reading, context=null) {
+            throw new Error("update_virtual_sensor() is not implemented by testdriver-vendor.js");
+        },
+
+        async remove_virtual_sensor(sensor_type, context=null) {
+            throw new Error("remove_virtual_sensor() is not implemented by testdriver-vendor.js");
+        },
+
+        async get_virtual_sensor_information(sensor_type, context=null) {
+            throw new Error("get_virtual_sensor_information() is not implemented by testdriver-vendor.js");
+        },
+
+        async set_device_posture(posture, context=null) {
+            throw new Error("set_device_posture() is not implemented by testdriver-vendor.js");
+        },
+
+        async clear_device_posture(context=null) {
+            throw new Error("clear_device_posture() is not implemented by testdriver-vendor.js");
+        }
+    };
+})();

+ 198 - 0
Tests/LibWeb/Text/input/wpt-import/wai-aria/scripts/aria-utils.js

@@ -0,0 +1,198 @@
+/* Utilities related to WAI-ARIA */
+
+const AriaUtils = {
+
+  /*
+  Tests simple role assignment: <div role="alert">x</div>
+  Not intended for nested, context-dependent, or other complex role tests.
+
+  Ex: AriaUtils.assignAndVerifyRolesByRoleNames(["group", "main", "button"])
+
+  */
+  assignAndVerifyRolesByRoleNames: function(roleNames) {
+    if (!Array.isArray(roleNames) || !roleNames.length) {
+      throw `Param roleNames of assignAndVerifyRolesByRoleNames("${roleNames}") should be an array containing at least one role string.`;
+    }
+    for (const role of roleNames) {
+      promise_test(async t => {
+        let el = document.createElement("div");
+        el.appendChild(document.createTextNode("x"));
+        el.setAttribute("role", role); // el.role not yet supported by Gecko.
+        document.body.appendChild(el);
+        const computedRole = await test_driver.get_computed_role(el);
+        assert_equals(computedRole, role, el.outerHTML);
+      }, `role: ${role}`);
+    }
+  },
+
+
+  /*
+  Tests computed ROLE of all elements matching selector
+  against the string value of their data-expectedrole attribute.
+
+  Ex: <div role="list"
+        data-testname="optional unique test name"
+        data-expectedrole="list"
+        class="ex">
+
+      AriaUtils.verifyRolesBySelector(".ex")
+
+  */
+  verifyRolesBySelector: function(selector, roleTestNamePrefix) {
+    const els = document.querySelectorAll(selector);
+    if (!els.length) {
+      throw `Selector passed in verifyRolesBySelector("${selector}") should match at least one element.`;
+    }
+    for (const el of els) {
+      let role = el.getAttribute("data-expectedrole");
+      let testName = el.getAttribute("data-testname") || role; // data-testname optional if role is unique per test file
+      if (typeof roleTestNamePrefix !== "undefined") {
+        testName = roleTestNamePrefix + testName;
+      }
+      promise_test(async t => {
+        const expectedRole = el.getAttribute("data-expectedrole");
+        const computedRole = await test_driver.get_computed_role(el);
+        assert_equals(computedRole, expectedRole, el.outerHTML);
+      }, `${testName}`);
+    }
+  },
+
+
+  /*
+  Tests computed ROLE of selected elements matching selector
+  against the string value of provided roles array.
+
+  Ex: <foo
+        data-testname="verify fooRole or barRole role on span"
+        class="ex-foo-or-bar">
+
+      AriaUtils.verifyRoleOrVariantRolesBySelector(".ex-foo-or-bar", ["fooRole", "barRole"]);
+
+  See also helper function verifyGenericRolesBySelector shorthand of the above using ["generic", "", "none"].
+
+  Note: This function should not be used to circumvent unexpected interop differences in implementations.
+  It should only be used in specific cases (like "generic") determined by ARIA WG or other spec maintainers to be acceptable for the purposes of testing.
+
+  */
+  verifyRoleOrVariantRolesBySelector: function(selector, roles) {
+    const els = document.querySelectorAll(selector);
+    if (!els.length) {
+      throw `Selector "${selector}" should match at least one element.`;
+    }
+    if (!roles.length || roles.length < 2) {
+      throw `Roles array ["${roles.join('", "')}"] should include at least two strings, a primary role and at least one acceptable implementation-specific variant. E.g. ["generic", "", "none"]…`;
+    }
+    for (const el of els) {
+      let testName = el.getAttribute("data-testname");
+      promise_test(async t => {
+        const expectedRoles = roles;
+        const computedRole = await test_driver.get_computed_role(el);
+        for (role of roles){
+          if (computedRole === role) {
+            return assert_equals(computedRole, role, `Computed Role: "${computedRole}" matches one of the acceptable role strings in ["${roles.join('", "')}"]: ${el.outerHTML}`);
+          }
+        }
+        return assert_false(true, `Computed Role: "${computedRole}" does not match any of the acceptable role strings in ["${roles.join('", "')}"]: ${el.outerHTML}`);
+      }, `${testName}`);
+    }
+  },
+
+
+  /*
+  Helper function for "generic" ROLE tests.
+
+  Ex: <span
+        data-testname="verify generic, none, or empty computed role on span"
+        class="ex-generic">
+
+      AriaUtils.verifyGenericRolesBySelector(".ex-generic");
+
+   This helper function is equivalant to AriaUtils.verifyRoleOrVariantRolesBySelector(".ex-generic", ["generic", "", "none"]);
+   See various issues and discussions linked from https://github.com/web-platform-tests/interop-accessibility/issues/48
+
+  */
+  verifyGenericRolesBySelector: function(selector) {
+    // ARIA WG determined implementation variants "none" (Chromium), and the empty string "" (WebKit), are sufficiently equivalent to "generic" for WPT test verification of HTML-AAM.
+    // See various discussions linked from https://github.com/web-platform-tests/interop-accessibility/issues/48
+    this.verifyRoleOrVariantRolesBySelector(selector, ["generic", "", "none"]);
+  },
+
+
+  /*
+  Tests computed LABEL of all elements matching selector
+  against the string value of their data-expectedlabel attribute.
+
+  Ex: <div aria-label="foo"
+        data-testname="optional unique test name"
+        data-expectedlabel="foo"
+        class="ex">
+
+      AriaUtils.verifyLabelsBySelector(".ex")
+
+  */
+  verifyLabelsBySelector: function(selector, labelTestNamePrefix) {
+    const els = document.querySelectorAll(selector);
+    if (!els.length) {
+      throw `Selector passed in verifyLabelsBySelector("${selector}") should match at least one element.`;
+    }
+    for (const el of els) {
+      let label = el.getAttribute("data-expectedlabel");
+      let testName = el.getAttribute("data-testname") || label; // data-testname optional if label is unique per test file
+      if (typeof labelTestNamePrefix !== "undefined") {
+        testName = labelTestNamePrefix + testName;
+      }
+      promise_test(async t => {
+        const expectedLabel = el.getAttribute("data-expectedlabel");
+        // XXX: Ladybird-specific change: upstream WPT has test_driver.get_computed_label(el) here,
+        // but we’ve changed that to the Ladybird-specific window.internals.getComputedLabel(el).
+        let computedLabel = await window.internals.getComputedLabel(el);
+        assert_not_equals(computedLabel, null, `get_computed_label(el) shouldn't return null for ${el.outerHTML}`);
+
+        // See:
+        // - https://github.com/w3c/accname/pull/165
+        // - https://github.com/w3c/accname/issues/192
+        // - https://github.com/w3c/accname/issues/208
+        //
+        // AccName references HTML's definition of ASCII Whitespace
+        // https://infra.spec.whatwg.org/#ascii-whitespace
+        // which matches tab (\t), newline (\n), formfeed (\f), return (\r), and regular space (\u0020).
+        // but it does NOT match non-breaking space (\xA0,\u00A0) and others matched by \s
+        const asciiWhitespace = /[\t\n\f\r\u0020]+/g;
+        computedLabel = computedLabel.replace(asciiWhitespace, '\u0020').replace(/^\u0020|\u0020$/g, '');
+
+        assert_equals(computedLabel, expectedLabel, el.outerHTML);
+      }, `${testName}`);
+    }
+  },
+
+
+  /*
+  Tests computed LABEL and ROLE of all elements matching selector using existing
+    verifyLabelsBySelector(), verifyRolesBySelector() functions and passes a test name prefix
+    to ensure uniqueness.
+
+  Ex: <div aria-label="foo" role="button"
+        data-testname="div with role=button is labelled via aria-label"
+        data-expectedlabel="foo"
+        data-expectedrole="button"
+        class="ex-role-and-label">
+
+      AriaUtils.verifyRolesAndLabelsBySelector(".ex-role-and-label")
+
+  */
+  verifyRolesAndLabelsBySelector: function(selector) {
+    let labelTestNamePrefix = "Label: ";
+    let roleTestNamePrefix = "Role: ";
+    const els = document.querySelectorAll(selector);
+    if (!els.length) {
+      throw `Selector passed in verifyRolesAndLabelsBySelector("${selector}") should match at least one element.`;
+    }
+    for (const el of els) {
+      el.classList.add("ex-label-only");
+      el.classList.add("ex-role-only");
+    }
+    this.verifyLabelsBySelector(".ex-label-only", labelTestNamePrefix);
+    this.verifyRolesBySelector(".ex-role-only", roleTestNamePrefix);
+  },
+};
+