From 120bc52f2384efd2345d6e84f508edc502f0e4da Mon Sep 17 00:00:00 2001 From: sideshowbarker Date: Wed, 30 Oct 2024 02:44:04 +0900 Subject: [PATCH] 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. --- .../accname/name/comp_embedded_control.txt | 36 + .../accname/name/comp_embedded_control.html | 134 ++ .../resources/testdriver-actions.js | 599 ++++++++ .../wpt-import/resources/testdriver-vendor.js | 1 + .../input/wpt-import/resources/testdriver.js | 1259 +++++++++++++++++ .../wpt-import/wai-aria/scripts/aria-utils.js | 198 +++ 6 files changed, 2227 insertions(+) create mode 100644 Tests/LibWeb/Text/expected/wpt-import/accname/name/comp_embedded_control.txt create mode 100644 Tests/LibWeb/Text/input/wpt-import/accname/name/comp_embedded_control.html create mode 100644 Tests/LibWeb/Text/input/wpt-import/resources/testdriver-actions.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/resources/testdriver-vendor.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/resources/testdriver.js create mode 100644 Tests/LibWeb/Text/input/wpt-import/wai-aria/scripts/aria-utils.js diff --git a/Tests/LibWeb/Text/expected/wpt-import/accname/name/comp_embedded_control.txt b/Tests/LibWeb/Text/expected/wpt-import/accname/name/comp_embedded_control.txt new file mode 100644 index 0000000000..9cdf9e2a0d --- /dev/null +++ b/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 diff --git a/Tests/LibWeb/Text/input/wpt-import/accname/name/comp_embedded_control.html b/Tests/LibWeb/Text/input/wpt-import/accname/name/comp_embedded_control.html new file mode 100644 index 0000000000..fc5cc2c1cb --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/accname/name/comp_embedded_control.html @@ -0,0 +1,134 @@ + + + + Name Comp: Embedded Control + + + + + + + + + +

Tests the #comp_embedded_control portions of the AccName Name Computation algorithm.

+ + + +

+ + + +

+ + + +

+ + + +

+ +

+ +

+ + + + + +

+ + + + +

+ +

+ +

+ +

+ +

+ +

+ + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/resources/testdriver-actions.js b/Tests/LibWeb/Text/input/wpt-import/resources/testdriver-actions.js new file mode 100644 index 0000000000..3e5ba74b4c --- /dev/null +++ b/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`_. + * + * @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.
" + + "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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_. For + * example, the "tab" key is represented as "``\uE004``". + * + * **Note:** these special-key codepoints are not necessarily + * what you would expect. For example, Esc is the + * invalid Unicode character ``\uE00C``, not the ``\u001B`` Escape + * character from ASCII. + * + * This matches the behaviour of the + * `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 + * `_. + * + * @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 + * `_ + * 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 + * `_ + * 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 + * `_ + * WebDriver command + * + * @param {Object} rect - A `WindowRect + * `_ + * @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 + * `_ + * 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 `_ + */ + 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 + * `_ 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * or derived object. + * @param {PermissionState} state - a `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 + * `_ + * WebDriver command. + * + * @param {Object} config - an `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 + * `_ + * 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 + * `_ + * WebDriver command. + * + * @param {String} authenticator_id - the ID of the authenticator + * @param {Object} credential - A `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 + * `_ + * 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 + * `_ + */ + 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 `_ into + * an automated 'autoAccept' or 'autoReject' mode, to allow testing + * without user interaction with the transaction UX prompt. + * + * Matches the `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 + * `_ + * 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 + * `_ 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * 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 + * `_ + * such as "accelerometer". + * @param {Object} [sensor_params={}] - Optional parameters described + * in `Create Virtual Sensor + * `_. + * @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 + * `_ + * 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 + * `_. + * + * @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 + * `_ + * 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 + * `_ + * WebDriver command. + * + * @param {String} sensor_type - A `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 + * `_ + * WebDriver command. + * + * @param {String} sensor_type - A `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 + * `_. + * 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 + * `_ + * WebDriver command. + * + * @param {String} posture - A `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 + * `_ + * 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"); + } + }; +})(); diff --git a/Tests/LibWeb/Text/input/wpt-import/wai-aria/scripts/aria-utils.js b/Tests/LibWeb/Text/input/wpt-import/wai-aria/scripts/aria-utils.js new file mode 100644 index 0000000000..ecb066693e --- /dev/null +++ b/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:
x
+ 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:
+ + 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: + + 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: + + 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:
+ + 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:
+ + 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); + }, +}; +