diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/resources | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/resources')
167 files changed, 32930 insertions, 0 deletions
diff --git a/testing/web-platform/tests/resources/.htaccess b/testing/web-platform/tests/resources/.htaccess new file mode 100644 index 0000000000..fd46101ca0 --- /dev/null +++ b/testing/web-platform/tests/resources/.htaccess @@ -0,0 +1,2 @@ +# make tests that use utf-16 not inherit the encoding for testharness.js et. al. +AddCharset utf-8 .css .js diff --git a/testing/web-platform/tests/resources/META.yml b/testing/web-platform/tests/resources/META.yml new file mode 100644 index 0000000000..64a240ccbe --- /dev/null +++ b/testing/web-platform/tests/resources/META.yml @@ -0,0 +1,2 @@ +suggested_reviewers: + - jgraham diff --git a/testing/web-platform/tests/resources/SVGAnimationTestCase-testharness.js b/testing/web-platform/tests/resources/SVGAnimationTestCase-testharness.js new file mode 100644 index 0000000000..9ebaf68039 --- /dev/null +++ b/testing/web-platform/tests/resources/SVGAnimationTestCase-testharness.js @@ -0,0 +1,102 @@ +// NOTE(edvardt): +// This file is a slimmed down wrapper for the old SVGAnimationTestCase.js, +// it has some convenience functions and should not be used for new tests. +// New tests should not build on this API as it's just meant to keep things +// working. + +// Helper functions +const xlinkNS = "http://www.w3.org/1999/xlink" + +function expectFillColor(element, red, green, blue, message) { + let color = window.getComputedStyle(element, null).fill; + var re = new RegExp("rgba?\\(([^, ]*), ([^, ]*), ([^, ]*)(?:, )?([^, ]*)\\)"); + rgb = re.exec(color); + + assert_approx_equals(Number(rgb[1]), red, 2.0, message); + assert_approx_equals(Number(rgb[2]), green, 2.0, message); + assert_approx_equals(Number(rgb[3]), blue, 2.0, message); +} + +function expectColor(element, red, green, blue, property) { + if (typeof property != "string") + color = getComputedStyle(element).getPropertyValue("color"); + else + color = getComputedStyle(element).getPropertyValue(property); + var re = new RegExp("rgba?\\(([^, ]*), ([^, ]*), ([^, ]*)(?:, )?([^, ]*)\\)"); + rgb = re.exec(color); + assert_approx_equals(Number(rgb[1]), red, 2.0); + assert_approx_equals(Number(rgb[2]), green, 2.0); + assert_approx_equals(Number(rgb[3]), blue, 2.0); +} + +function createSVGElement(type) { + return document.createElementNS("http://www.w3.org/2000/svg", type); +} + +// Inspired by Layoutests/animations/animation-test-helpers.js +function moveAnimationTimelineAndSample(index) { + var animationId = expectedResults[index][0]; + var time = expectedResults[index][1]; + var sampleCallback = expectedResults[index][2]; + var animation = rootSVGElement.ownerDocument.getElementById(animationId); + + // If we want to sample the animation end, add a small delta, to reliable point past the end of the animation. + newTime = time; + + // The sample time is relative to the start time of the animation, take that into account. + rootSVGElement.setCurrentTime(newTime); + + // NOTE(edvardt): + // This is a dumb hack, some of the old tests sampled before the animation start, this + // isn't technically part of the animation tests and is "impossible" to translate since + // tests start automatically. Thus I solved it by skipping it. + if (time != 0.0) + sampleCallback(); +} + +var currentTest = 0; +var expectedResults; + +function sampleAnimation(t) { + if (currentTest == expectedResults.length) { + t.done(); + return; + } + + moveAnimationTimelineAndSample(currentTest); + ++currentTest; + + step_timeout(t.step_func(function () { sampleAnimation(t); }), 0); +} + +function runAnimationTest(t, expected) { + if (!expected) + throw("Expected results are missing!"); + if (currentTest > 0) + throw("Not allowed to call runAnimationTest() twice"); + + expectedResults = expected; + testCount = expectedResults.length; + currentTest = 0; + + step_timeout(t.step_func(function () { sampleAnimation(this); }), 50); +} + +function smil_async_test(func) { + async_test(t => { + window.onload = t.step_func(function () { + // Pause animations, we'll drive them manually. + // This also ensures that the timeline is paused before + // it starts. This should make the instance time of the below + // 'click' (for instance) 0, and hence minimize rounding + // errors for the addition in moveAnimationTimelineAndSample. + rootSVGElement.pauseAnimations(); + + // If eg. an animation is running with begin="0s", and + // we want to sample the first time, before the animation + // starts, then we can't delay the testing by using an + // onclick event, as the animation would be past start time. + func(t); + }); + }); +} diff --git a/testing/web-platform/tests/resources/accesskey.js b/testing/web-platform/tests/resources/accesskey.js new file mode 100644 index 0000000000..e95c9d21e5 --- /dev/null +++ b/testing/web-platform/tests/resources/accesskey.js @@ -0,0 +1,34 @@ +/* + * Function that sends an accesskey using the proper key combination depending on the browser and OS. + * + * This needs that the test imports the following scripts: + * <script src="/resources/testdriver.js"></script> + * <script src="/resources/testdriver-actions.js"></script> + * <script src="/resources/testdriver-vendor.js"></script> +*/ +function pressAccessKey(accessKey){ + let controlKey = '\uE009'; // left Control key + let altKey = '\uE00A'; // left Alt key + let optionKey = altKey; // left Option key + let shiftKey = '\uE008'; // left Shift key + // There are differences in using accesskey across browsers and OS's. + // See: // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey + let isMacOSX = navigator.userAgent.indexOf("Mac") != -1; + let osAccessKey = isMacOSX ? [controlKey, optionKey] : [shiftKey, altKey]; + let actions = new test_driver.Actions(); + // Press keys. + for (let key of osAccessKey) { + actions = actions.keyDown(key); + } + actions = actions + .keyDown(accessKey) + .addTick() + .keyUp(accessKey); + osAccessKey.reverse(); + for (let key of osAccessKey) { + actions = actions.keyUp(key); + } + return actions.send(); +} + + diff --git a/testing/web-platform/tests/resources/blank.html b/testing/web-platform/tests/resources/blank.html new file mode 100644 index 0000000000..edeaa45bb6 --- /dev/null +++ b/testing/web-platform/tests/resources/blank.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <title>Blank Page</title> + <script> + window.onload = function(event) { + // This is needed to ensure the onload event fires when this page is + // opened as a popup. + // See https://github.com/web-platform-tests/wpt/pull/18157 + }; + </script> + </head> + <body> + </body> +</html> diff --git a/testing/web-platform/tests/resources/channel.sub.js b/testing/web-platform/tests/resources/channel.sub.js new file mode 100644 index 0000000000..370d4f5905 --- /dev/null +++ b/testing/web-platform/tests/resources/channel.sub.js @@ -0,0 +1,1097 @@ +(function() { + function randInt(bits) { + if (bits < 1 || bits > 53) { + throw new TypeError(); + } else { + if (bits >= 1 && bits <= 30) { + return 0 | ((1 << bits) * Math.random()); + } else { + var high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30); + var low = 0 | ((1 << 30) * Math.random()); + return high + low; + } + } + } + + + function toHex(x, length) { + var rv = x.toString(16); + while (rv.length < length) { + rv = "0" + rv; + } + return rv; + } + + function createUuid() { + return [toHex(randInt(32), 8), + toHex(randInt(16), 4), + toHex(0x4000 | randInt(12), 4), + toHex(0x8000 | randInt(14), 4), + toHex(randInt(48), 12)].join("-"); + } + + + /** + * Cache of WebSocket instances per channel + * + * For reading there can only be one channel with each UUID, so we + * just have a simple map of {uuid: WebSocket}. The socket can be + * closed when the channel is closed. + * + * For writing there can be many channels for each uuid. Those can + * share a websocket (within a specific global), so we have a map + * of {uuid: [WebSocket, count]}. Count is incremented when a + * channel is opened with a given uuid, and decremented when its + * closed. When the count reaches zero we can close the underlying + * socket. + */ + class SocketCache { + constructor() { + this.readSockets = new Map(); + this.writeSockets = new Map(); + }; + + async getOrCreate(type, uuid, onmessage=null) { + function createSocket() { + let protocol = self.isSecureContext ? "wss" : "ws"; + let port = self.isSecureContext? "{{ports[wss][0]}}" : "{{ports[ws][0]}}"; + let url = `${protocol}://{{host}}:${port}/msg_channel?uuid=${uuid}&direction=${type}`; + let socket = new WebSocket(url); + if (onmessage !== null) { + socket.onmessage = onmessage; + }; + return new Promise(resolve => socket.addEventListener("open", () => resolve(socket))); + } + + let socket; + if (type === "read") { + if (this.readSockets.has(uuid)) { + throw new Error("Can't create multiple read sockets with same UUID"); + } + socket = await createSocket(); + // If the socket is closed by the server, ensure it's removed from the cache + socket.addEventListener("close", () => this.readSockets.delete(uuid)); + this.readSockets.set(uuid, socket); + } else if (type === "write") { + let count; + if (onmessage !== null) { + throw new Error("Can't set message handler for write sockets"); + } + if (this.writeSockets.has(uuid)) { + [socket, count] = this.writeSockets.get(uuid); + } else { + socket = await createSocket(); + count = 0; + } + count += 1; + // If the socket is closed by the server, ensure it's removed from the cache + socket.addEventListener("close", () => this.writeSockets.delete(uuid)); + this.writeSockets.set(uuid, [socket, count]); + } else { + throw new Error(`Unknown type ${type}`); + } + return socket; + }; + + async close(type, uuid) { + let target = type === "read" ? this.readSockets : this.writeSockets; + const data = target.get(uuid); + if (!data) { + return; + } + let count, socket; + if (type == "read") { + socket = data; + count = 0; + } else if (type === "write") { + [socket, count] = data; + count -= 1; + if (count > 0) { + target.set(uuid, [socket, count]); + } + }; + if (count <= 0 && socket) { + target.delete(uuid); + socket.close(1000); + await new Promise(resolve => socket.addEventListener("close", resolve)); + } + }; + + async closeAll() { + let sockets = []; + this.readSockets.forEach(value => sockets.push(value)); + this.writeSockets.forEach(value => sockets.push(value[0])); + let closePromises = sockets.map(socket => + new Promise(resolve => socket.addEventListener("close", resolve))); + sockets.forEach(socket => socket.close(1000)); + this.readSockets.clear(); + this.writeSockets.clear(); + await Promise.all(closePromises); + } + } + + const socketCache = new SocketCache(); + + /** + * Abstract base class for objects that allow sending / receiving + * messages over a channel. + */ + class Channel { + type = null; + + constructor(uuid) { + /** UUID for the channel */ + this.uuid = uuid; + this.socket = null; + this.eventListeners = { + connect: new Set(), + close: new Set() + }; + } + + hasConnection() { + return this.socket !== null && this.socket.readyState <= WebSocket.OPEN; + } + + /** + * Connect to the channel. + * + * @param {Function} onmessage - Event handler function for + * the underlying websocket message. + */ + async connect(onmessage) { + if (this.hasConnection()) { + return; + } + this.socket = await socketCache.getOrCreate(this.type, this.uuid, onmessage); + this._dispatch("connect"); + } + + /** + * Close the channel and underlying websocket connection + */ + async close() { + this.socket = null; + await socketCache.close(this.type, this.uuid); + this._dispatch("close"); + } + + /** + * Add an event callback function. Supported message types are + * "connect", "close", and "message" (for ``RecvChannel``). + * + * @param {string} type - Message type. + * @param {Function} fn - Callback function. This is called + * with an event-like object, with ``type`` and ``data`` + * properties. + */ + addEventListener(type, fn) { + if (typeof type !== "string") { + throw new TypeError(`Expected string, got ${typeof type}`); + } + if (typeof fn !== "function") { + throw new TypeError(`Expected function, got ${typeof fn}`); + } + if (!this.eventListeners.hasOwnProperty(type)) { + throw new Error(`Unrecognised event type ${type}`); + } + this.eventListeners[type].add(fn); + }; + + /** + * Remove an event callback function. + * + * @param {string} type - Event type. + * @param {Function} fn - Callback function to remove. + */ + removeEventListener(type, fn) { + if (!typeof type === "string") { + throw new TypeError(`Expected string, got ${typeof type}`); + } + if (typeof fn !== "function") { + throw new TypeError(`Expected function, got ${typeof fn}`); + } + let listeners = this.eventListeners[type]; + if (listeners) { + listeners.delete(fn); + } + }; + + _dispatch(type, data) { + let listeners = this.eventListeners[type]; + if (listeners) { + // If any listener throws we end up not calling the other + // listeners. This hopefully makes debugging easier, but + // is different to DOM event listeners. + listeners.forEach(fn => fn({type, data})); + } + }; + + } + + /** + * Send messages over a channel + */ + class SendChannel extends Channel { + type = "write"; + + /** + * Connect to the channel. Automatically called when sending the + * first message. + */ + async connect() { + return super.connect(null); + } + + async _send(cmd, body=null) { + if (!this.hasConnection()) { + await this.connect(); + } + this.socket.send(JSON.stringify([cmd, body])); + } + + /** + * Send a message. The message object must be JSON-serializable. + * + * @param {Object} msg - The message object to send. + */ + async send(msg) { + await this._send("message", msg); + } + + /** + * Disconnect the associated `RecvChannel <#RecvChannel>`_, if + * any, on the server side. + */ + async disconnectReader() { + await this._send("disconnectReader"); + } + + /** + * Disconnect this channel on the server side. + */ + async delete() { + await this._send("delete"); + } + }; + self.SendChannel = SendChannel; + + const recvChannelsCreated = new Set(); + + /** + * Receive messages over a channel + */ + class RecvChannel extends Channel { + type = "read"; + + constructor(uuid) { + if (recvChannelsCreated.has(uuid)) { + throw new Error(`Already created RecvChannel with id ${uuid}`); + } + super(uuid); + this.eventListeners.message = new Set(); + } + + async connect() { + if (this.hasConnection()) { + return; + } + await super.connect(event => this.readMessage(event.data)); + } + + readMessage(data) { + let msg = JSON.parse(data); + this._dispatch("message", msg); + } + + /** + * Wait for the next message and return it (after passing it to + * existing handlers) + * + * @returns {Promise} - Promise that resolves to the message data. + */ + nextMessage() { + return new Promise(resolve => { + let fn = ({data}) => { + this.removeEventListener("message", fn); + resolve(data); + }; + this.addEventListener("message", fn); + }); + } + } + + /** + * Create a new channel pair + * + * @returns {Array} - Array of [RecvChannel, SendChannel] for the same channel. + */ + self.channel = function() { + let uuid = createUuid(); + let recvChannel = new RecvChannel(uuid); + let sendChannel = new SendChannel(uuid); + return [recvChannel, sendChannel]; + }; + + /** + * Create an unconnected channel defined by a `uuid` in + * ``location.href`` for listening for `RemoteGlobal + * <#RemoteGlobal>`_ messages. + * + * @returns {RemoteGlobalCommandRecvChannel} - Disconnected channel + */ + self.global_channel = function() { + let uuid = new URLSearchParams(location.search).get("uuid"); + if (!uuid) { + throw new Error("URL must have a uuid parameter to use as a RemoteGlobal"); + } + return new RemoteGlobalCommandRecvChannel(new RecvChannel(uuid)); + }; + + /** + * Start listening for `RemoteGlobal <#RemoteGlobal>`_ messages on + * a channel defined by a `uuid` in `location.href` + * + * @returns {RemoteGlobalCommandRecvChannel} - Connected channel + */ + self.start_global_channel = async function() { + let channel = self.global_channel(); + await channel.connect(); + return channel; + }; + + /** + * Close all WebSockets used by channels in the current realm. + * + */ + self.close_all_channel_sockets = async function() { + await socketCache.closeAll(); + // Spinning the event loop after the close events is necessary to + // ensure that the channels really are closed and don't affect + // bfcache behaviour in at least some implementations. + await new Promise(resolve => setTimeout(resolve, 0)); + }; + + /** + * Handler for `RemoteGlobal <#RemoteGlobal>`_ commands. + * + * This can't be constructed directly but must be obtained from + * `global_channel() <#global_channel>`_ or + * `start_global_channel() <#start_global_channel>`_. + */ + class RemoteGlobalCommandRecvChannel { + constructor(recvChannel) { + this.channel = recvChannel; + this.uuid = recvChannel.uuid; + this.channel.addEventListener("message", ({data}) => this.handleMessage(data)); + this.messageHandlers = new Set(); + }; + + /** + * Connect to the channel and start handling messages. + */ + async connect() { + await this.channel.connect(); + } + + /** + * Close the channel and underlying websocket connection + */ + async close() { + await this.channel.close(); + } + + async handleMessage(msg) { + const {id, command, params, respChannel} = msg; + let result = {}; + let resp = {id, result}; + if (command === "call") { + const fn = deserialize(params.fn); + const args = params.args.map(deserialize); + try { + let resultValue = await fn(...args); + result.result = serialize(resultValue); + } catch(e) { + let exception = serialize(e); + const getAsInt = (obj, prop) => { + let value = prop in obj ? parseInt(obj[prop]) : 0; + return Number.isNaN(value) ? 0 : value; + }; + result.exceptionDetails = { + text: e.toString(), + lineNumber: getAsInt(e, "lineNumber"), + columnNumber: getAsInt(e, "columnNumber"), + exception + }; + } + } else if (command === "postMessage") { + this.messageHandlers.forEach(fn => fn(deserialize(params.msg))); + } + if (respChannel) { + let chan = deserialize(respChannel); + await chan.connect(); + await chan.send(resp); + } + } + + /** + * Add a handler for ``postMessage`` messages + * + * @param {Function} fn - Callback function that receives the + * message. + */ + addMessageHandler(fn) { + this.messageHandlers.add(fn); + } + + /** + * Remove a handler for ``postMessage`` messages + * + * @param {Function} fn - Callback function to remove + */ + removeMessageHandler(fn) { + this.messageHandlers.delete(fn); + } + + /** + * Wait for the next ``postMessage`` message and return it + * (after passing it to existing handlers) + * + * @returns {Promise} - Promise that resolves to the message. + */ + nextMessage() { + return new Promise(resolve => { + let fn = (msg) => { + this.removeMessageHandler(fn); + resolve(msg); + }; + this.addMessageHandler(fn); + }); + } + } + + class RemoteGlobalResponseRecvChannel { + constructor(recvChannel) { + this.channel = recvChannel; + this.channel.addEventListener("message", ({data}) => this.handleMessage(data)); + this.responseHandlers = new Map(); + } + + setResponseHandler(commandId, fn) { + this.responseHandlers.set(commandId, fn); + } + + handleMessage(msg) { + let {id, result} = msg; + let handler = this.responseHandlers.get(id); + if (handler) { + this.responseHandlers.delete(id); + handler(result); + } + } + + close() { + return this.channel.close(); + } + } + + /** + * Object representing a remote global that has a + * `RemoteGlobalCommandRecvChannel + * <#RemoteGlobalCommandRecvChannel>`_ + */ + class RemoteGlobal { + /** + * Create a new RemoteGlobal object. + * + * This doesn't actually construct the global itself; that + * must be done elsewhere, with a ``uuid`` query parameter in + * its URL set to the same as the ``uuid`` property of this + * object. + * + * @param {SendChannel|string} [dest] - Either a SendChannel + * to the destination, or the UUID of the destination. If + * ommitted, a new UUID is generated, which can be used when + * constructing the URL for the global. + * + */ + constructor(dest) { + if (dest === undefined || dest === null) { + dest = createUuid(); + } + if (typeof dest == "string") { + /** UUID for the global */ + this.uuid = dest; + this.sendChannel = new SendChannel(dest); + } else if (dest instanceof SendChannel) { + this.sendChannel = dest; + this.uuid = dest.uuid; + } else { + throw new TypeError("Unrecognised type, expected string or SendChannel"); + } + this.recvChannel = null; + this.respChannel = null; + this.connected = false; + this.commandId = 0; + } + + /** + * Connect to the channel. Automatically called when sending the + * first message + */ + async connect() { + if (this.connected) { + return; + } + let [recvChannel, respChannel] = self.channel(); + await Promise.all([this.sendChannel.connect(), recvChannel.connect()]); + this.recvChannel = new RemoteGlobalResponseRecvChannel(recvChannel); + this.respChannel = respChannel; + this.connected = true; + } + + async sendMessage(command, params, hasResp=true) { + if (!this.connected) { + await this.connect(); + } + let msg = {id: this.commandId++, command, params}; + if (hasResp) { + msg.respChannel = serialize(this.respChannel); + } + let response; + if (hasResp) { + response = new Promise(resolve => + this.recvChannel.setResponseHandler(msg.id, resolve)); + } else { + response = null; + } + this.sendChannel.send(msg); + return await response; + } + + /** + * Run the function ``fn`` in the remote global, passing arguments + * ``args``, and return the result after awaiting any returned + * promise. + * + * @param {Function} fn - Function to run in the remote global. + * @param {...Any} args - Arguments to pass to the function + * @returns {Promise} - Promise resolving to the return value + * of the function. + */ + async call(fn, ...args) { + let result = await this.sendMessage("call", {fn: serialize(fn), args: args.map(x => serialize(x))}, true); + if (result.exceptionDetails) { + throw deserialize(result.exceptionDetails.exception); + } + return deserialize(result.result); + } + + /** + * Post a message to the remote + * + * @param {Any} msg - The message to send. + */ + async postMessage(msg) { + await this.sendMessage("postMessage", {msg: serialize(msg)}, false); + } + + /** + * Disconnect the associated `RemoteGlobalCommandRecvChannel + * <#RemoteGlobalCommandRecvChannel>`_, if any, on the server + * side. + * + * @returns {Promise} - Resolved once the channel is disconnected. + */ + disconnectReader() { + // This causes any readers to disconnect until they are explicitly reconnected + return this.sendChannel.disconnectReader(); + } + + /** + * Close the channel and underlying websocket connections + */ + close() { + let closers = [this.sendChannel.close()]; + if (this.recvChannel !== null) { + closers.push(this.recvChannel.close()); + } + if (this.respChannel !== null) { + closers.push(this.respChannel.close()); + } + return Promise.all(closers); + } + } + + self.RemoteGlobal = RemoteGlobal; + + function typeName(value) { + let type = typeof value; + if (type === "undefined" || + type === "string" || + type === "boolean" || + type === "number" || + type === "bigint" || + type === "symbol" || + type === "function") { + return type; + } + + if (value === null) { + return "null"; + } + // The handling of cross-global objects here is broken + if (value instanceof RemoteObject) { + return "remoteobject"; + } + if (value instanceof SendChannel) { + return "sendchannel"; + } + if (value instanceof RecvChannel) { + return "recvchannel"; + } + if (value instanceof Error) { + return "error"; + } + if (Array.isArray(value)) { + return "array"; + } + let constructor = value.constructor && value.constructor.name; + if (constructor === "RegExp" || + constructor === "Date" || + constructor === "Map" || + constructor === "Set" || + constructor == "WeakMap" || + constructor == "WeakSet") { + return constructor.toLowerCase(); + } + // The handling of cross-global objects here is broken + if (typeof window == "object" && window === self) { + if (value instanceof Element) { + return "element"; + } + if (value instanceof Document) { + return "document"; + } + if (value instanceof Node) { + return "node"; + } + if (value instanceof Window) { + return "window"; + } + } + if (Promise.resolve(value) === value) { + return "promise"; + } + return "object"; + } + + let remoteObjectsById = new Map(); + + function remoteId(obj) { + let rv; + rv = createUuid(); + remoteObjectsById.set(rv, obj); + return rv; + } + + /** + * Representation of a non-primitive type passed through a channel + */ + class RemoteObject { + constructor(type, objectId) { + this.type = type; + this.objectId = objectId; + } + + /** + * Create a RemoteObject containing a handle to reference obj + * + * @param {Any} obj - The object to reference. + */ + static from(obj) { + let type = typeName(obj); + let id = remoteId(obj); + return new RemoteObject(type, id); + } + + /** + * Return the local object referenced by the ``objectId`` of + * this ``RemoteObject``, or ``null`` if there isn't a such an + * object in this realm. + */ + toLocal() { + if (remoteObjectsById.has(this.objectId)) { + return remoteObjectsById.get(this.objectId); + } + return null; + } + + /** + * Remove the object from the local cache. This means that future + * calls to ``toLocal`` with the same objectId will always return + * ``null``. + */ + delete() { + remoteObjectsById.delete(this.objectId); + } + } + + self.RemoteObject = RemoteObject; + + /** + * Serialize an object as a JSON-compatible representation. + * + * The format used is similar (but not identical to) + * `WebDriver-BiDi + * <https://w3c.github.io/webdriver-bidi/#data-types-protocolValue>`_. + * + * Each item to be serialized can have the following fields: + * + * type - The name of the type being represented e.g. "string", or + * "map". For primitives this matches ``typeof``, but for + * ``object`` types that have particular support in the protocol + * e.g. arrays and maps, it is a custom value. + * + * value - A serialized representation of the object value. For + * container types this is a JSON container (i.e. an object or an + * array) containing a serialized representation of the child + * values. + * + * objectId - An integer used to handle object graphs. Where + * an object is present more than once in the serialization, the + * first instance has both ``value`` and ``objectId`` fields, but + * when encountered again, only ``objectId`` is present, with the + * same value as the first instance of the object. + * + * @param {Any} inValue - The value to be serialized. + * @returns {Object} - The serialized object value. + */ + function serialize(inValue) { + const queue = [{item: inValue}]; + let outValue = null; + + // Map from container object input to output value + let objectsSeen = new Map(); + let lastObjectId = 0; + + /* Instead of making this recursive, use a queue holding the objects to be + * serialized. Each item in the queue can have the following properties: + * + * item (required) - the input item to be serialized + * + * target - For collections, the output serialized object to + * which the serialization of the current item will be added. + * + * targetName - For serializing object members, the name of + * the property. For serializing maps either "key" or "value", + * depending on whether the item represents a key or a value + * in the map. + */ + while (queue.length > 0) { + const {item, target, targetName} = queue.shift(); + let type = typeName(item); + + let serialized = {type}; + + if (objectsSeen.has(item)) { + let outputValue = objectsSeen.get(item); + if (!outputValue.hasOwnProperty("objectId")) { + outputValue.objectId = lastObjectId++; + } + serialized.objectId = outputValue.objectId; + } else { + switch (type) { + case "undefined": + case "null": + break; + case "string": + case "boolean": + serialized.value = item; + break; + case "number": + if (item !== item) { + serialized.value = "NaN"; + } else if (item === 0 && 1/item == Number.NEGATIVE_INFINITY) { + serialized.value = "-0"; + } else if (item === Number.POSITIVE_INFINITY) { + serialized.value = "+Infinity"; + } else if (item === Number.NEGATIVE_INFINITY) { + serialized.value = "-Infinity"; + } else { + serialized.value = item; + } + break; + case "bigint": + case "function": + serialized.value = item.toString(); + break; + case "remoteobject": + serialized.value = { + type: item.type, + objectId: item.objectId + }; + break; + case "sendchannel": + serialized.value = item.uuid; + break; + case "regexp": + serialized.value = { + pattern: item.source, + flags: item.flags + }; + break; + case "date": + serialized.value = Date.prototype.toJSON.call(item); + break; + case "error": + serialized.value = { + type: item.constructor.name, + name: item.name, + message: item.message, + lineNumber: item.lineNumber, + columnNumber: item.columnNumber, + fileName: item.fileName, + stack: item.stack, + }; + break; + case "array": + case "set": + serialized.value = []; + for (let child of item) { + queue.push({item: child, target: serialized}); + } + break; + case "object": + serialized.value = {}; + for (let [targetName, child] of Object.entries(item)) { + queue.push({item: child, target: serialized, targetName}); + } + break; + case "map": + serialized.value = []; + for (let [childKey, childValue] of item.entries()) { + queue.push({item: childKey, target: serialized, targetName: "key"}); + queue.push({item: childValue, target: serialized, targetName: "value"}); + } + break; + default: + throw new TypeError(`Can't serialize value of type ${type}; consider using RemoteObject.from() to wrap the object`); + }; + } + if (serialized.objectId === undefined) { + objectsSeen.set(item, serialized); + } + + if (target === undefined) { + if (outValue !== null) { + throw new Error("Tried to create multiple output values"); + } + outValue = serialized; + } else { + switch (target.type) { + case "array": + case "set": + target.value.push(serialized); + break; + case "object": + target.value[targetName] = serialized; + break; + case "map": + // We always serialize key and value as adjacent items in the queue, + // so when we get the key push a new output array and then the value will + // be added on the next iteration. + if (targetName === "key") { + target.value.push([]); + } + target.value[target.value.length - 1].push(serialized); + break; + default: + throw new Error(`Unknown collection target type ${target.type}`); + } + } + } + return outValue; + } + + /** + * Deserialize an object from a JSON-compatible representation. + * + * For details on the serialized representation see serialize(). + * + * @param {Object} obj - The value to be deserialized. + * @returns {Any} - The deserialized value. + */ + function deserialize(obj) { + let deserialized = null; + let queue = [{item: obj, target: null}]; + let objectMap = new Map(); + + /* Instead of making this recursive, use a queue holding the objects to be + * deserialized. Each item in the queue has the following properties: + * + * item - The input item to be deserialised. + * + * target - For members of a collection, a wrapper around the + * output collection. This has a ``type`` field which is the + * name of the collection type, and a ``value`` field which is + * the actual output collection. For primitives, this is null. + * + * targetName - For object members, the property name on the + * output object. For maps, "key" if the item is a key in the output map, + * or "value" if it's a value in the output map. + */ + while (queue.length > 0) { + const {item, target, targetName} = queue.shift(); + const {type, value, objectId} = item; + let result; + let newTarget; + if (objectId !== undefined && value === undefined) { + result = objectMap.get(objectId); + } else { + switch(type) { + case "undefined": + result = undefined; + break; + case "null": + result = null; + break; + case "string": + case "boolean": + result = value; + break; + case "number": + if (typeof value === "string") { + switch(value) { + case "NaN": + result = NaN; + break; + case "-0": + result = -0; + break; + case "+Infinity": + result = Number.POSITIVE_INFINITY; + break; + case "-Infinity": + result = Number.NEGATIVE_INFINITY; + break; + default: + throw new Error(`Unexpected number value "${value}"`); + } + } else { + result = value; + } + break; + case "bigint": + result = BigInt(value); + break; + case "function": + result = new Function("...args", `return (${value}).apply(null, args)`); + break; + case "remoteobject": + let remote = new RemoteObject(value.type, value.objectId); + let local = remote.toLocal(); + if (local !== null) { + result = local; + } else { + result = remote; + } + break; + case "sendchannel": + result = new SendChannel(value); + break; + case "regexp": + result = new RegExp(value.pattern, value.flags); + break; + case "date": + result = new Date(value); + break; + case "error": + // The item.value.type property is the name of the error constructor. + // If we have a constructor with the same name in the current realm, + // construct an instance of that type, otherwise use a generic Error + // type. + if (item.value.type in self && + typeof self[item.value.type] === "function") { + result = new self[item.value.type](item.value.message); + } else { + result = new Error(item.value.message); + } + result.name = item.value.name; + result.lineNumber = item.value.lineNumber; + result.columnNumber = item.value.columnNumber; + result.fileName = item.value.fileName; + result.stack = item.value.stack; + break; + case "array": + result = []; + newTarget = {type, value: result}; + for (let child of value) { + queue.push({item: child, target: newTarget}); + } + break; + case "set": + result = new Set(); + newTarget = {type, value: result}; + for (let child of value) { + queue.push({item: child, target: newTarget}); + } + break; + case "object": + result = {}; + newTarget = {type, value: result}; + for (let [targetName, child] of Object.entries(value)) { + queue.push({item: child, target: newTarget, targetName}); + } + break; + case "map": + result = new Map(); + newTarget = {type, value: result}; + for (let [key, child] of value) { + queue.push({item: key, target: newTarget, targetName: "key"}); + queue.push({item: child, target: newTarget, targetName: "value"}); + } + break; + default: + throw new TypeError(`Can't deserialize object of type ${type}`); + } + if (objectId !== undefined) { + objectMap.set(objectId, result); + } + } + + if (target === null) { + if (deserialized !== null) { + throw new Error(`Tried to deserialized a non-root output value without a target` + ` container object.`); + } + deserialized = result; + } else { + switch(target.type) { + case "array": + target.value.push(result); + break; + case "set": + target.value.add(result); + break; + case "object": + target.value[targetName] = result; + break; + case "map": + // For maps the same target wrapper is shared between key and value. + // After deserializing the key, set the `key` property on the target + // until we come to the value. + if (targetName === "key") { + target.key = result; + } else { + target.value.set(target.key, result); + } + break; + default: + throw new Error(`Unknown target type ${target.type}`); + } + } + } + return deserialized; + } +})(); diff --git a/testing/web-platform/tests/resources/check-layout-th.js b/testing/web-platform/tests/resources/check-layout-th.js new file mode 100644 index 0000000000..54ddb35f31 --- /dev/null +++ b/testing/web-platform/tests/resources/check-layout-th.js @@ -0,0 +1,253 @@ +(function() { +// Test is initiated from body.onload, so explicit done() call is required. +setup({ explicit_done: true }); + +function checkSubtreeExpectedValues(t, parent, prefix) +{ + var checkedLayout = checkExpectedValues(t, parent, prefix); + Array.prototype.forEach.call(parent.childNodes, function(node) { + checkedLayout |= checkSubtreeExpectedValues(t, node, prefix); + }); + return checkedLayout; +} + +function checkAttribute(output, node, attribute) +{ + var result = node.getAttribute && node.getAttribute(attribute); + output.checked |= !!result; + return result; +} + +function assert_tolerance(actual, expected, message) +{ + if (isNaN(expected) || isNaN(actual) || Math.abs(actual - expected) >= 1) { + assert_equals(actual, Number(expected), message); + } +} + +function checkDataKeys(node) { + // The purpose of this list of data-* attributes is simply to ensure typos + // in tests are caught. It is therefore "ok" to add to this list for + // specific tests. + var validData = new Set([ + "data-anchor-polyfill", + "data-expected-width", + "data-expected-height", + "data-offset-x", + "data-offset-y", + "data-expected-client-width", + "data-expected-client-height", + "data-expected-scroll-width", + "data-expected-scroll-height", + "data-expected-bounding-client-rect-width", + "data-expected-bounding-client-rect-height", + "data-total-x", + "data-total-y", + "data-expected-display", + "data-expected-padding-top", + "data-expected-padding-bottom", + "data-expected-padding-left", + "data-expected-padding-right", + "data-expected-margin-top", + "data-expected-margin-bottom", + "data-expected-margin-left", + "data-expected-margin-right" + ]); + if (!node || !node.getAttributeNames) + return; + // Use "data-test" prefix if you need custom-named data elements. + for (let name of node.getAttributeNames()) { + if (name.startsWith("data-") && !name.startsWith("data-test")) + assert_true(validData.has(name), name + " is a valid data attribute"); + } +} + +function checkExpectedValues(t, node, prefix) +{ + checkDataKeys(node); + var output = { checked: false }; + + var expectedWidth = checkAttribute(output, node, "data-expected-width"); + if (expectedWidth) { + assert_tolerance(node.offsetWidth, expectedWidth, prefix + "width"); + } + + var expectedHeight = checkAttribute(output, node, "data-expected-height"); + if (expectedHeight) { + assert_tolerance(node.offsetHeight, expectedHeight, prefix + "height"); + } + + var expectedOffset = checkAttribute(output, node, "data-offset-x"); + if (expectedOffset) { + assert_tolerance(node.offsetLeft, expectedOffset, prefix + "offsetLeft"); + } + + var expectedOffset = checkAttribute(output, node, "data-offset-y"); + if (expectedOffset) { + assert_tolerance(node.offsetTop, expectedOffset, prefix + "offsetTop"); + } + + var expectedWidth = checkAttribute(output, node, "data-expected-client-width"); + if (expectedWidth) { + assert_tolerance(node.clientWidth, expectedWidth, prefix + "clientWidth"); + } + + var expectedHeight = checkAttribute(output, node, "data-expected-client-height"); + if (expectedHeight) { + assert_tolerance(node.clientHeight, expectedHeight, prefix + "clientHeight"); + } + + var expectedWidth = checkAttribute(output, node, "data-expected-scroll-width"); + if (expectedWidth) { + assert_tolerance(node.scrollWidth, expectedWidth, prefix + "scrollWidth"); + } + + var expectedHeight = checkAttribute(output, node, "data-expected-scroll-height"); + if (expectedHeight) { + assert_tolerance(node.scrollHeight, expectedHeight, prefix + "scrollHeight"); + } + + var expectedWidth = checkAttribute(output, node, "data-expected-bounding-client-rect-width"); + if (expectedWidth) { + assert_tolerance(node.getBoundingClientRect().width, expectedWidth, prefix + "getBoundingClientRect().width"); + } + + var expectedHeight = checkAttribute(output, node, "data-expected-bounding-client-rect-height"); + if (expectedHeight) { + assert_tolerance(node.getBoundingClientRect().height, expectedHeight, prefix + "getBoundingClientRect().height"); + } + + var expectedOffset = checkAttribute(output, node, "data-total-x"); + if (expectedOffset) { + var totalLeft = node.clientLeft + node.offsetLeft; + assert_tolerance(totalLeft, expectedOffset, prefix + + "clientLeft+offsetLeft (" + node.clientLeft + " + " + node.offsetLeft + ")"); + } + + var expectedOffset = checkAttribute(output, node, "data-total-y"); + if (expectedOffset) { + var totalTop = node.clientTop + node.offsetTop; + assert_tolerance(totalTop, expectedOffset, prefix + + "clientTop+offsetTop (" + node.clientTop + " + " + node.offsetTop + ")"); + } + + var expectedDisplay = checkAttribute(output, node, "data-expected-display"); + if (expectedDisplay) { + var actualDisplay = getComputedStyle(node).display; + assert_equals(actualDisplay, expectedDisplay, prefix + "display"); + } + + var expectedPaddingTop = checkAttribute(output, node, "data-expected-padding-top"); + if (expectedPaddingTop) { + var actualPaddingTop = getComputedStyle(node).paddingTop; + // Trim the unit "px" from the output. + actualPaddingTop = actualPaddingTop.slice(0, -2); + assert_equals(actualPaddingTop, expectedPaddingTop, prefix + "padding-top"); + } + + var expectedPaddingBottom = checkAttribute(output, node, "data-expected-padding-bottom"); + if (expectedPaddingBottom) { + var actualPaddingBottom = getComputedStyle(node).paddingBottom; + // Trim the unit "px" from the output. + actualPaddingBottom = actualPaddingBottom.slice(0, -2); + assert_equals(actualPaddingBottom, expectedPaddingBottom, prefix + "padding-bottom"); + } + + var expectedPaddingLeft = checkAttribute(output, node, "data-expected-padding-left"); + if (expectedPaddingLeft) { + var actualPaddingLeft = getComputedStyle(node).paddingLeft; + // Trim the unit "px" from the output. + actualPaddingLeft = actualPaddingLeft.slice(0, -2); + assert_equals(actualPaddingLeft, expectedPaddingLeft, prefix + "padding-left"); + } + + var expectedPaddingRight = checkAttribute(output, node, "data-expected-padding-right"); + if (expectedPaddingRight) { + var actualPaddingRight = getComputedStyle(node).paddingRight; + // Trim the unit "px" from the output. + actualPaddingRight = actualPaddingRight.slice(0, -2); + assert_equals(actualPaddingRight, expectedPaddingRight, prefix + "padding-right"); + } + + var expectedMarginTop = checkAttribute(output, node, "data-expected-margin-top"); + if (expectedMarginTop) { + var actualMarginTop = getComputedStyle(node).marginTop; + // Trim the unit "px" from the output. + actualMarginTop = actualMarginTop.slice(0, -2); + assert_equals(actualMarginTop, expectedMarginTop, prefix + "margin-top"); + } + + var expectedMarginBottom = checkAttribute(output, node, "data-expected-margin-bottom"); + if (expectedMarginBottom) { + var actualMarginBottom = getComputedStyle(node).marginBottom; + // Trim the unit "px" from the output. + actualMarginBottom = actualMarginBottom.slice(0, -2); + assert_equals(actualMarginBottom, expectedMarginBottom, prefix + "margin-bottom"); + } + + var expectedMarginLeft = checkAttribute(output, node, "data-expected-margin-left"); + if (expectedMarginLeft) { + var actualMarginLeft = getComputedStyle(node).marginLeft; + // Trim the unit "px" from the output. + actualMarginLeft = actualMarginLeft.slice(0, -2); + assert_equals(actualMarginLeft, expectedMarginLeft, prefix + "margin-left"); + } + + var expectedMarginRight = checkAttribute(output, node, "data-expected-margin-right"); + if (expectedMarginRight) { + var actualMarginRight = getComputedStyle(node).marginRight; + // Trim the unit "px" from the output. + actualMarginRight = actualMarginRight.slice(0, -2); + assert_equals(actualMarginRight, expectedMarginRight, prefix + "margin-right"); + } + + return output.checked; +} + +var testNumber = 0; +var highlightError = false; // displays outline around failed test element. +var printDomOnError = true; // prints dom when test fails. + +window.checkLayout = function(selectorList, callDone = true) +{ + if (!selectorList) { + console.error("You must provide a CSS selector of nodes to check."); + return; + } + var nodes = document.querySelectorAll(selectorList); + nodes = Array.prototype.slice.call(nodes); + var checkedLayout = false; + Array.prototype.forEach.call(nodes, function(node) { + const title = node.title == '' ? '' : `: ${node.title}`; + test(function(t) { + var container = node.parentNode.className == 'container' ? node.parentNode : node; + var prefix = + printDomOnError ? '\n' + container.outerHTML + '\n' : ''; + var passed = false; + try { + checkedLayout |= checkExpectedValues(t, node.parentNode, prefix); + checkedLayout |= checkSubtreeExpectedValues(t, node, prefix); + passed = true; + } finally { + if (!passed && highlightError) { + if (!document.getElementById('testharness_error_css')) { + var style = document.createElement('style'); + style.id = 'testharness_error_css'; + style.textContent = '.testharness_error { outline: red dotted 2px !important; }'; + document.body.appendChild(style); + } + if (node) + node.classList.add('testharness_error'); + } + checkedLayout |= !passed; + } + }, `${selectorList} ${++testNumber}${title}`); + }); + if (!checkedLayout) { + console.error("No valid data-* attributes found in selector list : " + selectorList); + } + if (callDone) + done(); +}; + +})(); diff --git a/testing/web-platform/tests/resources/check-layout.js b/testing/web-platform/tests/resources/check-layout.js new file mode 100644 index 0000000000..8634481497 --- /dev/null +++ b/testing/web-platform/tests/resources/check-layout.js @@ -0,0 +1,245 @@ +(function() { + +function insertAfter(nodeToAdd, referenceNode) +{ + if (referenceNode == document.body) { + document.body.appendChild(nodeToAdd); + return; + } + + if (referenceNode.nextSibling) + referenceNode.parentNode.insertBefore(nodeToAdd, referenceNode.nextSibling); + else + referenceNode.parentNode.appendChild(nodeToAdd); +} + +function positionedAncestor(node) +{ + var ancestor = node.parentNode; + while (getComputedStyle(ancestor).position == 'static') + ancestor = ancestor.parentNode; + return ancestor; +} + +function checkSubtreeExpectedValues(parent, failures) +{ + var checkedLayout = checkExpectedValues(parent, failures); + Array.prototype.forEach.call(parent.childNodes, function(node) { + checkedLayout |= checkSubtreeExpectedValues(node, failures); + }); + return checkedLayout; +} + +function checkAttribute(output, node, attribute) +{ + var result = node.getAttribute && node.getAttribute(attribute); + output.checked |= !!result; + return result; +} + +function checkExpectedValues(node, failures) +{ + var output = { checked: false }; + var expectedWidth = checkAttribute(output, node, "data-expected-width"); + if (expectedWidth) { + if (isNaN(expectedWidth) || Math.abs(node.offsetWidth - expectedWidth) >= 1) + failures.push("Expected " + expectedWidth + " for width, but got " + node.offsetWidth + ". "); + } + + var expectedHeight = checkAttribute(output, node, "data-expected-height"); + if (expectedHeight) { + if (isNaN(expectedHeight) || Math.abs(node.offsetHeight - expectedHeight) >= 1) + failures.push("Expected " + expectedHeight + " for height, but got " + node.offsetHeight + ". "); + } + + var expectedOffset = checkAttribute(output, node, "data-offset-x"); + if (expectedOffset) { + if (isNaN(expectedOffset) || Math.abs(node.offsetLeft - expectedOffset) >= 1) + failures.push("Expected " + expectedOffset + " for offsetLeft, but got " + node.offsetLeft + ". "); + } + + var expectedOffset = checkAttribute(output, node, "data-offset-y"); + if (expectedOffset) { + if (isNaN(expectedOffset) || Math.abs(node.offsetTop - expectedOffset) >= 1) + failures.push("Expected " + expectedOffset + " for offsetTop, but got " + node.offsetTop + ". "); + } + + var expectedOffset = checkAttribute(output, node, "data-positioned-offset-x"); + if (expectedOffset) { + var actualOffset = node.getBoundingClientRect().left - positionedAncestor(node).getBoundingClientRect().left; + if (isNaN(expectedOffset) || Math.abs(actualOffset - expectedOffset) >= 1) + failures.push("Expected " + expectedOffset + " for getBoundingClientRect().left offset, but got " + actualOffset + ". "); + } + + var expectedOffset = checkAttribute(output, node, "data-positioned-offset-y"); + if (expectedOffset) { + var actualOffset = node.getBoundingClientRect().top - positionedAncestor(node).getBoundingClientRect().top; + if (isNaN(expectedOffset) || Math.abs(actualOffset - expectedOffset) >= 1) + failures.push("Expected " + expectedOffset + " for getBoundingClientRect().top offset, but got " + actualOffset + ". "); + } + + var expectedWidth = checkAttribute(output, node, "data-expected-client-width"); + if (expectedWidth) { + if (isNaN(expectedWidth) || Math.abs(node.clientWidth - expectedWidth) >= 1) + failures.push("Expected " + expectedWidth + " for clientWidth, but got " + node.clientWidth + ". "); + } + + var expectedHeight = checkAttribute(output, node, "data-expected-client-height"); + if (expectedHeight) { + if (isNaN(expectedHeight) || Math.abs(node.clientHeight - expectedHeight) >= 1) + failures.push("Expected " + expectedHeight + " for clientHeight, but got " + node.clientHeight + ". "); + } + + var expectedWidth = checkAttribute(output, node, "data-expected-scroll-width"); + if (expectedWidth) { + if (isNaN(expectedWidth) || Math.abs(node.scrollWidth - expectedWidth) >= 1) + failures.push("Expected " + expectedWidth + " for scrollWidth, but got " + node.scrollWidth + ". "); + } + + var expectedHeight = checkAttribute(output, node, "data-expected-scroll-height"); + if (expectedHeight) { + if (isNaN(expectedHeight) || Math.abs(node.scrollHeight - expectedHeight) >= 1) + failures.push("Expected " + expectedHeight + " for scrollHeight, but got " + node.scrollHeight + ". "); + } + + var expectedOffset = checkAttribute(output, node, "data-total-x"); + if (expectedOffset) { + var totalLeft = node.clientLeft + node.offsetLeft; + if (isNaN(expectedOffset) || Math.abs(totalLeft - expectedOffset) >= 1) + failures.push("Expected " + expectedOffset + " for clientLeft+offsetLeft, but got " + totalLeft + ", clientLeft: " + node.clientLeft + ", offsetLeft: " + node.offsetLeft + ". "); + } + + var expectedOffset = checkAttribute(output, node, "data-total-y"); + if (expectedOffset) { + var totalTop = node.clientTop + node.offsetTop; + if (isNaN(expectedOffset) || Math.abs(totalTop - expectedOffset) >= 1) + failures.push("Expected " + expectedOffset + " for clientTop+offsetTop, but got " + totalTop + ", clientTop: " + node.clientTop + ", + offsetTop: " + node.offsetTop + ". "); + } + + var expectedDisplay = checkAttribute(output, node, "data-expected-display"); + if (expectedDisplay) { + var actualDisplay = getComputedStyle(node).display; + if (actualDisplay != expectedDisplay) + failures.push("Expected " + expectedDisplay + " for display, but got " + actualDisplay + ". "); + } + + var expectedPaddingTop = checkAttribute(output, node, "data-expected-padding-top"); + if (expectedPaddingTop) { + var actualPaddingTop = getComputedStyle(node).paddingTop; + // Trim the unit "px" from the output. + actualPaddingTop = actualPaddingTop.substring(0, actualPaddingTop.length - 2); + if (actualPaddingTop != expectedPaddingTop) + failures.push("Expected " + expectedPaddingTop + " for padding-top, but got " + actualPaddingTop + ". "); + } + + var expectedPaddingBottom = checkAttribute(output, node, "data-expected-padding-bottom"); + if (expectedPaddingBottom) { + var actualPaddingBottom = getComputedStyle(node).paddingBottom; + // Trim the unit "px" from the output. + actualPaddingBottom = actualPaddingBottom.substring(0, actualPaddingBottom.length - 2); + if (actualPaddingBottom != expectedPaddingBottom) + failures.push("Expected " + expectedPaddingBottom + " for padding-bottom, but got " + actualPaddingBottom + ". "); + } + + var expectedPaddingLeft = checkAttribute(output, node, "data-expected-padding-left"); + if (expectedPaddingLeft) { + var actualPaddingLeft = getComputedStyle(node).paddingLeft; + // Trim the unit "px" from the output. + actualPaddingLeft = actualPaddingLeft.substring(0, actualPaddingLeft.length - 2); + if (actualPaddingLeft != expectedPaddingLeft) + failures.push("Expected " + expectedPaddingLeft + " for padding-left, but got " + actualPaddingLeft + ". "); + } + + var expectedPaddingRight = checkAttribute(output, node, "data-expected-padding-right"); + if (expectedPaddingRight) { + var actualPaddingRight = getComputedStyle(node).paddingRight; + // Trim the unit "px" from the output. + actualPaddingRight = actualPaddingRight.substring(0, actualPaddingRight.length - 2); + if (actualPaddingRight != expectedPaddingRight) + failures.push("Expected " + expectedPaddingRight + " for padding-right, but got " + actualPaddingRight + ". "); + } + + var expectedMarginTop = checkAttribute(output, node, "data-expected-margin-top"); + if (expectedMarginTop) { + var actualMarginTop = getComputedStyle(node).marginTop; + // Trim the unit "px" from the output. + actualMarginTop = actualMarginTop.substring(0, actualMarginTop.length - 2); + if (actualMarginTop != expectedMarginTop) + failures.push("Expected " + expectedMarginTop + " for margin-top, but got " + actualMarginTop + ". "); + } + + var expectedMarginBottom = checkAttribute(output, node, "data-expected-margin-bottom"); + if (expectedMarginBottom) { + var actualMarginBottom = getComputedStyle(node).marginBottom; + // Trim the unit "px" from the output. + actualMarginBottom = actualMarginBottom.substring(0, actualMarginBottom.length - 2); + if (actualMarginBottom != expectedMarginBottom) + failures.push("Expected " + expectedMarginBottom + " for margin-bottom, but got " + actualMarginBottom + ". "); + } + + var expectedMarginLeft = checkAttribute(output, node, "data-expected-margin-left"); + if (expectedMarginLeft) { + var actualMarginLeft = getComputedStyle(node).marginLeft; + // Trim the unit "px" from the output. + actualMarginLeft = actualMarginLeft.substring(0, actualMarginLeft.length - 2); + if (actualMarginLeft != expectedMarginLeft) + failures.push("Expected " + expectedMarginLeft + " for margin-left, but got " + actualMarginLeft + ". "); + } + + var expectedMarginRight = checkAttribute(output, node, "data-expected-margin-right"); + if (expectedMarginRight) { + var actualMarginRight = getComputedStyle(node).marginRight; + // Trim the unit "px" from the output. + actualMarginRight = actualMarginRight.substring(0, actualMarginRight.length - 2); + if (actualMarginRight != expectedMarginRight) + failures.push("Expected " + expectedMarginRight + " for margin-right, but got " + actualMarginRight + ". "); + } + + return output.checked; +} + +window.checkLayout = function(selectorList, outputContainer) +{ + var result = true; + if (!selectorList) { + document.body.appendChild(document.createTextNode("You must provide a CSS selector of nodes to check.")); + return; + } + var nodes = document.querySelectorAll(selectorList); + nodes = Array.prototype.slice.call(nodes); + nodes.reverse(); + var checkedLayout = false; + Array.prototype.forEach.call(nodes, function(node) { + var failures = []; + checkedLayout |= checkExpectedValues(node.parentNode, failures); + checkedLayout |= checkSubtreeExpectedValues(node, failures); + + var container = node.parentNode.className == 'container' ? node.parentNode : node; + + var pre = document.createElement('pre'); + if (failures.length) { + pre.className = 'FAIL'; + result = false; + } + pre.appendChild(document.createTextNode(failures.length ? "FAIL:\n" + failures.join('\n') + '\n\n' + container.outerHTML : "PASS")); + + var referenceNode = container; + if (outputContainer) { + if (!outputContainer.lastChild) { + // Inserting a text node so we have something to insertAfter. + outputContainer.textContent = " "; + } + referenceNode = outputContainer.lastChild; + } + insertAfter(pre, referenceNode); + }); + + if (!checkedLayout) { + document.body.appendChild(document.createTextNode("FAIL: No valid data-* attributes found in selector list : " + selectorList)); + return false; + } + + return result; +} + +})(); diff --git a/testing/web-platform/tests/resources/chromium/README.md b/testing/web-platform/tests/resources/chromium/README.md new file mode 100644 index 0000000000..be090b332f --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/README.md @@ -0,0 +1,7 @@ +This directory contains Chromium-specific test resources, including mocks for +test-only APIs implemented with +[MojoJS](https://chromium.googlesource.com/chromium/src/+/main/mojo/public/js/README.md). + +Please do **not** copy `*.mojom.m.js` into this directory. Follow this doc if you +want to add new MojoJS-backed mocks: +https://chromium.googlesource.com/chromium/src/+/main/docs/testing/web_platform_tests.md#mojojs diff --git a/testing/web-platform/tests/resources/chromium/contacts_manager_mock.js b/testing/web-platform/tests/resources/chromium/contacts_manager_mock.js new file mode 100644 index 0000000000..049685242b --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/contacts_manager_mock.js @@ -0,0 +1,90 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import {ContactsManager, ContactsManagerReceiver} from '/gen/third_party/blink/public/mojom/contacts/contacts_manager.mojom.m.js'; + +self.WebContactsTest = (() => { + class MockContacts { + constructor() { + this.receiver_ = new ContactsManagerReceiver(this); + + this.interceptor_ = + new MojoInterfaceInterceptor(ContactsManager.$interfaceName); + this.interceptor_.oninterfacerequest = + e => this.receiver_.$.bindHandle(e.handle); + this.interceptor_.start(); + + this.selectedContacts_ = []; + } + + formatAddress_(address) { + // These are all required fields in the mojo definition. + return { + country: address.country || '', + addressLine: address.addressLine || [], + region: address.region || '', + city: address.city || '', + dependentLocality: address.dependentLocality || '', + postalCode: address.postCode || '', + sortingCode: address.sortingCode || '', + organization: address.organization || '', + recipient: address.recipient || '', + phone: address.phone || '', + }; + } + + async select(multiple, includeNames, includeEmails, includeTel, includeAddresses, includeIcons) { + if (this.selectedContacts_ === null) + return {contacts: null}; + + const contactInfos = await Promise.all(this.selectedContacts_.map(async contact => { + const contactInfo = {}; + if (includeNames) + contactInfo.name = contact.name || []; + if (includeEmails) + contactInfo.email = contact.email || []; + if (includeTel) + contactInfo.tel = contact.tel || []; + if (includeAddresses) { + contactInfo.address = (contact.address || []).map(address => this.formatAddress_(address)); + } + if (includeIcons) { + contactInfo.icon = await Promise.all( + (contact.icon || []).map(async blob => ({ + mimeType: blob.type, + data: (await blob.text()).split('').map(s => s.charCodeAt(0)), + }))); + } + return contactInfo; + })); + + if (!contactInfos.length) return {contacts: []}; + if (!multiple) return {contacts: [contactInfos[0]]}; + return {contacts: contactInfos}; + } + + setSelectedContacts(contacts) { + this.selectedContacts_ = contacts; + } + + reset() { + this.receiver_.$.close(); + this.interceptor_.stop(); + } + } + + const mockContacts = new MockContacts(); + + class ContactsTestChromium { + constructor() { + Object.freeze(this); // Make it immutable. + } + + setSelectedContacts(contacts) { + mockContacts.setSelectedContacts(contacts); + } + } + + return ContactsTestChromium; +})(); diff --git a/testing/web-platform/tests/resources/chromium/content-index-helpers.js b/testing/web-platform/tests/resources/chromium/content-index-helpers.js new file mode 100644 index 0000000000..936fe84c9b --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/content-index-helpers.js @@ -0,0 +1,9 @@ +import {ContentIndexService} from '/gen/third_party/blink/public/mojom/content_index/content_index.mojom.m.js'; + +// Returns a promise if the chromium based browser fetches icons for +// content-index. +export async function fetchesIcons() { + const remote = ContentIndexService.getRemote(); + const {iconSizes} = await remote.getIconSizes(); + return iconSizes.length > 0; +}; diff --git a/testing/web-platform/tests/resources/chromium/enable-hyperlink-auditing.js b/testing/web-platform/tests/resources/chromium/enable-hyperlink-auditing.js new file mode 100644 index 0000000000..263f6512f0 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/enable-hyperlink-auditing.js @@ -0,0 +1,2 @@ +if (window.testRunner) + testRunner.overridePreference("WebKitHyperlinkAuditingEnabled", 1); diff --git a/testing/web-platform/tests/resources/chromium/fake-hid.js b/testing/web-platform/tests/resources/chromium/fake-hid.js new file mode 100644 index 0000000000..70a01490d8 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/fake-hid.js @@ -0,0 +1,297 @@ +import {HidConnectionReceiver, HidDeviceInfo} from '/gen/services/device/public/mojom/hid.mojom.m.js'; +import {HidService, HidServiceReceiver} from '/gen/third_party/blink/public/mojom/hid/hid.mojom.m.js'; + +// Fake implementation of device.mojom.HidConnection. HidConnection represents +// an open connection to a HID device and can be used to send and receive +// reports. +class FakeHidConnection { + constructor(client) { + this.client_ = client; + this.receiver_ = new HidConnectionReceiver(this); + this.expectedWrites_ = []; + this.expectedGetFeatureReports_ = []; + this.expectedSendFeatureReports_ = []; + } + + bindNewPipeAndPassRemote() { + return this.receiver_.$.bindNewPipeAndPassRemote(); + } + + // Simulate an input report sent from the device to the host. The connection + // client's onInputReport method will be called with the provided |reportId| + // and |buffer|. + simulateInputReport(reportId, reportData) { + if (this.client_) { + this.client_.onInputReport(reportId, reportData); + } + } + + // Specify the result for an expected call to write. If |success| is true the + // write will be successful, otherwise it will simulate a failure. The + // parameters of the next write call must match |reportId| and |buffer|. + queueExpectedWrite(success, reportId, reportData) { + this.expectedWrites_.push({ + params: {reportId, data: reportData}, + result: {success}, + }); + } + + // Specify the result for an expected call to getFeatureReport. If |success| + // is true the operation is successful, otherwise it will simulate a failure. + // The parameter of the next getFeatureReport call must match |reportId|. + queueExpectedGetFeatureReport(success, reportId, reportData) { + this.expectedGetFeatureReports_.push({ + params: {reportId}, + result: {success, buffer: reportData}, + }); + } + + // Specify the result for an expected call to sendFeatureReport. If |success| + // is true the operation is successful, otherwise it will simulate a failure. + // The parameters of the next sendFeatureReport call must match |reportId| and + // |buffer|. + queueExpectedSendFeatureReport(success, reportId, reportData) { + this.expectedSendFeatureReports_.push({ + params: {reportId, data: reportData}, + result: {success}, + }); + } + + // Asserts that there are no more expected operations. + assertExpectationsMet() { + assert_equals(this.expectedWrites_.length, 0); + assert_equals(this.expectedGetFeatureReports_.length, 0); + assert_equals(this.expectedSendFeatureReports_.length, 0); + } + + read() {} + + // Implementation of HidConnection::Write. Causes an assertion failure if + // there are no expected write operations, or if the parameters do not match + // the expected call. + async write(reportId, buffer) { + let expectedWrite = this.expectedWrites_.shift(); + assert_not_equals(expectedWrite, undefined); + assert_equals(reportId, expectedWrite.params.reportId); + let actual = new Uint8Array(buffer); + compareDataViews( + new DataView(actual.buffer, actual.byteOffset), + new DataView( + expectedWrite.params.data.buffer, + expectedWrite.params.data.byteOffset)); + return expectedWrite.result; + } + + // Implementation of HidConnection::GetFeatureReport. Causes an assertion + // failure if there are no expected write operations, or if the parameters do + // not match the expected call. + async getFeatureReport(reportId) { + let expectedGetFeatureReport = this.expectedGetFeatureReports_.shift(); + assert_not_equals(expectedGetFeatureReport, undefined); + assert_equals(reportId, expectedGetFeatureReport.params.reportId); + return expectedGetFeatureReport.result; + } + + // Implementation of HidConnection::SendFeatureReport. Causes an assertion + // failure if there are no expected write operations, or if the parameters do + // not match the expected call. + async sendFeatureReport(reportId, buffer) { + let expectedSendFeatureReport = this.expectedSendFeatureReports_.shift(); + assert_not_equals(expectedSendFeatureReport, undefined); + assert_equals(reportId, expectedSendFeatureReport.params.reportId); + let actual = new Uint8Array(buffer); + compareDataViews( + new DataView(actual.buffer, actual.byteOffset), + new DataView( + expectedSendFeatureReport.params.data.buffer, + expectedSendFeatureReport.params.data.byteOffset)); + return expectedSendFeatureReport.result; + } +} + + +// A fake implementation of the HidService mojo interface. HidService manages +// HID device access for clients in the render process. Typically, when a client +// requests access to a HID device a chooser dialog is shown with a list of +// available HID devices. Selecting a device from the chooser also grants +// permission for the client to access that device. +// +// The fake implementation allows tests to simulate connected devices. It also +// skips the chooser dialog and instead allows tests to specify which device +// should be selected. All devices are treated as if the user had already +// granted permission. It is possible to revoke permission with forget() later. +class FakeHidService { + constructor() { + this.interceptor_ = new MojoInterfaceInterceptor(HidService.$interfaceName); + this.interceptor_.oninterfacerequest = e => this.bind(e.handle); + this.receiver_ = new HidServiceReceiver(this); + this.nextGuidValue_ = 0; + this.simulateConnectFailure_ = false; + this.reset(); + } + + start() { + this.interceptor_.start(); + } + + stop() { + this.interceptor_.stop(); + } + + reset() { + this.devices_ = new Map(); + this.allowedDevices_ = new Map(); + this.fakeConnections_ = new Map(); + this.selectedDevices_ = []; + } + + // Creates and returns a HidDeviceInfo with the specified device IDs. + makeDevice(vendorId, productId) { + let guidValue = ++this.nextGuidValue_; + let info = new HidDeviceInfo(); + info.guid = 'guid-' + guidValue.toString(); + info.physicalDeviceId = 'physical-device-id-' + guidValue.toString(); + info.vendorId = vendorId; + info.productId = productId; + info.productName = 'product name'; + info.serialNumber = '0'; + info.reportDescriptor = new Uint8Array(); + info.collections = []; + info.deviceNode = 'device node'; + return info; + } + + // Simulates a connected device the client has already been granted permission + // to. Returns the key used to store the device in the map. The key is either + // the physical device ID, or the device GUID if it has no physical device ID. + addDevice(deviceInfo, grantPermission = true) { + let key = deviceInfo.physicalDeviceId; + if (key.length === 0) + key = deviceInfo.guid; + + let devices = this.devices_.get(key) || []; + devices.push(deviceInfo); + this.devices_.set(key, devices); + + if (grantPermission) { + let allowedDevices = this.allowedDevices_.get(key) || []; + allowedDevices.push(deviceInfo); + this.allowedDevices_.set(key, allowedDevices); + } + + if (this.client_) + this.client_.deviceAdded(deviceInfo); + return key; + } + + // Simulates disconnecting a connected device. + removeDevice(key) { + let devices = this.devices_.get(key); + this.devices_.delete(key); + if (this.client_ && devices) { + devices.forEach(deviceInfo => { + this.client_.deviceRemoved(deviceInfo); + }); + } + } + + // Simulates updating the device information for a connected device. + changeDevice(deviceInfo) { + let key = deviceInfo.physicalDeviceId; + if (key.length === 0) + key = deviceInfo.guid; + + let devices = this.devices_.get(key) || []; + let i = devices.length; + while (i--) { + if (devices[i].guid == deviceInfo.guid) + devices.splice(i, 1); + } + devices.push(deviceInfo); + this.devices_.set(key, devices); + + let allowedDevices = this.allowedDevices_.get(key) || []; + let j = allowedDevices.length; + while (j--) { + if (allowedDevices[j].guid == deviceInfo.guid) + allowedDevices.splice(j, 1); + } + allowedDevices.push(deviceInfo); + this.allowedDevices_.set(key, allowedDevices); + + if (this.client_) + this.client_.deviceChanged(deviceInfo); + return key; + } + + // Sets a flag that causes the next call to connect() to fail. + simulateConnectFailure() { + this.simulateConnectFailure_ = true; + } + + // Sets the key of the device that will be returned as the selected item the + // next time requestDevice is called. The device with this key must have been + // previously added with addDevice. + setSelectedDevice(key) { + this.selectedDevices_ = this.devices_.get(key); + } + + // Returns the fake HidConnection object for this device, if there is one. A + // connection is created once the device is opened. + getFakeConnection(guid) { + return this.fakeConnections_.get(guid); + } + + bind(handle) { + this.receiver_.$.bindHandle(handle); + } + + registerClient(client) { + this.client_ = client; + } + + // Returns an array of connected devices the client has already been granted + // permission to access. + async getDevices() { + let devices = []; + this.allowedDevices_.forEach((value) => { + devices = devices.concat(value); + }); + return {devices}; + } + + // Simulates a device chooser prompt, returning |selectedDevices_| as the + // simulated selection. |options| is ignored. + async requestDevice(options) { + return {devices: this.selectedDevices_}; + } + + // Returns a fake connection to the device with the specified GUID. If + // |connectionClient| is not null, its onInputReport method will be called + // when input reports are received. If simulateConnectFailure() was called + // then a null connection is returned instead, indicating failure. + async connect(guid, connectionClient) { + if (this.simulateConnectFailure_) { + this.simulateConnectFailure_ = false; + return {connection: null}; + } + const fakeConnection = new FakeHidConnection(connectionClient); + this.fakeConnections_.set(guid, fakeConnection); + return {connection: fakeConnection.bindNewPipeAndPassRemote()}; + } + + // Removes the allowed device. + async forget(deviceInfo) { + for (const [key, value] of this.allowedDevices_) { + for (const device of value) { + if (device.guid == deviceInfo.guid) { + this.allowedDevices_.delete(key); + break; + } + } + } + return {success: true}; + } +} + +export const fakeHidService = new FakeHidService(); diff --git a/testing/web-platform/tests/resources/chromium/fake-serial.js b/testing/web-platform/tests/resources/chromium/fake-serial.js new file mode 100644 index 0000000000..29c8168fb5 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/fake-serial.js @@ -0,0 +1,445 @@ +import {SerialPortFlushMode, SerialPortRemote, SerialReceiveError, SerialPortReceiver, SerialSendError} from '/gen/services/device/public/mojom/serial.mojom.m.js'; +import {SerialService, SerialServiceReceiver} from '/gen/third_party/blink/public/mojom/serial/serial.mojom.m.js'; + +// Implementation of an UnderlyingSource to create a ReadableStream from a Mojo +// data pipe consumer handle. +class DataPipeSource { + constructor(consumer) { + this.consumer_ = consumer; + } + + async pull(controller) { + let chunk = new ArrayBuffer(64); + let {result, numBytes} = this.consumer_.readData(chunk); + if (result == Mojo.RESULT_OK) { + controller.enqueue(new Uint8Array(chunk, 0, numBytes)); + return; + } else if (result == Mojo.RESULT_FAILED_PRECONDITION) { + controller.close(); + return; + } else if (result == Mojo.RESULT_SHOULD_WAIT) { + await this.readable(); + return this.pull(controller); + } + } + + cancel() { + if (this.watcher_) + this.watcher_.cancel(); + this.consumer_.close(); + } + + readable() { + return new Promise((resolve) => { + this.watcher_ = + this.consumer_.watch({ readable: true, peerClosed: true }, () => { + this.watcher_.cancel(); + this.watcher_ = undefined; + resolve(); + }); + }); + } +} + +// Implementation of an UnderlyingSink to create a WritableStream from a Mojo +// data pipe producer handle. +class DataPipeSink { + constructor(producer) { + this._producer = producer; + } + + async write(chunk, controller) { + while (true) { + let {result, numBytes} = this._producer.writeData(chunk); + if (result == Mojo.RESULT_OK) { + if (numBytes == chunk.byteLength) { + return; + } + chunk = chunk.slice(numBytes); + } else if (result == Mojo.RESULT_FAILED_PRECONDITION) { + throw new DOMException('The pipe is closed.', 'InvalidStateError'); + } else if (result == Mojo.RESULT_SHOULD_WAIT) { + await this.writable(); + } + } + } + + close() { + assert_equals(undefined, this._watcher); + this._producer.close(); + } + + abort(reason) { + if (this._watcher) + this._watcher.cancel(); + this._producer.close(); + } + + writable() { + return new Promise((resolve) => { + this._watcher = + this._producer.watch({ writable: true, peerClosed: true }, () => { + this._watcher.cancel(); + this._watcher = undefined; + resolve(); + }); + }); + } +} + +// Implementation of device.mojom.SerialPort. +class FakeSerialPort { + constructor() { + this.inputSignals_ = { + dataCarrierDetect: false, + clearToSend: false, + ringIndicator: false, + dataSetReady: false + }; + this.inputSignalFailure_ = false; + this.outputSignals_ = { + dataTerminalReady: false, + requestToSend: false, + break: false + }; + this.outputSignalFailure_ = false; + } + + open(options, client) { + if (this.receiver_ !== undefined) { + // Port already open. + return null; + } + + let port = new SerialPortRemote(); + this.receiver_ = new SerialPortReceiver(this); + this.receiver_.$.bindHandle(port.$.bindNewPipeAndPassReceiver().handle); + + this.options_ = options; + this.client_ = client; + // OS typically sets DTR on open. + this.outputSignals_.dataTerminalReady = true; + + return port; + } + + write(data) { + return this.writer_.write(data); + } + + read() { + return this.reader_.read(); + } + + // Reads from the port until at least |targetLength| is read or the stream is + // closed. The data is returned as a combined Uint8Array. + readWithLength(targetLength) { + return readWithLength(this.reader_, targetLength); + } + + simulateReadError(error) { + this.writer_.close(); + this.writer_.releaseLock(); + this.writer_ = undefined; + this.writable_ = undefined; + this.client_.onReadError(error); + } + + simulateParityError() { + this.simulateReadError(SerialReceiveError.PARITY_ERROR); + } + + simulateDisconnectOnRead() { + this.simulateReadError(SerialReceiveError.DISCONNECTED); + } + + simulateWriteError(error) { + this.reader_.cancel(); + this.reader_ = undefined; + this.readable_ = undefined; + this.client_.onSendError(error); + } + + simulateSystemErrorOnWrite() { + this.simulateWriteError(SerialSendError.SYSTEM_ERROR); + } + + simulateDisconnectOnWrite() { + this.simulateWriteError(SerialSendError.DISCONNECTED); + } + + simulateInputSignals(signals) { + this.inputSignals_ = signals; + } + + simulateInputSignalFailure(fail) { + this.inputSignalFailure_ = fail; + } + + get outputSignals() { + return this.outputSignals_; + } + + simulateOutputSignalFailure(fail) { + this.outputSignalFailure_ = fail; + } + + writable() { + if (this.writable_) + return Promise.resolve(); + + if (!this.writablePromise_) { + this.writablePromise_ = new Promise((resolve) => { + this.writableResolver_ = resolve; + }); + } + + return this.writablePromise_; + } + + readable() { + if (this.readable_) + return Promise.resolve(); + + if (!this.readablePromise_) { + this.readablePromise_ = new Promise((resolve) => { + this.readableResolver_ = resolve; + }); + } + + return this.readablePromise_; + } + + async startWriting(in_stream) { + this.readable_ = new ReadableStream(new DataPipeSource(in_stream)); + this.reader_ = this.readable_.getReader(); + if (this.readableResolver_) { + this.readableResolver_(); + this.readableResolver_ = undefined; + this.readablePromise_ = undefined; + } + } + + async startReading(out_stream) { + this.writable_ = new WritableStream(new DataPipeSink(out_stream)); + this.writer_ = this.writable_.getWriter(); + if (this.writableResolver_) { + this.writableResolver_(); + this.writableResolver_ = undefined; + this.writablePromise_ = undefined; + } + } + + async flush(mode) { + switch (mode) { + case SerialPortFlushMode.kReceive: + this.writer_.abort(); + this.writer_.releaseLock(); + this.writer_ = undefined; + this.writable_ = undefined; + break; + case SerialPortFlushMode.kTransmit: + if (this.reader_) { + this.reader_.cancel(); + this.reader_ = undefined; + } + this.readable_ = undefined; + break; + } + } + + async drain() { + await this.reader_.closed; + } + + async getControlSignals() { + if (this.inputSignalFailure_) { + return {signals: null}; + } + + const signals = { + dcd: this.inputSignals_.dataCarrierDetect, + cts: this.inputSignals_.clearToSend, + ri: this.inputSignals_.ringIndicator, + dsr: this.inputSignals_.dataSetReady + }; + return {signals}; + } + + async setControlSignals(signals) { + if (this.outputSignalFailure_) { + return {success: false}; + } + + if (signals.hasDtr) { + this.outputSignals_.dataTerminalReady = signals.dtr; + } + if (signals.hasRts) { + this.outputSignals_.requestToSend = signals.rts; + } + if (signals.hasBrk) { + this.outputSignals_.break = signals.brk; + } + return { success: true }; + } + + async configurePort(options) { + this.options_ = options; + return { success: true }; + } + + async getPortInfo() { + return { + bitrate: this.options_.bitrate, + dataBits: this.options_.datBits, + parityBit: this.options_.parityBit, + stopBits: this.options_.stopBits, + ctsFlowControl: + this.options_.hasCtsFlowControl && this.options_.ctsFlowControl, + }; + } + + async close() { + // OS typically clears DTR on close. + this.outputSignals_.dataTerminalReady = false; + if (this.writer_) { + this.writer_.close(); + this.writer_.releaseLock(); + this.writer_ = undefined; + } + this.writable_ = undefined; + + // Close the receiver asynchronously so the reply to this message can be + // sent first. + const receiver = this.receiver_; + this.receiver_ = undefined; + setTimeout(() => { + receiver.$.close(); + }, 0); + + return {}; + } +} + +// Implementation of blink.mojom.SerialService. +class FakeSerialService { + constructor() { + this.interceptor_ = + new MojoInterfaceInterceptor(SerialService.$interfaceName); + this.interceptor_.oninterfacerequest = e => this.bind(e.handle); + this.receiver_ = new SerialServiceReceiver(this); + this.clients_ = []; + this.nextToken_ = 0; + this.reset(); + } + + start() { + this.interceptor_.start(); + } + + stop() { + this.interceptor_.stop(); + } + + reset() { + this.ports_ = new Map(); + this.selectedPort_ = null; + } + + addPort(info) { + let portInfo = {}; + if (info?.usbVendorId !== undefined) { + portInfo.hasUsbVendorId = true; + portInfo.usbVendorId = info.usbVendorId; + } + if (info?.usbProductId !== undefined) { + portInfo.hasUsbProductId = true; + portInfo.usbProductId = info.usbProductId; + } + + let token = ++this.nextToken_; + portInfo.token = {high: 0n, low: BigInt(token)}; + + let record = { + portInfo: portInfo, + fakePort: new FakeSerialPort(), + }; + this.ports_.set(token, record); + + for (let client of this.clients_) { + client.onPortAdded(portInfo); + } + + return token; + } + + removePort(token) { + let record = this.ports_.get(token); + if (record === undefined) { + return; + } + + this.ports_.delete(token); + + for (let client of this.clients_) { + client.onPortRemoved(record.portInfo); + } + } + + setSelectedPort(token) { + this.selectedPort_ = this.ports_.get(token); + } + + getFakePort(token) { + let record = this.ports_.get(token); + if (record === undefined) + return undefined; + return record.fakePort; + } + + bind(handle) { + this.receiver_.$.bindHandle(handle); + } + + async setClient(client_remote) { + this.clients_.push(client_remote); + } + + async getPorts() { + return { + ports: Array.from(this.ports_, ([token, record]) => record.portInfo) + }; + } + + async requestPort(filters) { + if (this.selectedPort_) + return { port: this.selectedPort_.portInfo }; + else + return { port: null }; + } + + async openPort(token, options, client) { + let record = this.ports_.get(Number(token.low)); + if (record !== undefined) { + return {port: record.fakePort.open(options, client)}; + } else { + return {port: null}; + } + } + + async forgetPort(token) { + let record = this.ports_.get(Number(token.low)); + if (record === undefined) { + return {success: false}; + } + + this.ports_.delete(Number(token.low)); + if (record.fakePort.receiver_) { + record.fakePort.receiver_.$.close(); + record.fakePort.receiver_ = undefined; + } + return {success: true}; + } +} + +export const fakeSerialService = new FakeSerialService(); diff --git a/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js b/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js new file mode 100644 index 0000000000..baf57c7c85 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js @@ -0,0 +1,523 @@ +import {ReportingMode, Sensor, SensorClientRemote, SensorReceiver, SensorRemote, SensorType} from '/gen/services/device/public/mojom/sensor.mojom.m.js'; +import {SensorCreationResult, SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS} from '/gen/services/device/public/mojom/sensor_provider.mojom.m.js'; +import {WebSensorProvider, WebSensorProviderReceiver} from '/gen/third_party/blink/public/mojom/sensor/web_sensor_provider.mojom.m.js'; + +// A "sliding window" that iterates over |data| and returns one item at a +// time, advancing and wrapping around as needed. |data| must be an array of +// arrays. +self.RingBuffer = class { + constructor(data) { + this.bufferPosition_ = 0; + // Validate |data|'s format and deep-copy every element. + this.data_ = Array.from(data, element => { + if (!Array.isArray(element)) { + throw new TypeError('Every |data| element must be an array.'); + } + return Array.from(element); + }) + } + + next() { + const value = this.data_[this.bufferPosition_]; + this.bufferPosition_ = (this.bufferPosition_ + 1) % this.data_.length; + return { done: false, value: value }; + } + + value() { + return this.data_[this.bufferPosition_]; + } + + [Symbol.iterator]() { + return this; + } +}; + +class DefaultSensorTraits { + // https://w3c.github.io/sensors/#threshold-check-algorithm + static isSignificantlyDifferent(reading1, reading2) { + return true; + } + + // https://w3c.github.io/sensors/#reading-quantization-algorithm + static roundToMultiple(reading) { + return reading; + } + + // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm + static areReadingsEqual(reading1, reading2) { + return false; + } +} + +class AmbientLightSensorTraits extends DefaultSensorTraits { + // https://w3c.github.io/ambient-light/#reduce-sensor-accuracy + static #ROUNDING_MULTIPLE = 50; + static #SIGNIFICANCE_THRESHOLD = 25; + + // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm + static isSignificantlyDifferent([illuminance1], [illuminance2]) { + return Math.abs(illuminance1 - illuminance2) >= + this.#SIGNIFICANCE_THRESHOLD; + } + + // https://w3c.github.io/ambient-light/#ambient-light-reading-quantization-algorithm + static roundToMultiple(reading) { + const illuminance = reading[0]; + const scaledValue = + illuminance / AmbientLightSensorTraits.#ROUNDING_MULTIPLE; + let roundedReading = reading.splice(); + + if (illuminance < 0.0) { + roundedReading[0] = -AmbientLightSensorTraits.#ROUNDING_MULTIPLE * + Math.floor(-scaledValue + 0.5); + } else { + roundedReading[0] = AmbientLightSensorTraits.#ROUNDING_MULTIPLE * + Math.floor(scaledValue + 0.5); + } + + return roundedReading; + } + + // https://w3c.github.io/ambient-light/#ambient-light-threshold-check-algorithm + static areReadingsEqual([illuminance1], [illuminance2]) { + return illuminance1 === illuminance2; + } +} + +self.GenericSensorTest = (() => { + // Default sensor frequency in default configurations. + const DEFAULT_FREQUENCY = 5; + + // Class that mocks Sensor interface defined in + // https://cs.chromium.org/chromium/src/services/device/public/mojom/sensor.mojom + class MockSensor { + static #BUFFER_OFFSET_TIMESTAMP = 1; + static #BUFFER_OFFSET_READINGS = 2; + + constructor(sensorRequest, buffer, reportingMode, sensorType) { + this.client_ = null; + this.startShouldFail_ = false; + this.notifyOnReadingChange_ = true; + this.reportingMode_ = reportingMode; + this.sensorType_ = sensorType; + this.sensorReadingTimerId_ = null; + this.readingData_ = null; + this.requestedFrequencies_ = []; + // The Blink implementation (third_party/blink/renderer/modules/sensor/sensor.cc) + // sets a timestamp by creating a DOMHighResTimeStamp from a given platform timestamp. + // In this mock implementation we use a starting value + // and an increment step value that resemble a platform timestamp reasonably enough. + this.timestamp_ = window.performance.timeOrigin; + // |buffer| represents a SensorReadingSharedBuffer on the C++ side in + // Chromium. It consists, in this order, of a + // SensorReadingField<OneWriterSeqLock> (an 8-byte union that includes + // 32-bit integer used by the lock class), and a SensorReading consisting + // of an 8-byte timestamp and 4 8-byte reading fields. + // + // |this.buffer_[0]| is zeroed by default, which allows OneWriterSeqLock + // to work with our custom memory buffer that did not actually create a + // OneWriterSeqLock instance. It is never changed manually here. + // + // Use MockSensor.#BUFFER_OFFSET_TIMESTAMP and + // MockSensor.#BUFFER_OFFSET_READINGS to access the other positions in + // |this.buffer_| without having to hardcode magic numbers in the code. + this.buffer_ = buffer; + this.buffer_.fill(0); + this.receiver_ = new SensorReceiver(this); + this.receiver_.$.bindHandle(sensorRequest.handle); + this.lastRawReading_ = null; + this.lastRoundedReading_ = null; + + if (sensorType == SensorType.AMBIENT_LIGHT) { + this.sensorTraits = AmbientLightSensorTraits; + } else { + this.sensorTraits = DefaultSensorTraits; + } + } + + // Returns default configuration. + async getDefaultConfiguration() { + return { frequency: DEFAULT_FREQUENCY }; + } + + // Adds configuration for the sensor and starts reporting fake data + // through setSensorReading function. + async addConfiguration(configuration) { + this.requestedFrequencies_.push(configuration.frequency); + // Sort using descending order. + this.requestedFrequencies_.sort( + (first, second) => { return second - first }); + + if (!this.startShouldFail_ ) + this.startReading(); + + return { success: !this.startShouldFail_ }; + } + + // Removes sensor configuration from the list of active configurations and + // stops notification about sensor reading changes if + // requestedFrequencies_ is empty. + removeConfiguration(configuration) { + const index = this.requestedFrequencies_.indexOf(configuration.frequency); + if (index == -1) + return; + + this.requestedFrequencies_.splice(index, 1); + if (this.requestedFrequencies_.length === 0) + this.stopReading(); + } + + // ConfigureReadingChangeNotifications(bool enabled) + // Configures whether to report a reading change when in ON_CHANGE + // reporting mode. + configureReadingChangeNotifications(notifyOnReadingChange) { + this.notifyOnReadingChange_ = notifyOnReadingChange; + } + + resume() { + this.startReading(); + } + + suspend() { + this.stopReading(); + } + + // Mock functions + + // Resets mock Sensor state. + reset() { + this.stopReading(); + this.startShouldFail_ = false; + this.requestedFrequencies_ = []; + this.notifyOnReadingChange_ = true; + this.readingData_ = null; + this.buffer_.fill(0); + this.receiver_.$.close(); + this.lastRawReading_ = null; + this.lastRoundedReading_ = null; + } + + // Sets fake data that is used to deliver sensor reading updates. + setSensorReading(readingData) { + this.readingData_ = new RingBuffer(readingData); + } + + // This is a workaround to accommodate Blink's Device Orientation + // implementation. In general, all tests should use setSensorReading() + // instead. + setSensorReadingImmediately(readingData) { + this.setSensorReading(readingData); + + const reading = this.readingData_.value(); + this.buffer_.set(reading, MockSensor.#BUFFER_OFFSET_READINGS); + this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++; + } + + // Sets flag that forces sensor to fail when addConfiguration is invoked. + setStartShouldFail(shouldFail) { + this.startShouldFail_ = shouldFail; + } + + startReading() { + if (this.readingData_ != null) { + this.stopReading(); + } + let maxFrequencyUsed = this.requestedFrequencies_[0]; + let timeout = (1 / maxFrequencyUsed) * 1000; + this.sensorReadingTimerId_ = window.setInterval(() => { + if (this.readingData_) { + // |buffer_| is a TypedArray, so we need to make sure pass an + // array to set(). + const reading = this.readingData_.next().value; + if (!Array.isArray(reading)) { + throw new TypeError("startReading(): The readings passed to " + + "setSensorReading() must be arrays"); + } + + if (this.reportingMode_ == ReportingMode.ON_CHANGE && + this.lastRawReading_ !== null && + !this.sensorTraits.isSignificantlyDifferent( + this.lastRawReading_, reading)) { + // In case new value is not significantly different compared to + // old value, new value is not sent. + return; + } + + this.lastRawReading_ = reading.slice(); + const roundedReading = this.sensorTraits.roundToMultiple(reading); + + if (this.reportingMode_ == ReportingMode.ON_CHANGE && + this.lastRoundedReading_ !== null && + this.sensorTraits.areReadingsEqual( + roundedReading, this.lastRoundedReading_)) { + // In case new rounded value is not different compared to old + // value, new value is not sent. + return; + } + this.buffer_.set(roundedReading, MockSensor.#BUFFER_OFFSET_READINGS); + this.lastRoundedReading_ = roundedReading; + } + + // For all tests sensor reading should have monotonically + // increasing timestamp. + this.buffer_[MockSensor.#BUFFER_OFFSET_TIMESTAMP] = this.timestamp_++; + + if (this.reportingMode_ === ReportingMode.ON_CHANGE && + this.notifyOnReadingChange_) { + this.client_.sensorReadingChanged(); + } + }, timeout); + } + + stopReading() { + if (this.sensorReadingTimerId_ != null) { + window.clearInterval(this.sensorReadingTimerId_); + this.sensorReadingTimerId_ = null; + } + this.buffer_.fill(0); + this.lastRawReading_ = null; + this.lastRoundedReading_ = null; + } + + getSamplingFrequency() { + if (this.requestedFrequencies_.length == 0) { + throw new Error("getSamplingFrequency(): No configured frequency"); + } + return this.requestedFrequencies_[0]; + } + + isReadingData() { + return this.sensorReadingTimerId_ != null; + } + } + + // Class that mocks the WebSensorProvider interface defined in + // https://cs.chromium.org/chromium/src/third_party/blink/public/mojom/sensor/web_sensor_provider.mojom + class MockSensorProvider { + constructor() { + this.readingSizeInBytes_ = + Number(SensorInitParams_READ_BUFFER_SIZE_FOR_TESTS); + this.sharedBufferSizeInBytes_ = + this.readingSizeInBytes_ * (SensorType.MAX_VALUE + 1); + let rv = Mojo.createSharedBuffer(this.sharedBufferSizeInBytes_); + if (rv.result != Mojo.RESULT_OK) { + throw new Error('MockSensorProvider: Failed to create shared buffer'); + } + const handle = rv.handle; + rv = handle.mapBuffer(0, this.sharedBufferSizeInBytes_); + if (rv.result != Mojo.RESULT_OK) { + throw new Error("MockSensorProvider: Failed to map shared buffer"); + } + this.shmemArrayBuffer_ = rv.buffer; + rv = handle.duplicateBufferHandle({readOnly: true}); + if (rv.result != Mojo.RESULT_OK) { + throw new Error( + 'MockSensorProvider: failed to duplicate shared buffer'); + } + this.readOnlySharedBufferHandle_ = rv.handle; + this.activeSensors_ = new Map(); + this.resolveFuncs_ = new Map(); + this.getSensorShouldFail_ = new Map(); + this.permissionsDenied_ = new Map(); + this.maxFrequency_ = 60; + this.minFrequency_ = 1; + this.mojomSensorType_ = new Map([ + ['Accelerometer', SensorType.ACCELEROMETER], + ['LinearAccelerationSensor', SensorType.LINEAR_ACCELERATION], + ['GravitySensor', SensorType.GRAVITY], + ['AmbientLightSensor', SensorType.AMBIENT_LIGHT], + ['Gyroscope', SensorType.GYROSCOPE], + ['Magnetometer', SensorType.MAGNETOMETER], + ['AbsoluteOrientationSensor', + SensorType.ABSOLUTE_ORIENTATION_QUATERNION], + ['AbsoluteOrientationEulerAngles', + SensorType.ABSOLUTE_ORIENTATION_EULER_ANGLES], + ['RelativeOrientationSensor', + SensorType.RELATIVE_ORIENTATION_QUATERNION], + ['RelativeOrientationEulerAngles', + SensorType.RELATIVE_ORIENTATION_EULER_ANGLES], + ['ProximitySensor', SensorType.PROXIMITY] + ]); + this.receiver_ = new WebSensorProviderReceiver(this); + + this.interceptor_ = + new MojoInterfaceInterceptor(WebSensorProvider.$interfaceName); + this.interceptor_.oninterfacerequest = e => { + this.bindToPipe(e.handle); + }; + this.interceptor_.start(); + } + + // Returns initialized Sensor proxy to the client. + async getSensor(type) { + if (this.getSensorShouldFail_.get(type)) { + return {result: SensorCreationResult.ERROR_NOT_AVAILABLE, + initParams: null}; + } + if (this.permissionsDenied_.get(type)) { + return {result: SensorCreationResult.ERROR_NOT_ALLOWED, + initParams: null}; + } + + const offset = type * this.readingSizeInBytes_; + const reportingMode = ReportingMode.ON_CHANGE; + + const sensor = new SensorRemote(); + if (!this.activeSensors_.has(type)) { + const shmemView = new Float64Array( + this.shmemArrayBuffer_, offset, + this.readingSizeInBytes_ / Float64Array.BYTES_PER_ELEMENT); + const mockSensor = new MockSensor( + sensor.$.bindNewPipeAndPassReceiver(), shmemView, reportingMode, + type); + this.activeSensors_.set(type, mockSensor); + this.activeSensors_.get(type).client_ = new SensorClientRemote(); + } + + const rv = this.readOnlySharedBufferHandle_.duplicateBufferHandle( + {readOnly: true}); + if (rv.result != Mojo.RESULT_OK) { + throw new Error('getSensor(): failed to duplicate shared buffer'); + } + + const defaultConfig = { frequency: DEFAULT_FREQUENCY }; + // Consider sensor traits to meet assertions in C++ code (see + // services/device/public/cpp/generic_sensor/sensor_traits.h) + if (type == SensorType.AMBIENT_LIGHT || type == SensorType.MAGNETOMETER) { + this.maxFrequency_ = Math.min(10, this.maxFrequency_); + } + + const client = this.activeSensors_.get(type).client_; + const initParams = { + sensor, + clientReceiver: client.$.bindNewPipeAndPassReceiver(), + memory: {buffer: rv.handle}, + bufferOffset: BigInt(offset), + mode: reportingMode, + defaultConfiguration: defaultConfig, + minimumFrequency: this.minFrequency_, + maximumFrequency: this.maxFrequency_ + }; + + if (this.resolveFuncs_.has(type)) { + for (let resolveFunc of this.resolveFuncs_.get(type)) { + resolveFunc(this.activeSensors_.get(type)); + } + this.resolveFuncs_.delete(type); + } + + return {result: SensorCreationResult.SUCCESS, initParams}; + } + + // Binds object to mojo message pipe + bindToPipe(pipe) { + this.receiver_.$.bindHandle(pipe); + } + + // Mock functions + + // Resets state of mock SensorProvider between test runs. + reset() { + for (const sensor of this.activeSensors_.values()) { + sensor.reset(); + } + this.activeSensors_.clear(); + this.resolveFuncs_.clear(); + this.getSensorShouldFail_.clear(); + this.permissionsDenied_.clear(); + this.maxFrequency_ = 60; + this.minFrequency_ = 1; + this.receiver_.$.close(); + this.interceptor_.stop(); + } + + // Sets flag that forces mock SensorProvider to fail when getSensor() is + // invoked. + setGetSensorShouldFail(sensorType, shouldFail) { + this.getSensorShouldFail_.set(this.mojomSensorType_.get(sensorType), + shouldFail); + } + + setPermissionsDenied(sensorType, permissionsDenied) { + this.permissionsDenied_.set(this.mojomSensorType_.get(sensorType), + permissionsDenied); + } + + // Returns mock sensor that was created in getSensor to the layout test. + getCreatedSensor(sensorType) { + const type = this.mojomSensorType_.get(sensorType); + if (typeof type != "number") { + throw new TypeError(`getCreatedSensor(): Invalid sensor type ${sensorType}`); + } + + if (this.activeSensors_.has(type)) { + return Promise.resolve(this.activeSensors_.get(type)); + } + + return new Promise(resolve => { + if (!this.resolveFuncs_.has(type)) { + this.resolveFuncs_.set(type, []); + } + this.resolveFuncs_.get(type).push(resolve); + }); + } + + // Sets the maximum frequency for a concrete sensor. + setMaximumSupportedFrequency(frequency) { + this.maxFrequency_ = frequency; + } + + // Sets the minimum frequency for a concrete sensor. + setMinimumSupportedFrequency(frequency) { + this.minFrequency_ = frequency; + } + } + + let testInternal = { + initialized: false, + sensorProvider: null + } + + class GenericSensorTestChromium { + constructor() { + Object.freeze(this); // Make it immutable. + } + + async initialize() { + if (testInternal.initialized) + throw new Error('Call reset() before initialize().'); + + // Grant sensor permissions for Chromium testdriver. + // testdriver.js only works in the top-level browsing context, so do + // nothing if we're in e.g. an iframe. + if (window.parent === window) { + for (const entry + of ['accelerometer', 'gyroscope', 'magnetometer', + 'ambient-light-sensor']) { + await test_driver.set_permission({name: entry}, 'granted'); + } + } + + testInternal.sensorProvider = new MockSensorProvider; + testInternal.initialized = true; + } + // Resets state of sensor mocks between test runs. + async reset() { + if (!testInternal.initialized) + throw new Error('Call initialize() before reset().'); + testInternal.sensorProvider.reset(); + testInternal.sensorProvider = null; + testInternal.initialized = false; + + // Wait for an event loop iteration to let any pending mojo commands in + // the sensor provider finish. + await new Promise(resolve => setTimeout(resolve, 0)); + } + + getSensorProvider() { + return testInternal.sensorProvider; + } + } + + return GenericSensorTestChromium; +})(); diff --git a/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js.headers b/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js.headers new file mode 100644 index 0000000000..6805c323df --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/generic_sensor_mocks.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js b/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js new file mode 100644 index 0000000000..b0d2e0af0a --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js @@ -0,0 +1,136 @@ +import {BarcodeDetectionReceiver, BarcodeFormat} from '/gen/services/shape_detection/public/mojom/barcodedetection.mojom.m.js'; +import {BarcodeDetectionProvider, BarcodeDetectionProviderReceiver} from '/gen/services/shape_detection/public/mojom/barcodedetection_provider.mojom.m.js'; + +self.BarcodeDetectionTest = (() => { + // Class that mocks BarcodeDetectionProvider interface defined in + // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/barcodedetection_provider.mojom + class MockBarcodeDetectionProvider { + constructor() { + this.receiver_ = new BarcodeDetectionProviderReceiver(this); + + this.interceptor_ = new MojoInterfaceInterceptor( + BarcodeDetectionProvider.$interfaceName); + this.interceptor_.oninterfacerequest = e => { + if (this.should_close_pipe_on_request_) + e.handle.close(); + else + this.receiver_.$.bindHandle(e.handle); + } + this.interceptor_.start(); + this.should_close_pipe_on_request_ = false; + } + + createBarcodeDetection(request, options) { + this.mockService_ = new MockBarcodeDetection(request, options); + } + + enumerateSupportedFormats() { + return { + supportedFormats: [ + BarcodeFormat.AZTEC, + BarcodeFormat.DATA_MATRIX, + BarcodeFormat.QR_CODE, + ] + }; + } + + getFrameData() { + return this.mockService_.bufferData_; + } + + getFormats() { + return this.mockService_.options_.formats; + } + + reset() { + this.mockService_ = null; + this.should_close_pipe_on_request_ = false; + this.receiver_.$.close(); + this.interceptor_.stop(); + } + + // simulate a 'no implementation available' case + simulateNoImplementation() { + this.should_close_pipe_on_request_ = true; + } + } + + // Class that mocks BarcodeDetection interface defined in + // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/barcodedetection.mojom + class MockBarcodeDetection { + constructor(request, options) { + this.options_ = options; + this.receiver_ = new BarcodeDetectionReceiver(this); + this.receiver_.$.bindHandle(request.handle); + } + + detect(bitmapData) { + this.bufferData_ = + new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData)); + return { + results: [ + { + rawValue : "cats", + boundingBox: { x: 1.0, y: 1.0, width: 100.0, height: 100.0 }, + format: BarcodeFormat.QR_CODE, + cornerPoints: [ + { x: 1.0, y: 1.0 }, + { x: 101.0, y: 1.0 }, + { x: 101.0, y: 101.0 }, + { x: 1.0, y: 101.0 } + ], + }, + { + rawValue : "dogs", + boundingBox: { x: 2.0, y: 2.0, width: 50.0, height: 50.0 }, + format: BarcodeFormat.CODE_128, + cornerPoints: [ + { x: 2.0, y: 2.0 }, + { x: 52.0, y: 2.0 }, + { x: 52.0, y: 52.0 }, + { x: 2.0, y: 52.0 } + ], + }, + ], + }; + } + } + + let testInternal = { + initialized: false, + MockBarcodeDetectionProvider: null + } + + class BarcodeDetectionTestChromium { + constructor() { + Object.freeze(this); // Make it immutable. + } + + initialize() { + if (testInternal.initialized) + throw new Error('Call reset() before initialize().'); + + testInternal.MockBarcodeDetectionProvider = new MockBarcodeDetectionProvider; + testInternal.initialized = true; + } + + // Resets state of barcode detection mocks between test runs. + async reset() { + if (!testInternal.initialized) + throw new Error('Call initialize() before reset().'); + testInternal.MockBarcodeDetectionProvider.reset(); + testInternal.MockBarcodeDetectionProvider = null; + testInternal.initialized = false; + + await new Promise(resolve => setTimeout(resolve, 0)); + } + + MockBarcodeDetectionProvider() { + return testInternal.MockBarcodeDetectionProvider; + } + } + + return BarcodeDetectionTestChromium; +})(); + +self.BarcodeFormat = BarcodeFormat; diff --git a/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js.headers b/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js.headers new file mode 100644 index 0000000000..6c61a34a4e --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-barcodedetection.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8
\ No newline at end of file diff --git a/testing/web-platform/tests/resources/chromium/mock-battery-monitor.headers b/testing/web-platform/tests/resources/chromium/mock-battery-monitor.headers new file mode 100644 index 0000000000..6805c323df --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-battery-monitor.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/testing/web-platform/tests/resources/chromium/mock-battery-monitor.js b/testing/web-platform/tests/resources/chromium/mock-battery-monitor.js new file mode 100644 index 0000000000..8fa27bc56a --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-battery-monitor.js @@ -0,0 +1,61 @@ +import {BatteryMonitor, BatteryMonitorReceiver} from '/gen/services/device/public/mojom/battery_monitor.mojom.m.js'; + +class MockBatteryMonitor { + constructor() { + this.receiver_ = new BatteryMonitorReceiver(this); + this.interceptor_ = + new MojoInterfaceInterceptor(BatteryMonitor.$interfaceName); + this.interceptor_.oninterfacerequest = e => + this.receiver_.$.bindHandle(e.handle); + this.reset(); + } + + start() { + this.interceptor_.start(); + } + + stop() { + this.interceptor_.stop(); + } + + reset() { + this.pendingRequests_ = []; + this.status_ = null; + this.lastKnownStatus_ = null; + } + + queryNextStatus() { + const result = new Promise(resolve => this.pendingRequests_.push(resolve)); + this.runCallbacks_(); + return result; + } + + setBatteryStatus(charging, chargingTime, dischargingTime, level) { + this.status_ = {charging, chargingTime, dischargingTime, level}; + this.lastKnownStatus_ = this.status_; + this.runCallbacks_(); + } + + verifyBatteryStatus(manager) { + assert_not_equals(manager, undefined); + assert_not_equals(this.lastKnownStatus_, null); + assert_equals(manager.charging, this.lastKnownStatus_.charging); + assert_equals(manager.chargingTime, this.lastKnownStatus_.chargingTime); + assert_equals( + manager.dischargingTime, this.lastKnownStatus_.dischargingTime); + assert_equals(manager.level, this.lastKnownStatus_.level); + } + + runCallbacks_() { + if (!this.status_ || !this.pendingRequests_.length) + return; + + let result = {status: this.status_}; + while (this.pendingRequests_.length) { + this.pendingRequests_.pop()(result); + } + this.status_ = null; + } +} + +export const mockBatteryMonitor = new MockBatteryMonitor(); diff --git a/testing/web-platform/tests/resources/chromium/mock-direct-sockets.js b/testing/web-platform/tests/resources/chromium/mock-direct-sockets.js new file mode 100644 index 0000000000..6d557f7a01 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-direct-sockets.js @@ -0,0 +1,94 @@ +'use strict'; + +import {DirectSocketsService, DirectSocketsServiceReceiver} from '/gen/third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom.m.js'; + +self.DirectSocketsServiceTest = (() => { + // Class that mocks DirectSocketsService interface defined in + // https://source.chromium.org/chromium/chromium/src/third_party/blink/public/mojom/direct_sockets/direct_sockets.mojom + class MockDirectSocketsService { + constructor() { + this.interceptor_ = new MojoInterfaceInterceptor(DirectSocketsService.$interfaceName); + this.receiver_ = new DirectSocketsServiceReceiver(this); + this.interceptor_.oninterfacerequest = e => + this.receiver_.$.bindHandle(e.handle); + this.interceptor_.start(); + } + + reset() { + this.receiver_.$.close(); + this.interceptor_.stop(); + } + + openTCPSocket( + options, + receiver, + observer) { + return Promise.resolve({ + // return result = net:Error::NOT_IMPLEMENTED (code -11) + result: -11 + }); + } + + openConnectedUDPSocket( + options, + receiver, + listener) { + return Promise.resolve({ + // return result = net:Error::NOT_IMPLEMENTED (code -11) + result: -11 + }); + } + + openBoundUDPSocket( + options, + receiver, + listener) { + return Promise.resolve({ + // return result = net:Error::NOT_IMPLEMENTED (code -11) + result: -11 + }); + } + + openTCPServerSocket( + options, + receiver) { + return Promise.resolve({ + // return result = net:Error::NOT_IMPLEMENTED (code -11) + result: -11 + }); + } + } + + let testInternal = { + initialized: false, + mockDirectSocketsService: null + } + + class DirectSocketsServiceTestChromium { + constructor() { + Object.freeze(this); // Make it immutable. + } + + initialize() { + if (!testInternal.initialized) { + testInternal = { + mockDirectSocketsService: new MockDirectSocketsService(), + initialized: true + }; + } + } + + async reset() { + if (testInternal.initialized) { + testInternal.mockDirectSocketsService.reset(); + testInternal = { + mockDirectSocketsService: null, + initialized: false + }; + await new Promise(resolve => setTimeout(resolve, 0)); + } + } + } + + return DirectSocketsServiceTestChromium; +})(); diff --git a/testing/web-platform/tests/resources/chromium/mock-facedetection.js b/testing/web-platform/tests/resources/chromium/mock-facedetection.js new file mode 100644 index 0000000000..7ae658621e --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-facedetection.js @@ -0,0 +1,130 @@ +import {FaceDetectionReceiver, LandmarkType} from '/gen/services/shape_detection/public/mojom/facedetection.mojom.m.js'; +import {FaceDetectionProvider, FaceDetectionProviderReceiver} from '/gen/services/shape_detection/public/mojom/facedetection_provider.mojom.m.js'; + +self.FaceDetectionTest = (() => { + // Class that mocks FaceDetectionProvider interface defined in + // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/facedetection_provider.mojom + class MockFaceDetectionProvider { + constructor() { + this.receiver_ = new FaceDetectionProviderReceiver(this); + + this.interceptor_ = new MojoInterfaceInterceptor( + FaceDetectionProvider.$interfaceName); + this.interceptor_.oninterfacerequest = + e => this.receiver_.$.bindHandle(e.handle); + this.interceptor_.start(); + } + + createFaceDetection(request, options) { + this.mockService_ = new MockFaceDetection(request, options); + } + + getFrameData() { + return this.mockService_.bufferData_; + } + + getMaxDetectedFaces() { + return this.mockService_.maxDetectedFaces_; + } + + getFastMode () { + return this.mockService_.fastMode_; + } + + reset() { + this.mockService_ = null; + this.receiver_.$.close(); + this.interceptor_.stop(); + } + } + + // Class that mocks FaceDetection interface defined in + // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/facedetection.mojom + class MockFaceDetection { + constructor(request, options) { + this.maxDetectedFaces_ = options.maxDetectedFaces; + this.fastMode_ = options.fastMode; + this.receiver_ = new FaceDetectionReceiver(this); + this.receiver_.$.bindHandle(request.handle); + } + + detect(bitmapData) { + this.bufferData_ = + new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData)); + return Promise.resolve({ + results: [ + { + boundingBox: {x: 1.0, y: 1.0, width: 100.0, height: 100.0}, + landmarks: [{ + type: LandmarkType.EYE, + locations: [{x: 4.0, y: 5.0}] + }, + { + type: LandmarkType.EYE, + locations: [ + {x: 4.0, y: 5.0}, {x: 5.0, y: 4.0}, {x: 6.0, y: 3.0}, + {x: 7.0, y: 4.0}, {x: 8.0, y: 5.0}, {x: 7.0, y: 6.0}, + {x: 6.0, y: 7.0}, {x: 5.0, y: 6.0} + ] + }] + }, + { + boundingBox: {x: 2.0, y: 2.0, width: 200.0, height: 200.0}, + landmarks: [{ + type: LandmarkType.NOSE, + locations: [{x: 100.0, y: 50.0}] + }, + { + type: LandmarkType.NOSE, + locations: [ + {x: 80.0, y: 50.0}, {x: 70.0, y: 60.0}, {x: 60.0, y: 70.0}, + {x: 70.0, y: 60.0}, {x: 80.0, y: 70.0}, {x: 90.0, y: 80.0}, + {x: 100.0, y: 70.0}, {x: 90.0, y: 60.0}, {x: 80.0, y: 50.0} + ] + }] + }, + { + boundingBox: {x: 3.0, y: 3.0, width: 300.0, height: 300.0}, + landmarks: [] + }, + ] + }); + } + } + + let testInternal = { + initialized: false, + MockFaceDetectionProvider: null + } + + class FaceDetectionTestChromium { + constructor() { + Object.freeze(this); // Make it immutable. + } + + initialize() { + if (testInternal.initialized) + throw new Error('Call reset() before initialize().'); + + testInternal.MockFaceDetectionProvider = new MockFaceDetectionProvider; + testInternal.initialized = true; + } + + // Resets state of face detection mocks between test runs. + async reset() { + if (!testInternal.initialized) + throw new Error('Call initialize() before reset().'); + testInternal.MockFaceDetectionProvider.reset(); + testInternal.MockFaceDetectionProvider = null; + testInternal.initialized = false; + + await new Promise(resolve => setTimeout(resolve, 0)); + } + + MockFaceDetectionProvider() { + return testInternal.MockFaceDetectionProvider; + } + } + + return FaceDetectionTestChromium; +})(); diff --git a/testing/web-platform/tests/resources/chromium/mock-facedetection.js.headers b/testing/web-platform/tests/resources/chromium/mock-facedetection.js.headers new file mode 100644 index 0000000000..6c61a34a4e --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-facedetection.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8
\ No newline at end of file diff --git a/testing/web-platform/tests/resources/chromium/mock-idle-detection.js b/testing/web-platform/tests/resources/chromium/mock-idle-detection.js new file mode 100644 index 0000000000..54fe5dd01e --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-idle-detection.js @@ -0,0 +1,80 @@ +import {IdleManager, IdleManagerError, IdleManagerReceiver} from '/gen/third_party/blink/public/mojom/idle/idle_manager.mojom.m.js'; + +/** + * This is a testing framework that enables us to test the user idle detection + * by intercepting the connection between the renderer and the browser and + * exposing a mocking API for tests. + * + * Usage: + * + * 1) Include <script src="mock.js"></script> in your file. + * 2) Set expectations + * expect(addMonitor).andReturn((threshold, monitorPtr, callback) => { + * // mock behavior + * }) + * 3) Call navigator.idle.query() + * + * The mocking API is blink agnostic and is designed such that other engines + * could implement it too. Here are the symbols that are exposed to tests: + * + * - function addMonitor(): the main/only function that can be mocked. + * - function expect(): the main/only function that enables us to mock it. + * - function close(): disconnects the interceptor. + * - enum UserIdleState {IDLE, ACTIVE}: blink agnostic constants. + * - enum ScreenIdleState {LOCKED, UNLOCKED}: blink agnostic constants. + */ + +class FakeIdleMonitor { + addMonitor(threshold, monitorPtr, callback) { + return this.handler.addMonitor(threshold, monitorPtr); + } + setHandler(handler) { + this.handler = handler; + return this; + } + setBinding(binding) { + this.binding = binding; + return this; + } + close() { + this.binding.$.close(); + } +} + +self.IdleDetectorError = {}; + +self.addMonitor = function addMonitor(threshold, monitorPtr, callback) { + throw new Error("expected to be overriden by tests"); +} + +async function close() { + interceptor.close(); +} + +self.expect = function(call) { + return { + andReturn(callback) { + let handler = {}; + handler[call.name] = callback; + interceptor.setHandler(handler); + } + }; +}; + +function intercept() { + let result = new FakeIdleMonitor(); + + let binding = new IdleManagerReceiver(result); + let interceptor = new MojoInterfaceInterceptor(IdleManager.$interfaceName); + interceptor.oninterfacerequest = e => binding.$.bindHandle(e.handle); + interceptor.start(); + + self.IdleDetectorError.SUCCESS = IdleManagerError.kSuccess; + self.IdleDetectorError.PERMISSION_DISABLED = + IdleManagerError.kPermissionDisabled; + + result.setBinding(binding); + return result; +} + +const interceptor = intercept(); diff --git a/testing/web-platform/tests/resources/chromium/mock-imagecapture.js b/testing/web-platform/tests/resources/chromium/mock-imagecapture.js new file mode 100644 index 0000000000..8424e1e36c --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-imagecapture.js @@ -0,0 +1,309 @@ +import {BackgroundBlurMode, FillLightMode, ImageCapture, ImageCaptureReceiver, MeteringMode, RedEyeReduction} from '/gen/media/capture/mojom/image_capture.mojom.m.js'; + +self.ImageCaptureTest = (() => { + // Class that mocks ImageCapture interface defined in + // https://cs.chromium.org/chromium/src/media/capture/mojom/image_capture.mojom + class MockImageCapture { + constructor() { + this.interceptor_ = + new MojoInterfaceInterceptor(ImageCapture.$interfaceName); + this.interceptor_.oninterfacerequest = + e => this.receiver_.$.bindHandle(e.handle); + this.interceptor_.start(); + + this.state_ = { + state: { + supportedWhiteBalanceModes: [ + MeteringMode.SINGLE_SHOT, + MeteringMode.CONTINUOUS + ], + currentWhiteBalanceMode: MeteringMode.CONTINUOUS, + supportedExposureModes: [ + MeteringMode.MANUAL, + MeteringMode.SINGLE_SHOT, + MeteringMode.CONTINUOUS + ], + currentExposureMode: MeteringMode.MANUAL, + supportedFocusModes: [ + MeteringMode.MANUAL, + MeteringMode.SINGLE_SHOT + ], + currentFocusMode: MeteringMode.MANUAL, + pointsOfInterest: [{ + x: 0.4, + y: 0.6 + }], + + exposureCompensation: { + min: -200.0, + max: 200.0, + current: 33.0, + step: 33.0 + }, + exposureTime: { + min: 100.0, + max: 100000.0, + current: 1000.0, + step: 100.0 + }, + colorTemperature: { + min: 2500.0, + max: 6500.0, + current: 6000.0, + step: 1000.0 + }, + iso: { + min: 100.0, + max: 12000.0, + current: 400.0, + step: 1.0 + }, + + brightness: { + min: 1.0, + max: 10.0, + current: 5.0, + step: 1.0 + }, + contrast: { + min: 2.0, + max: 9.0, + current: 5.0, + step: 1.0 + }, + saturation: { + min: 3.0, + max: 8.0, + current: 6.0, + step: 1.0 + }, + sharpness: { + min: 4.0, + max: 7.0, + current: 7.0, + step: 1.0 + }, + + focusDistance: { + min: 1.0, + max: 10.0, + current: 3.0, + step: 1.0 + }, + + pan: { + min: 0.0, + max: 10.0, + current: 5.0, + step: 2.0 + }, + + tilt: { + min: 0.0, + max: 10.0, + current: 5.0, + step: 2.0 + }, + + zoom: { + min: 0.0, + max: 10.0, + current: 5.0, + step: 5.0 + }, + + supportsTorch: true, + torch: false, + + redEyeReduction: RedEyeReduction.CONTROLLABLE, + height: { + min: 240.0, + max: 2448.0, + current: 240.0, + step: 2.0 + }, + width: { + min: 320.0, + max: 3264.0, + current: 320.0, + step: 3.0 + }, + fillLightMode: [FillLightMode.AUTO, FillLightMode.FLASH], + + supportedBackgroundBlurModes: [ + BackgroundBlurMode.OFF, + BackgroundBlurMode.BLUR + ], + backgroundBlurMode: BackgroundBlurMode.OFF, + } + }; + this.panTiltZoomPermissionStatus_ = null; + this.settings_ = null; + this.receiver_ = new ImageCaptureReceiver(this); + } + + reset() { + this.receiver_.$.close(); + this.interceptor_.stop(); + } + + async getPhotoState(source_id) { + const shouldKeepPanTiltZoom = await this.isPanTiltZoomPermissionGranted(); + if (shouldKeepPanTiltZoom) + return Promise.resolve(this.state_); + + const newState = {...this.state_}; + newState.state.pan = {}; + newState.state.tilt = {}; + newState.state.zoom = {}; + return Promise.resolve(newState); + } + + async setPhotoOptions(source_id, settings) { + const isAllowedToControlPanTiltZoom = await this.isPanTiltZoomPermissionGranted(); + if (!isAllowedToControlPanTiltZoom && + (settings.hasPan || settings.hasTilt || settings.hasZoom)) { + return Promise.resolve({ success: false }); + } + this.settings_ = settings; + if (settings.hasIso) + this.state_.state.iso.current = settings.iso; + if (settings.hasHeight) + this.state_.state.height.current = settings.height; + if (settings.hasWidth) + this.state_.state.width.current = settings.width; + if (settings.hasPan) + this.state_.state.pan.current = settings.pan; + if (settings.hasTilt) + this.state_.state.tilt.current = settings.tilt; + if (settings.hasZoom) + this.state_.state.zoom.current = settings.zoom; + if (settings.hasFocusMode) + this.state_.state.currentFocusMode = settings.focusMode; + if (settings.hasFocusDistance) + this.state_.state.focusDistance.current = settings.focusDistance; + + if (settings.pointsOfInterest.length > 0) { + this.state_.state.pointsOfInterest = + settings.pointsOfInterest; + } + + if (settings.hasExposureMode) + this.state_.state.currentExposureMode = settings.exposureMode; + + if (settings.hasExposureCompensation) { + this.state_.state.exposureCompensation.current = + settings.exposureCompensation; + } + if (settings.hasExposureTime) { + this.state_.state.exposureTime.current = + settings.exposureTime; + } + if (settings.hasWhiteBalanceMode) { + this.state_.state.currentWhiteBalanceMode = + settings.whiteBalanceMode; + } + if (settings.hasFillLightMode) + this.state_.state.fillLightMode = [settings.fillLightMode]; + if (settings.hasRedEyeReduction) + this.state_.state.redEyeReduction = settings.redEyeReduction; + if (settings.hasColorTemperature) { + this.state_.state.colorTemperature.current = + settings.colorTemperature; + } + if (settings.hasBrightness) + this.state_.state.brightness.current = settings.brightness; + if (settings.hasContrast) + this.state_.state.contrast.current = settings.contrast; + if (settings.hasSaturation) + this.state_.state.saturation.current = settings.saturation; + if (settings.hasSharpness) + this.state_.state.sharpness.current = settings.sharpness; + + if (settings.hasTorch) + this.state_.state.torch = settings.torch; + + if (settings.hasBackgroundBlurMode) + this.state_.state.backgroundBlurMode = [settings.backgroundBlurMode]; + + return Promise.resolve({ + success: true + }); + } + + takePhoto(source_id) { + return Promise.resolve({ + blob: { + mimeType: 'image/cat', + data: new Array(2) + } + }); + } + + async isPanTiltZoomPermissionGranted() { + if (!this.panTiltZoomPermissionStatus_) { + this.panTiltZoomPermissionStatus_ = await navigator.permissions.query({ + name: "camera", + panTiltZoom: true + }); + } + return this.panTiltZoomPermissionStatus_.state == "granted"; + } + + state() { + return this.state_.state; + } + + turnOffBackgroundBlurMode() { + this.state_.state.backgroundBlurMode = BackgroundBlurMode.OFF; + } + turnOnBackgroundBlurMode() { + this.state_.state.backgroundBlurMode = BackgroundBlurMode.BLUR; + } + turnOffSupportedBackgroundBlurModes() { + this.state_.state.supportedBackgroundBlurModes = [BackgroundBlurMode.OFF]; + } + turnOnSupportedBackgroundBlurModes() { + this.state_.state.supportedBackgroundBlurModes = [BackgroundBlurMode.BLUR]; + } + + options() { + return this.settings_; + } + } + + let testInternal = { + initialized: false, + mockImageCapture: null + } + + class ImageCaptureTestChromium { + + constructor() { + Object.freeze(this); // Make it immutable. + } + + initialize() { + if (testInternal.initialized) + throw new Error('Call reset() before initialize().'); + + testInternal.mockImageCapture = new MockImageCapture; + testInternal.initialized = true; + } + // Resets state of image capture mocks between test runs. + async reset() { + if (!testInternal.initialized) + throw new Error('Call initialize() before reset().'); + testInternal.mockImageCapture.reset(); + testInternal.mockImageCapture = null; + testInternal.initialized = false; + + await new Promise(resolve => setTimeout(resolve, 0)); + } + mockImageCapture() { + return testInternal.mockImageCapture; + } + } + + return ImageCaptureTestChromium; +})(); diff --git a/testing/web-platform/tests/resources/chromium/mock-managed-config.js b/testing/web-platform/tests/resources/chromium/mock-managed-config.js new file mode 100644 index 0000000000..c9980e1285 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-managed-config.js @@ -0,0 +1,91 @@ +'use strict' + +import{ManagedConfigurationObserverRemote, ManagedConfigurationService, ManagedConfigurationServiceReceiver} from '/gen/third_party/blink/public/mojom/device/device.mojom.m.js'; + + +self.ManagedConfigTest = (() => { + // Class that mocks ManagedConfigurationService interface defined in + // https://source.chromium.org/chromium/chromium/src/third_party/blink/public/mojom/device/device.mojom + class MockManagedConfig { + constructor() { + this.receiver_ = new ManagedConfigurationServiceReceiver(this); + this.interceptor_ = new MojoInterfaceInterceptor( + ManagedConfigurationService.$interfaceName); + this.interceptor_.oninterfacerequest = e => + this.receiver_.$.bindHandle(e.handle); + this.interceptor_.start(); + this.subscription_ = null; + this.reset(); + } + + reset() { + this.configuration_ = null; + this.onObserverAdd_ = null; + } + + async getManagedConfiguration(keys) { + if (this.configuration_ === null) { + return {}; + } + + return { + configurations: Object.keys(this.configuration_) + .filter(key => keys.includes(key)) + .reduce( + (obj, key) => { + obj[key] = + JSON.stringify(this.configuration_[key]); + return obj; + }, + {}) + }; + } + + subscribeToManagedConfiguration(remote) { + this.subscription_ = remote; + if (this.onObserverAdd_ !== null) { + this.onObserverAdd_(); + } + } + + setManagedConfig(value) { + this.configuration_ = value; + if (this.subscription_ !== null) { + this.subscription_.onConfigurationChanged(); + } + } + } + + let testInternal = { + initialized: false, + mockManagedConfig: null + } + + class ManagedConfigTestChromium { + constructor() { + Object.freeze(this); // Make it immutable. + } + + initialize() { + if (testInternal.mockManagedConfig !== null) { + testInternal.mockManagedConfig.reset(); + return; + } + + testInternal.mockManagedConfig = new MockManagedConfig; + testInternal.initialized = true; + } + + setManagedConfig(config) { + testInternal.mockManagedConfig.setManagedConfig(config); + } + + async nextObserverAdded() { + await new Promise(resolve => { + testInternal.mockManagedConfig.onObserverAdd_ = resolve; + }); + } + } + + return ManagedConfigTestChromium; +})(); diff --git a/testing/web-platform/tests/resources/chromium/mock-pressure-service.js b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js new file mode 100644 index 0000000000..02d10f856a --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js @@ -0,0 +1,134 @@ +import {PressureManager, PressureManagerReceiver, PressureStatus} from '/gen/services/device/public/mojom/pressure_manager.mojom.m.js' +import {PressureSource, PressureState} from '/gen/services/device/public/mojom/pressure_update.mojom.m.js' + +class MockPressureService { + constructor() { + this.receiver_ = new PressureManagerReceiver(this); + this.interceptor_ = + new MojoInterfaceInterceptor(PressureManager.$interfaceName); + this.interceptor_.oninterfacerequest = e => { + this.receiver_.$.bindHandle(e.handle); + }; + this.reset(); + this.mojomSourceType_ = new Map([['cpu', PressureSource.kCpu]]); + this.mojomStateType_ = new Map([ + ['nominal', PressureState.kNominal], ['fair', PressureState.kFair], + ['serious', PressureState.kSerious], ['critical', PressureState.kCritical] + ]); + this.pressureServiceReadingTimerId_ = null; + } + + start() { + this.interceptor_.start(); + } + + stop() { + this.stopPlatformCollector(); + this.receiver_.$.close(); + this.interceptor_.stop(); + + // Wait for an event loop iteration to let any pending mojo commands in + // the pressure service finish. + return new Promise(resolve => setTimeout(resolve, 0)); + } + + reset() { + this.observers_ = []; + this.pressureUpdate_ = null; + this.pressureServiceReadingTimerId_ = null; + this.pressureStatus_ = PressureStatus.kOk; + this.updatesDelivered_ = 0; + } + + async addClient(observer, source) { + if (this.observers_.indexOf(observer) >= 0) + throw new Error('addClient() has already been called'); + + // TODO(crbug.com/1342184): Consider other sources. + // For now, "cpu" is the only source. + if (source !== PressureSource.kCpu) + throw new Error('Call addClient() with a wrong PressureSource'); + + observer.onConnectionError.addListener(() => { + // Remove this observer from observer array. + this.observers_.splice(this.observers_.indexOf(observer), 1); + }); + this.observers_.push(observer); + + return {status: this.pressureStatus_}; + } + + startPlatformCollector(sampleRate) { + if (sampleRate === 0) + return; + + if (this.pressureServiceReadingTimerId_ != null) + this.stopPlatformCollector(); + + // The following code for calculating the timestamp was taken from + // https://source.chromium.org/chromium/chromium/src/+/main:third_party/ + // blink/web_tests/http/tests/resources/ + // geolocation-mock.js;l=131;drc=37a9b6c03b9bda9fcd62fc0e5e8016c278abd31f + + // The new Date().getTime() returns the number of milliseconds since the + // UNIX epoch (1970-01-01 00::00:00 UTC), while |internalValue| of the + // device.mojom.PressureUpdate represents the value of microseconds since + // the Windows FILETIME epoch (1601-01-01 00:00:00 UTC). So add the delta + // when sets the |internalValue|. See more info in //base/time/time.h. + const windowsEpoch = Date.UTC(1601, 0, 1, 0, 0, 0, 0); + const unixEpoch = Date.UTC(1970, 0, 1, 0, 0, 0, 0); + // |epochDeltaInMs| equals to base::Time::kTimeTToMicrosecondsOffset. + const epochDeltaInMs = unixEpoch - windowsEpoch; + + const timeout = (1 / sampleRate) * 1000; + this.pressureServiceReadingTimerId_ = self.setInterval(() => { + if (this.pressureUpdate_ === null || this.observers_.length === 0) + return; + this.pressureUpdate_.timestamp = { + internalValue: BigInt((new Date().getTime() + epochDeltaInMs) * 1000) + }; + for (let observer of this.observers_) + observer.onPressureUpdated(this.pressureUpdate_); + this.updatesDelivered_++; + }, timeout); + } + + stopPlatformCollector() { + if (this.pressureServiceReadingTimerId_ != null) { + self.clearInterval(this.pressureServiceReadingTimerId_); + this.pressureServiceReadingTimerId_ = null; + } + this.updatesDelivered_ = 0; + } + + updatesDelivered() { + return this.updatesDelivered_; + } + + setPressureUpdate(source, state) { + if (!this.mojomSourceType_.has(source)) + throw new Error(`PressureSource '${source}' is invalid`); + + if (!this.mojomStateType_.has(state)) + throw new Error(`PressureState '${state}' is invalid`); + + this.pressureUpdate_ = { + source: this.mojomSourceType_.get(source), + state: this.mojomStateType_.get(state), + }; + } + + setExpectedFailure(expectedException) { + assert_true( + expectedException instanceof DOMException, + 'setExpectedFailure() expects a DOMException instance'); + if (expectedException.name === 'NotSupportedError') { + this.pressureStatus_ = PressureStatus.kNotSupported; + } else { + throw new TypeError( + `Unexpected DOMException '${expectedException.name}'`); + } + } +} + +export const mockPressureService = new MockPressureService(); diff --git a/testing/web-platform/tests/resources/chromium/mock-pressure-service.js.headers b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js.headers new file mode 100644 index 0000000000..6805c323df --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-pressure-service.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/testing/web-platform/tests/resources/chromium/mock-subapps.js b/testing/web-platform/tests/resources/chromium/mock-subapps.js new file mode 100644 index 0000000000..b81936713b --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-subapps.js @@ -0,0 +1,89 @@ +'use strict'; + +import {SubAppsService, SubAppsServiceReceiver, SubAppsServiceResultCode} from '/gen/third_party/blink/public/mojom/subapps/sub_apps_service.mojom.m.js'; + +self.SubAppsServiceTest = (() => { + // Class that mocks SubAppsService interface defined in /third_party/blink/public/mojom/subapps/sub_apps_service.mojom + + class MockSubAppsService { + constructor() { + this.interceptor_ = + new MojoInterfaceInterceptor(SubAppsService.$interfaceName); + this.receiver_ = new SubAppsServiceReceiver(this); + this.interceptor_.oninterfacerequest = + e => this.receiver_.$.bindHandle(e.handle); + this.interceptor_.start(); + } + + reset() { + this.interceptor_.stop(); + this.receiver_.$.close(); + } + + add(sub_apps) { + return Promise.resolve({ + result: testInternal.addCallReturnValue, + }); + } + + list() { + return Promise.resolve({ + result: { + resultCode: testInternal.serviceResultCode, + subAppsList: testInternal.listCallReturnValue, + } + }); + } + + remove() { + return Promise.resolve({ + result: testInternal.removeCallReturnValue, + }); + } + } + + let testInternal = { + initialized: false, + mockSubAppsService: null, + serviceResultCode: 0, + addCallReturnValue: [], + listCallReturnValue: [], + removeCallReturnValue: [], + } + + class SubAppsServiceTestChromium { + constructor() { + Object.freeze(this); // Make it immutable. + } + + initialize(service_result_code, add_call_return_value, list_call_return_value, remove_call_return_value) { + if (!testInternal.initialized) { + testInternal = { + mockSubAppsService: new MockSubAppsService(), + initialized: true, + serviceResultCode: service_result_code, + addCallReturnValue: add_call_return_value, + listCallReturnValue: list_call_return_value, + removeCallReturnValue: remove_call_return_value, + }; + }; + } + + async reset() { + if (testInternal.initialized) { + testInternal.mockSubAppsService.reset(); + testInternal = { + mockSubAppsService: null, + initialized: false, + serviceResultCode: 0, + addCallReturnValue: [], + listCallReturnValue: [], + removeCallReturnValue: [], + }; + await new Promise(resolve => setTimeout(resolve, 0)); + } + } + } + + return SubAppsServiceTestChromium; +})(); diff --git a/testing/web-platform/tests/resources/chromium/mock-textdetection.js b/testing/web-platform/tests/resources/chromium/mock-textdetection.js new file mode 100644 index 0000000000..52ca987e28 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-textdetection.js @@ -0,0 +1,92 @@ +import {TextDetection, TextDetectionReceiver} from '/gen/services/shape_detection/public/mojom/textdetection.mojom.m.js'; + +self.TextDetectionTest = (() => { + // Class that mocks TextDetection interface defined in + // https://cs.chromium.org/chromium/src/services/shape_detection/public/mojom/textdetection.mojom + class MockTextDetection { + constructor() { + this.receiver_ = new TextDetectionReceiver(this); + this.interceptor_ = + new MojoInterfaceInterceptor(TextDetection.$interfaceName); + this.interceptor_.oninterfacerequest = + e => this.receiver_.$.bindHandle(e.handle); + this.interceptor_.start(); + } + + detect(bitmapData) { + this.bufferData_ = + new Uint32Array(getArrayBufferFromBigBuffer(bitmapData.pixelData)); + return Promise.resolve({ + results: [ + { + rawValue : "cats", + boundingBox: { x: 1.0, y: 1.0, width: 100.0, height: 100.0 }, + cornerPoints: [ + { x: 1.0, y: 1.0 }, + { x: 101.0, y: 1.0 }, + { x: 101.0, y: 101.0 }, + { x: 1.0, y: 101.0 } + ] + }, + { + rawValue : "dogs", + boundingBox: { x: 2.0, y: 2.0, width: 50.0, height: 50.0 }, + cornerPoints: [ + { x: 2.0, y: 2.0 }, + { x: 52.0, y: 2.0 }, + { x: 52.0, y: 52.0 }, + { x: 2.0, y: 52.0 } + ] + }, + ], + }); + } + + getFrameData() { + return this.bufferData_; + } + + reset() { + this.receiver_.$.close(); + this.interceptor_.stop(); + } + + } + + let testInternal = { + initialized: false, + MockTextDetection: null + } + + class TextDetectionTestChromium { + constructor() { + Object.freeze(this); // Make it immutable. + } + + initialize() { + if (testInternal.initialized) + throw new Error('Call reset() before initialize().'); + + testInternal.MockTextDetection = new MockTextDetection; + testInternal.initialized = true; + } + + // Resets state of text detection mocks between test runs. + async reset() { + if (!testInternal.initialized) + throw new Error('Call initialize() before reset().'); + testInternal.MockTextDetection.reset(); + testInternal.MockTextDetection = null; + testInternal.initialized = false; + + await new Promise(resolve => setTimeout(resolve, 0)); + } + + MockTextDetection() { + return testInternal.MockTextDetection; + } + } + + return TextDetectionTestChromium; + +})(); diff --git a/testing/web-platform/tests/resources/chromium/mock-textdetection.js.headers b/testing/web-platform/tests/resources/chromium/mock-textdetection.js.headers new file mode 100644 index 0000000000..6c61a34a4e --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/mock-textdetection.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8
\ No newline at end of file diff --git a/testing/web-platform/tests/resources/chromium/nfc-mock.js b/testing/web-platform/tests/resources/chromium/nfc-mock.js new file mode 100644 index 0000000000..31a71b9e22 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/nfc-mock.js @@ -0,0 +1,437 @@ +import {NDEFErrorType, NDEFRecordTypeCategory, NFC, NFCReceiver} from '/gen/services/device/public/mojom/nfc.mojom.m.js'; + +// Converts between NDEFMessageInit https://w3c.github.io/web-nfc/#dom-ndefmessage +// and mojom.NDEFMessage structure, so that watch function can be tested. +function toMojoNDEFMessage(message) { + let ndefMessage = {data: []}; + for (let record of message.records) { + ndefMessage.data.push(toMojoNDEFRecord(record)); + } + return ndefMessage; +} + +function toMojoNDEFRecord(record) { + let nfcRecord = {}; + // Simply checks the existence of ':' to decide whether it's an external + // type or a local type. As a mock, no need to really implement the validation + // algorithms for them. + if (record.recordType.startsWith(':')) { + nfcRecord.category = NDEFRecordTypeCategory.kLocal; + } else if (record.recordType.search(':') != -1) { + nfcRecord.category = NDEFRecordTypeCategory.kExternal; + } else { + nfcRecord.category = NDEFRecordTypeCategory.kStandardized; + } + nfcRecord.recordType = record.recordType; + nfcRecord.mediaType = record.mediaType; + nfcRecord.id = record.id; + if (record.recordType == 'text') { + nfcRecord.encoding = record.encoding == null? 'utf-8': record.encoding; + nfcRecord.lang = record.lang == null? 'en': record.lang; + } + nfcRecord.data = toByteArray(record.data); + if (record.data != null && record.data.records !== undefined) { + // |record.data| may be an NDEFMessageInit, i.e. the payload is a message. + nfcRecord.payloadMessage = toMojoNDEFMessage(record.data); + } + return nfcRecord; +} + +// Converts JS objects to byte array. +function toByteArray(data) { + if (data instanceof ArrayBuffer) + return new Uint8Array(data); + else if (ArrayBuffer.isView(data)) + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength); + + let byteArray = new Uint8Array(0); + let tmpData = data; + if (typeof tmpData === 'object' || typeof tmpData === 'number') + tmpData = JSON.stringify(tmpData); + + if (typeof tmpData === 'string') + byteArray = new TextEncoder().encode(tmpData); + + return byteArray; +} + +// Compares NDEFRecords that were provided / received by the mock service. +// TODO: Use different getters to get received record data, +// see spec changes at https://github.com/w3c/web-nfc/pull/243. +self.compareNDEFRecords = function(providedRecord, receivedRecord) { + assert_equals(providedRecord.recordType, receivedRecord.recordType); + + if (providedRecord.id === undefined) { + assert_equals(null, receivedRecord.id); + } else { + assert_equals(providedRecord.id, receivedRecord.id); + } + + if (providedRecord.mediaType === undefined) { + assert_equals(null, receivedRecord.mediaType); + } else { + assert_equals(providedRecord.mediaType, receivedRecord.mediaType); + } + + assert_not_equals(providedRecord.recordType, 'empty'); + + if (providedRecord.recordType == 'text') { + assert_equals( + providedRecord.encoding == null? 'utf-8': providedRecord.encoding, + receivedRecord.encoding); + assert_equals(providedRecord.lang == null? 'en': providedRecord.lang, + receivedRecord.lang); + } else { + assert_equals(null, receivedRecord.encoding); + assert_equals(null, receivedRecord.lang); + } + + assert_array_equals(toByteArray(providedRecord.data), + new Uint8Array(receivedRecord.data)); +} + +// Compares NDEFWriteOptions structures that were provided to API and +// received by the mock mojo service. +self.assertNDEFWriteOptionsEqual = function(provided, received) { + if (provided.overwrite !== undefined) + assert_equals(provided.overwrite, !!received.overwrite); + else + assert_equals(!!received.overwrite, true); +} + +// Compares NDEFReaderOptions structures that were provided to API and +// received by the mock mojo service. +self.assertNDEFReaderOptionsEqual = function(provided, received) { + if (provided.url !== undefined) + assert_equals(provided.url, received.url); + else + assert_equals(received.url, ''); + + if (provided.mediaType !== undefined) + assert_equals(provided.mediaType, received.mediaType); + else + assert_equals(received.mediaType, ''); + + if (provided.recordType !== undefined) { + assert_equals(provided.recordType, received.recordType); + } +} + +function createNDEFError(type) { + return {error: (type != null ? {errorType: type, errorMessage: ''} : null)}; +} + +self.WebNFCTest = (() => { + class MockNFC { + constructor() { + this.receiver_ = new NFCReceiver(this); + + this.interceptor_ = new MojoInterfaceInterceptor(NFC.$interfaceName); + this.interceptor_.oninterfacerequest = e => { + if (this.should_close_pipe_on_request_) + e.handle.close(); + else + this.receiver_.$.bindHandle(e.handle); + } + + this.interceptor_.start(); + + this.hw_status_ = NFCHWStatus.ENABLED; + this.pushed_message_ = null; + this.pending_write_options_ = null; + this.pending_push_promise_func_ = null; + this.push_completed_ = true; + this.pending_make_read_only_promise_func_ = null; + this.make_read_only_completed_ = true; + this.client_ = null; + this.watchers_ = []; + this.reading_messages_ = []; + this.operations_suspended_ = false; + this.is_formatted_tag_ = false; + this.data_transfer_failed_ = false; + this.should_close_pipe_on_request_ = false; + } + + // NFC delegate functions. + async push(message, options) { + const error = this.getHWError(); + if (error) + return error; + // Cancels previous pending push operation. + if (this.pending_push_promise_func_) { + this.cancelPendingPushOperation(); + } + + this.pushed_message_ = message; + this.pending_write_options_ = options; + return new Promise(resolve => { + if (this.operations_suspended_ || !this.push_completed_) { + // Leaves the push pending. + this.pending_push_promise_func_ = resolve; + } else if (this.is_formatted_tag_ && !options.overwrite) { + // Resolves with NotAllowedError if there are NDEF records on the device + // and overwrite is false. + resolve(createNDEFError(NDEFErrorType.NOT_ALLOWED)); + } else if (this.data_transfer_failed_) { + // Resolves with NetworkError if data transfer fails. + resolve(createNDEFError(NDEFErrorType.IO_ERROR)); + } else { + resolve(createNDEFError(null)); + } + }); + } + + async cancelPush() { + this.cancelPendingPushOperation(); + return createNDEFError(null); + } + + async makeReadOnly(options) { + const error = this.getHWError(); + if (error) + return error; + // Cancels previous pending makeReadOnly operation. + if (this.pending_make_read_only_promise_func_) { + this.cancelPendingMakeReadOnlyOperation(); + } + + if (this.operations_suspended_ || !this.make_read_only_completed_) { + // Leaves the makeReadOnly pending. + return new Promise(resolve => { + this.pending_make_read_only_promise_func_ = resolve; + }); + } else if (this.data_transfer_failed_) { + // Resolves with NetworkError if data transfer fails. + return createNDEFError(NDEFErrorType.IO_ERROR); + } else { + return createNDEFError(null); + } + } + + async cancelMakeReadOnly() { + this.cancelPendingMakeReadOnlyOperation(); + return createNDEFError(null); + } + + setClient(client) { + this.client_ = client; + } + + async watch(id) { + assert_true(id > 0); + const error = this.getHWError(); + if (error) { + return error; + } + + this.watchers_.push({id: id}); + // Ignores reading if NFC operation is suspended + // or the NFC tag does not expose NDEF technology. + if (!this.operations_suspended_) { + // Triggers onWatch if the new watcher matches existing messages. + for (let message of this.reading_messages_) { + this.client_.onWatch( + [id], fake_tag_serial_number, toMojoNDEFMessage(message)); + } + } + + return createNDEFError(null); + } + + cancelWatch(id) { + let index = this.watchers_.findIndex(value => value.id === id); + if (index !== -1) { + this.watchers_.splice(index, 1); + } + } + + getHWError() { + if (this.hw_status_ === NFCHWStatus.DISABLED) + return createNDEFError(NDEFErrorType.NOT_READABLE); + if (this.hw_status_ === NFCHWStatus.NOT_SUPPORTED) + return createNDEFError(NDEFErrorType.NOT_SUPPORTED); + return null; + } + + setHWStatus(status) { + this.hw_status_ = status; + } + + pushedMessage() { + return this.pushed_message_; + } + + writeOptions() { + return this.pending_write_options_; + } + + watchOptions() { + assert_not_equals(this.watchers_.length, 0); + return this.watchers_[this.watchers_.length - 1].options; + } + + setPendingPushCompleted(result) { + this.push_completed_ = result; + } + + setPendingMakeReadOnlyCompleted(result) { + this.make_read_only_completed_ = result; + } + + reset() { + this.hw_status_ = NFCHWStatus.ENABLED; + this.watchers_ = []; + this.reading_messages_ = []; + this.operations_suspended_ = false; + this.cancelPendingPushOperation(); + this.cancelPendingMakeReadOnlyOperation(); + this.is_formatted_tag_ = false; + this.data_transfer_failed_ = false; + this.should_close_pipe_on_request_ = false; + } + + cancelPendingPushOperation() { + if (this.pending_push_promise_func_) { + this.pending_push_promise_func_( + createNDEFError(NDEFErrorType.OPERATION_CANCELLED)); + this.pending_push_promise_func_ = null; + } + + this.pushed_message_ = null; + this.pending_write_options_ = null; + this.push_completed_ = true; + } + + cancelPendingMakeReadOnlyOperation() { + if (this.pending_make_read_only_promise_func_) { + this.pending_make_read_only_promise_func_( + createNDEFError(NDEFErrorType.OPERATION_CANCELLED)); + this.pending_make_read_only_promise_func_ = null; + } + + this.make_read_only_completed_ = true; + } + + // Sets message that is used to deliver NFC reading updates. + setReadingMessage(message) { + this.reading_messages_.push(message); + // Ignores reading if NFC operation is suspended. + if(this.operations_suspended_) return; + // when overwrite is false, the write algorithm will read the NFC tag + // to determine if it has NDEF records on it. + if (this.pending_write_options_ && this.pending_write_options_.overwrite) + return; + // Triggers onWatch if the new message matches existing watchers. + for (let watcher of this.watchers_) { + this.client_.onWatch( + [watcher.id], fake_tag_serial_number, + toMojoNDEFMessage(message)); + } + } + + // Suspends all pending NFC operations. Could be used when web page + // visibility is lost. + suspendNFCOperations() { + this.operations_suspended_ = true; + } + + // Resumes all suspended NFC operations. + resumeNFCOperations() { + this.operations_suspended_ = false; + // Resumes pending NFC reading. + for (let watcher of this.watchers_) { + for (let message of this.reading_messages_) { + this.client_.onWatch( + [watcher.id], fake_tag_serial_number, + toMojoNDEFMessage(message)); + } + } + // Resumes pending push operation. + if (this.pending_push_promise_func_ && this.push_completed_) { + this.pending_push_promise_func_(createNDEFError(null)); + this.pending_push_promise_func_ = null; + } + // Resumes pending makeReadOnly operation. + if (this.pending_make_read_only_promise_func_ && + this.make_read_only_completed_) { + this.pending_make_read_only_promise_func_(createNDEFError(null)); + this.pending_make_read_only_promise_func_ = null; + } + } + + // Simulates the device coming in proximity does not expose NDEF technology. + simulateNonNDEFTagDiscovered() { + // Notify NotSupportedError to all active readers. + if (this.watchers_.length != 0) { + this.client_.onError({ + errorType: NDEFErrorType.NOT_SUPPORTED, + errorMessage: '' + }); + } + // Reject the pending push with NotSupportedError. + if (this.pending_push_promise_func_) { + this.pending_push_promise_func_( + createNDEFError(NDEFErrorType.NOT_SUPPORTED)); + this.pending_push_promise_func_ = null; + } + // Reject the pending makeReadOnly with NotSupportedError. + if (this.pending_make_read_only_promise_func_) { + this.pending_make_read_only_promise_func_( + createNDEFError(NDEFErrorType.NOT_SUPPORTED)); + this.pending_make_read_only_promise_func_ = null; + } + } + + setIsFormattedTag(isFormatted) { + this.is_formatted_tag_ = isFormatted; + } + + simulateDataTransferFails() { + this.data_transfer_failed_ = true; + } + + simulateClosedPipe() { + this.should_close_pipe_on_request_ = true; + } + } + + let testInternal = { + initialized: false, + mockNFC: null + } + + class NFCTestChromium { + constructor() { + Object.freeze(this); // Makes it immutable. + } + + async initialize() { + if (testInternal.initialized) + throw new Error('Call reset() before initialize().'); + + // Grant nfc permissions for Chromium testdriver. + await test_driver.set_permission({ name: 'nfc' }, 'granted'); + + if (testInternal.mockNFC == null) { + testInternal.mockNFC = new MockNFC(); + } + testInternal.initialized = true; + } + + // Reuses the nfc mock but resets its state between test runs. + async reset() { + if (!testInternal.initialized) + throw new Error('Call initialize() before reset().'); + testInternal.mockNFC.reset(); + testInternal.initialized = false; + + await new Promise(resolve => setTimeout(resolve, 0)); + } + + getMockNFC() { + return testInternal.mockNFC; + } + } + + return NFCTestChromium; +})(); diff --git a/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js new file mode 100644 index 0000000000..ecea5e760c --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js @@ -0,0 +1,629 @@ +'use strict'; + +const content = {}; +const bluetooth = {}; +const MOJO_CHOOSER_EVENT_TYPE_MAP = {}; + +function toMojoCentralState(state) { + switch (state) { + case 'absent': + return bluetooth.mojom.CentralState.ABSENT; + case 'powered-off': + return bluetooth.mojom.CentralState.POWERED_OFF; + case 'powered-on': + return bluetooth.mojom.CentralState.POWERED_ON; + default: + throw `Unsupported value ${state} for state.`; + } +} + +// Converts bluetooth.mojom.WriteType to a string. If |writeType| is +// invalid, this method will throw. +function writeTypeToString(writeType) { + switch (writeType) { + case bluetooth.mojom.WriteType.kNone: + return 'none'; + case bluetooth.mojom.WriteType.kWriteDefaultDeprecated: + return 'default-deprecated'; + case bluetooth.mojom.WriteType.kWriteWithResponse: + return 'with-response'; + case bluetooth.mojom.WriteType.kWriteWithoutResponse: + return 'without-response'; + default: + throw `Unknown bluetooth.mojom.WriteType: ${writeType}`; + } +} + +// Canonicalizes UUIDs and converts them to Mojo UUIDs. +function canonicalizeAndConvertToMojoUUID(uuids) { + let canonicalUUIDs = uuids.map(val => ({uuid: BluetoothUUID.getService(val)})); + return canonicalUUIDs; +} + +// Converts WebIDL a record<DOMString, BufferSource> to a map<K, array<uint8>> to +// use for Mojo, where the value for K is calculated using keyFn. +function convertToMojoMap(record, keyFn, isNumberKey = false) { + let map = new Map(); + for (const [key, value] of Object.entries(record)) { + let buffer = ArrayBuffer.isView(value) ? value.buffer : value; + if (isNumberKey) { + let numberKey = parseInt(key); + if (Number.isNaN(numberKey)) + throw `Map key ${key} is not a number`; + map.set(keyFn(numberKey), Array.from(new Uint8Array(buffer))); + continue; + } + map.set(keyFn(key), Array.from(new Uint8Array(buffer))); + } + return map; +} + +function ArrayToMojoCharacteristicProperties(arr) { + const struct = {}; + arr.forEach(property => { struct[property] = true; }); + return struct; +} + +class FakeBluetooth { + constructor() { + this.fake_bluetooth_ptr_ = new bluetooth.mojom.FakeBluetoothRemote(); + this.fake_bluetooth_ptr_.$.bindNewPipeAndPassReceiver().bindInBrowser('process'); + this.fake_central_ = null; + } + + // Set it to indicate whether the platform supports BLE. For example, + // Windows 7 is a platform that doesn't support Low Energy. On the other + // hand Windows 10 is a platform that does support LE, even if there is no + // Bluetooth radio present. + async setLESupported(supported) { + if (typeof supported !== 'boolean') throw 'Type Not Supported'; + await this.fake_bluetooth_ptr_.setLESupported(supported); + } + + // Returns a promise that resolves with a FakeCentral that clients can use + // to simulate events that a device in the Central/Observer role would + // receive as well as monitor the operations performed by the device in the + // Central/Observer role. + // Calls sets LE as supported. + // + // A "Central" object would allow its clients to receive advertising events + // and initiate connections to peripherals i.e. operations of two roles + // defined by the Bluetooth Spec: Observer and Central. + // See Bluetooth 4.2 Vol 3 Part C 2.2.2 "Roles when Operating over an + // LE Physical Transport". + async simulateCentral({state}) { + if (this.fake_central_) + throw 'simulateCentral() should only be called once'; + + await this.setLESupported(true); + + let {fakeCentral: fake_central_ptr} = + await this.fake_bluetooth_ptr_.simulateCentral( + toMojoCentralState(state)); + this.fake_central_ = new FakeCentral(fake_central_ptr); + return this.fake_central_; + } + + // Returns true if there are no pending responses. + async allResponsesConsumed() { + let {consumed} = await this.fake_bluetooth_ptr_.allResponsesConsumed(); + return consumed; + } + + // Returns a promise that resolves with a FakeChooser that clients can use to + // simulate chooser events. + async getManualChooser() { + if (typeof this.fake_chooser_ === 'undefined') { + this.fake_chooser_ = new FakeChooser(); + } + return this.fake_chooser_; + } +} + +// FakeCentral allows clients to simulate events that a device in the +// Central/Observer role would receive as well as monitor the operations +// performed by the device in the Central/Observer role. +class FakeCentral { + constructor(fake_central_ptr) { + this.fake_central_ptr_ = fake_central_ptr; + this.peripherals_ = new Map(); + } + + // Simulates a peripheral with |address|, |name|, |manufacturerData| and + // |known_service_uuids| that has already been connected to the system. If the + // peripheral existed already it updates its name, manufacturer data, and + // known UUIDs. |known_service_uuids| should be an array of + // BluetoothServiceUUIDs + // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid + // + // Platforms offer methods to retrieve devices that have already been + // connected to the system or weren't connected through the UA e.g. a user + // connected a peripheral through the system's settings. This method is + // intended to simulate peripherals that those methods would return. + async simulatePreconnectedPeripheral( + {address, name, manufacturerData = {}, knownServiceUUIDs = []}) { + await this.fake_central_ptr_.simulatePreconnectedPeripheral( + address, name, + convertToMojoMap(manufacturerData, Number, true /* isNumberKey */), + canonicalizeAndConvertToMojoUUID(knownServiceUUIDs)); + + return this.fetchOrCreatePeripheral_(address); + } + + // Simulates an advertisement packet described by |scanResult| being received + // from a device. If central is currently scanning, the device will appear on + // the list of discovered devices. + async simulateAdvertisementReceived(scanResult) { + // Create a deep-copy to prevent the original |scanResult| from being + // modified when the UUIDs, manufacturer, and service data are converted. + let clonedScanResult = JSON.parse(JSON.stringify(scanResult)); + + if ('uuids' in scanResult.scanRecord) { + clonedScanResult.scanRecord.uuids = + canonicalizeAndConvertToMojoUUID(scanResult.scanRecord.uuids); + } + + // Convert the optional appearance and txPower fields to the corresponding + // Mojo structures, since Mojo does not support optional interger values. If + // the fields are undefined, set the hasValue field as false and value as 0. + // Otherwise, set the hasValue field as true and value with the field value. + const has_appearance = 'appearance' in scanResult.scanRecord; + clonedScanResult.scanRecord.appearance = { + hasValue: has_appearance, + value: (has_appearance ? scanResult.scanRecord.appearance : 0) + } + + const has_tx_power = 'txPower' in scanResult.scanRecord; + clonedScanResult.scanRecord.txPower = { + hasValue: has_tx_power, + value: (has_tx_power ? scanResult.scanRecord.txPower : 0) + } + + // Convert manufacturerData from a record<DOMString, BufferSource> into a + // map<uint8, array<uint8>> for Mojo. + if ('manufacturerData' in scanResult.scanRecord) { + clonedScanResult.scanRecord.manufacturerData = convertToMojoMap( + scanResult.scanRecord.manufacturerData, Number, + true /* isNumberKey */); + } + + // Convert serviceData from a record<DOMString, BufferSource> into a + // map<string, array<uint8>> for Mojo. + if ('serviceData' in scanResult.scanRecord) { + clonedScanResult.scanRecord.serviceData.serviceData = convertToMojoMap( + scanResult.scanRecord.serviceData, BluetoothUUID.getService, + false /* isNumberKey */); + } + + await this.fake_central_ptr_.simulateAdvertisementReceived( + clonedScanResult); + + return this.fetchOrCreatePeripheral_(clonedScanResult.deviceAddress); + } + + // Simulates a change in the central device described by |state|. For example, + // setState('powered-off') can be used to simulate the central device powering + // off. + // + // This method should be used for any central state changes after + // simulateCentral() has been called to create a FakeCentral object. + async setState(state) { + await this.fake_central_ptr_.setState(toMojoCentralState(state)); + } + + // Create a fake_peripheral object from the given address. + fetchOrCreatePeripheral_(address) { + let peripheral = this.peripherals_.get(address); + if (peripheral === undefined) { + peripheral = new FakePeripheral(address, this.fake_central_ptr_); + this.peripherals_.set(address, peripheral); + } + return peripheral; + } +} + +class FakePeripheral { + constructor(address, fake_central_ptr) { + this.address = address; + this.fake_central_ptr_ = fake_central_ptr; + } + + // Adds a fake GATT Service with |uuid| to be discovered when discovering + // the peripheral's GATT Attributes. Returns a FakeRemoteGATTService + // corresponding to this service. |uuid| should be a BluetoothServiceUUIDs + // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothserviceuuid + async addFakeService({uuid}) { + let {serviceId: service_id} = await this.fake_central_ptr_.addFakeService( + this.address, {uuid: BluetoothUUID.getService(uuid)}); + + if (service_id === null) throw 'addFakeService failed'; + + return new FakeRemoteGATTService( + service_id, this.address, this.fake_central_ptr_); + } + + // Sets the next GATT Connection request response to |code|. |code| could be + // an HCI Error Code from BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a + // number outside that range returned by specific platforms e.g. Android + // returns 0x101 to signal a GATT failure + // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE + async setNextGATTConnectionResponse({code}) { + let {success} = + await this.fake_central_ptr_.setNextGATTConnectionResponse( + this.address, code); + + if (success !== true) throw 'setNextGATTConnectionResponse failed.'; + } + + // Sets the next GATT Discovery request response for peripheral with + // |address| to |code|. |code| could be an HCI Error Code from + // BT 4.2 Vol 2 Part D 1.3 List Of Error Codes or a number outside that + // range returned by specific platforms e.g. Android returns 0x101 to signal + // a GATT failure + // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE + // + // The following procedures defined at BT 4.2 Vol 3 Part G Section 4. + // "GATT Feature Requirements" are used to discover attributes of the + // GATT Server: + // - Primary Service Discovery + // - Relationship Discovery + // - Characteristic Discovery + // - Characteristic Descriptor Discovery + // This method aims to simulate the response once all of these procedures + // have completed or if there was an error during any of them. + async setNextGATTDiscoveryResponse({code}) { + let {success} = + await this.fake_central_ptr_.setNextGATTDiscoveryResponse( + this.address, code); + + if (success !== true) throw 'setNextGATTDiscoveryResponse failed.'; + } + + // Simulates a GATT disconnection from the peripheral with |address|. + async simulateGATTDisconnection() { + let {success} = + await this.fake_central_ptr_.simulateGATTDisconnection(this.address); + + if (success !== true) throw 'simulateGATTDisconnection failed.'; + } + + // Simulates an Indication from the peripheral's GATT `Service Changed` + // Characteristic from BT 4.2 Vol 3 Part G 7.1. This Indication is signaled + // when services, characteristics, or descriptors are changed, added, or + // removed. + // + // The value for `Service Changed` is a range of attribute handles that have + // changed. However, this testing specification works at an abstracted + // level and does not expose setting attribute handles when adding + // attributes. Consequently, this simulate method should include the full + // range of all the peripheral's attribute handle values. + async simulateGATTServicesChanged() { + let {success} = + await this.fake_central_ptr_.simulateGATTServicesChanged(this.address); + + if (success !== true) throw 'simulateGATTServicesChanged failed.'; + } +} + +class FakeRemoteGATTService { + constructor(service_id, peripheral_address, fake_central_ptr) { + this.service_id_ = service_id; + this.peripheral_address_ = peripheral_address; + this.fake_central_ptr_ = fake_central_ptr; + } + + // Adds a fake GATT Characteristic with |uuid| and |properties| + // to this fake service. The characteristic will be found when discovering + // the peripheral's GATT Attributes. Returns a FakeRemoteGATTCharacteristic + // corresponding to the added characteristic. + async addFakeCharacteristic({uuid, properties}) { + let {characteristicId: characteristic_id} = + await this.fake_central_ptr_.addFakeCharacteristic( + {uuid: BluetoothUUID.getCharacteristic(uuid)}, + ArrayToMojoCharacteristicProperties(properties), + this.service_id_, + this.peripheral_address_); + + if (characteristic_id === null) throw 'addFakeCharacteristic failed'; + + return new FakeRemoteGATTCharacteristic( + characteristic_id, this.service_id_, + this.peripheral_address_, this.fake_central_ptr_); + } + + // Removes the fake GATT service from its fake peripheral. + async remove() { + let {success} = + await this.fake_central_ptr_.removeFakeService( + this.service_id_, + this.peripheral_address_); + + if (!success) throw 'remove failed'; + } +} + +class FakeRemoteGATTCharacteristic { + constructor(characteristic_id, service_id, peripheral_address, + fake_central_ptr) { + this.ids_ = [characteristic_id, service_id, peripheral_address]; + this.descriptors_ = []; + this.fake_central_ptr_ = fake_central_ptr; + } + + // Adds a fake GATT Descriptor with |uuid| to be discovered when + // discovering the peripheral's GATT Attributes. Returns a + // FakeRemoteGATTDescriptor corresponding to this descriptor. |uuid| should + // be a BluetoothDescriptorUUID + // https://webbluetoothcg.github.io/web-bluetooth/#typedefdef-bluetoothdescriptoruuid + async addFakeDescriptor({uuid}) { + let {descriptorId: descriptor_id} = + await this.fake_central_ptr_.addFakeDescriptor( + {uuid: BluetoothUUID.getDescriptor(uuid)}, ...this.ids_); + + if (descriptor_id === null) throw 'addFakeDescriptor failed'; + + let fake_descriptor = new FakeRemoteGATTDescriptor( + descriptor_id, ...this.ids_, this.fake_central_ptr_); + this.descriptors_.push(fake_descriptor); + + return fake_descriptor; + } + + // Sets the next read response for characteristic to |code| and |value|. + // |code| could be a GATT Error Response from + // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range + // returned by specific platforms e.g. Android returns 0x101 to signal a GATT + // failure. + // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE + async setNextReadResponse(gatt_code, value=null) { + if (gatt_code === 0 && value === null) { + throw '|value| can\'t be null if read should success.'; + } + if (gatt_code !== 0 && value !== null) { + throw '|value| must be null if read should fail.'; + } + + let {success} = + await this.fake_central_ptr_.setNextReadCharacteristicResponse( + gatt_code, value, ...this.ids_); + + if (!success) throw 'setNextReadCharacteristicResponse failed'; + } + + // Sets the next write response for this characteristic to |code|. If + // writing to a characteristic that only supports 'write_without_response' + // the set response will be ignored. + // |code| could be a GATT Error Response from + // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range + // returned by specific platforms e.g. Android returns 0x101 to signal a GATT + // failure. + async setNextWriteResponse(gatt_code) { + let {success} = + await this.fake_central_ptr_.setNextWriteCharacteristicResponse( + gatt_code, ...this.ids_); + + if (!success) throw 'setNextWriteCharacteristicResponse failed'; + } + + // Sets the next subscribe to notifications response for characteristic with + // |characteristic_id| in |service_id| and in |peripheral_address| to + // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F + // 3.4.1.1 Error Response or a number outside that range returned by + // specific platforms e.g. Android returns 0x101 to signal a GATT failure. + async setNextSubscribeToNotificationsResponse(gatt_code) { + let {success} = + await this.fake_central_ptr_.setNextSubscribeToNotificationsResponse( + gatt_code, ...this.ids_); + + if (!success) throw 'setNextSubscribeToNotificationsResponse failed'; + } + + // Sets the next unsubscribe to notifications response for characteristic with + // |characteristic_id| in |service_id| and in |peripheral_address| to + // |code|. |code| could be a GATT Error Response from BT 4.2 Vol 3 Part F + // 3.4.1.1 Error Response or a number outside that range returned by + // specific platforms e.g. Android returns 0x101 to signal a GATT failure. + async setNextUnsubscribeFromNotificationsResponse(gatt_code) { + let {success} = + await this.fake_central_ptr_.setNextUnsubscribeFromNotificationsResponse( + gatt_code, ...this.ids_); + + if (!success) throw 'setNextUnsubscribeToNotificationsResponse failed'; + } + + // Returns true if notifications from the characteristic have been subscribed + // to. + async isNotifying() { + let {success, isNotifying} = + await this.fake_central_ptr_.isNotifying(...this.ids_); + + if (!success) throw 'isNotifying failed'; + + return isNotifying; + } + + // Gets the last successfully written value to the characteristic and its + // write type. Write type is one of 'none', 'default-deprecated', + // 'with-response', 'without-response'. Returns {lastValue: null, + // lastWriteType: 'none'} if no value has yet been written to the + // characteristic. + async getLastWrittenValue() { + let {success, value, writeType} = + await this.fake_central_ptr_.getLastWrittenCharacteristicValue( + ...this.ids_); + + if (!success) throw 'getLastWrittenCharacteristicValue failed'; + + return {lastValue: value, lastWriteType: writeTypeToString(writeType)}; + } + + // Removes the fake GATT Characteristic from its fake service. + async remove() { + let {success} = + await this.fake_central_ptr_.removeFakeCharacteristic(...this.ids_); + + if (!success) throw 'remove failed'; + } +} + +class FakeRemoteGATTDescriptor { + constructor(descriptor_id, + characteristic_id, + service_id, + peripheral_address, + fake_central_ptr) { + this.ids_ = [ + descriptor_id, characteristic_id, service_id, peripheral_address]; + this.fake_central_ptr_ = fake_central_ptr; + } + + // Sets the next read response for descriptor to |code| and |value|. + // |code| could be a GATT Error Response from + // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range + // returned by specific platforms e.g. Android returns 0x101 to signal a GATT + // failure. + // https://developer.android.com/reference/android/bluetooth/BluetoothGatt.html#GATT_FAILURE + async setNextReadResponse(gatt_code, value=null) { + if (gatt_code === 0 && value === null) { + throw '|value| cannot be null if read should succeed.'; + } + if (gatt_code !== 0 && value !== null) { + throw '|value| must be null if read should fail.'; + } + + let {success} = + await this.fake_central_ptr_.setNextReadDescriptorResponse( + gatt_code, value, ...this.ids_); + + if (!success) throw 'setNextReadDescriptorResponse failed'; + } + + // Sets the next write response for this descriptor to |code|. + // |code| could be a GATT Error Response from + // BT 4.2 Vol 3 Part F 3.4.1.1 Error Response or a number outside that range + // returned by specific platforms e.g. Android returns 0x101 to signal a GATT + // failure. + async setNextWriteResponse(gatt_code) { + let {success} = + await this.fake_central_ptr_.setNextWriteDescriptorResponse( + gatt_code, ...this.ids_); + + if (!success) throw 'setNextWriteDescriptorResponse failed'; + } + + // Gets the last successfully written value to the descriptor. + // Returns null if no value has yet been written to the descriptor. + async getLastWrittenValue() { + let {success, value} = + await this.fake_central_ptr_.getLastWrittenDescriptorValue( + ...this.ids_); + + if (!success) throw 'getLastWrittenDescriptorValue failed'; + + return value; + } + + // Removes the fake GATT Descriptor from its fake characteristic. + async remove() { + let {success} = + await this.fake_central_ptr_.removeFakeDescriptor(...this.ids_); + + if (!success) throw 'remove failed'; + } +} + +// FakeChooser allows clients to simulate user actions on a Bluetooth chooser, +// and records the events produced by the Bluetooth chooser. +class FakeChooser { + constructor() { + let fakeBluetoothChooserFactoryRemote = + new content.mojom.FakeBluetoothChooserFactoryRemote(); + fakeBluetoothChooserFactoryRemote.$.bindNewPipeAndPassReceiver().bindInBrowser('process'); + + this.fake_bluetooth_chooser_ptr_ = + new content.mojom.FakeBluetoothChooserRemote(); + this.fake_bluetooth_chooser_client_receiver_ = + new content.mojom.FakeBluetoothChooserClientReceiver(this); + fakeBluetoothChooserFactoryRemote.createFakeBluetoothChooser( + this.fake_bluetooth_chooser_ptr_.$.bindNewPipeAndPassReceiver(), + this.fake_bluetooth_chooser_client_receiver_.$.associateAndPassRemote()); + + this.events_ = new Array(); + this.event_listener_ = null; + } + + // If the chooser has received more events than |numOfEvents| this function + // will reject the promise, else it will wait until |numOfEvents| events are + // received before resolving with an array of |FakeBluetoothChooserEvent| + // objects. + async waitForEvents(numOfEvents) { + return new Promise(resolve => { + if (this.events_.length > numOfEvents) { + throw `Asked for ${numOfEvents} event(s), but received ` + + `${this.events_.length}.`; + } + + this.event_listener_ = () => { + if (this.events_.length === numOfEvents) { + let result = Array.from(this.events_); + this.event_listener_ = null; + this.events_ = []; + resolve(result); + } + }; + this.event_listener_(); + }); + } + + async selectPeripheral(peripheral) { + if (!(peripheral instanceof FakePeripheral)) { + throw '|peripheral| must be an instance of FakePeripheral'; + } + await this.fake_bluetooth_chooser_ptr_.selectPeripheral(peripheral.address); + } + + async cancel() { + await this.fake_bluetooth_chooser_ptr_.cancel(); + } + + async rescan() { + await this.fake_bluetooth_chooser_ptr_.rescan(); + } + + onEvent(chooserEvent) { + chooserEvent.type = MOJO_CHOOSER_EVENT_TYPE_MAP[chooserEvent.type]; + this.events_.push(chooserEvent); + if (this.event_listener_ !== null) { + this.event_listener_(); + } + } +} + +async function initializeChromiumResources() { + content.mojom = await import( + '/gen/content/web_test/common/fake_bluetooth_chooser.mojom.m.js'); + bluetooth.mojom = await import( + '/gen/device/bluetooth/public/mojom/test/fake_bluetooth.mojom.m.js'); + + const map = MOJO_CHOOSER_EVENT_TYPE_MAP; + const types = content.mojom.ChooserEventType; + map[types.CHOOSER_OPENED] = 'chooser-opened'; + map[types.CHOOSER_CLOSED] = 'chooser-closed'; + map[types.ADAPTER_REMOVED] = 'adapter-removed'; + map[types.ADAPTER_DISABLED] = 'adapter-disabled'; + map[types.ADAPTER_ENABLED] = 'adapter-enabled'; + map[types.DISCOVERY_FAILED_TO_START] = 'discovery-failed-to-start'; + map[types.DISCOVERING] = 'discovering'; + map[types.DISCOVERY_IDLE] = 'discovery-idle'; + map[types.ADD_OR_UPDATE_DEVICE] = 'add-or-update-device'; + + // If this line fails, it means that current environment does not support the + // Web Bluetooth Test API. + try { + navigator.bluetooth.test = new FakeBluetooth(); + } catch { + throw 'Web Bluetooth Test API is not implemented on this ' + + 'environment. See the bluetooth README at ' + + 'https://github.com/web-platform-tests/wpt/blob/master/bluetooth/README.md#web-bluetooth-testing'; + } +} diff --git a/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js.headers b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js.headers new file mode 100644 index 0000000000..6805c323df --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/web-bluetooth-test.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/testing/web-platform/tests/resources/chromium/webusb-child-test.js b/testing/web-platform/tests/resources/chromium/webusb-child-test.js new file mode 100644 index 0000000000..21412f66b0 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/webusb-child-test.js @@ -0,0 +1,47 @@ +'use strict'; + +// This polyfill prepares a child context to be attached to a parent context. +// The parent must call navigator.usb.test.attachToContext() to attach to the +// child context. +(() => { + if (this.constructor.name === 'DedicatedWorkerGlobalScope' || + this !== window.top) { + + // Run Chromium specific set up code. + if (typeof MojoInterfaceInterceptor !== 'undefined') { + let messageChannel = new MessageChannel(); + messageChannel.port1.onmessage = async (messageEvent) => { + if (messageEvent.data.type === 'Attach') { + messageEvent.data.interfaces.forEach(interfaceName => { + let interfaceInterceptor = + new MojoInterfaceInterceptor(interfaceName); + interfaceInterceptor.oninterfacerequest = + e => messageChannel.port1.postMessage({ + type: interfaceName, + handle: e.handle + }, [e.handle]); + interfaceInterceptor.start(); + }); + + // Wait for a call to GetDevices() to ensure that the interface + // handles are forwarded to the parent context. + try { + await navigator.usb.getDevices(); + } catch (e) { + // This can happen in case of, for example, testing usb disallowed + // iframe. + console.error(`getDevices() throws error: ${e.name}: ${e.message}`); + } + + messageChannel.port1.postMessage({ type: 'Complete' }); + } + }; + + let message = { type: 'ReadyForAttachment', port: messageChannel.port2 }; + if (typeof Window !== 'undefined') + parent.postMessage(message, '*', [messageChannel.port2]); + else + postMessage(message, [messageChannel.port2]); + } + } +})(); diff --git a/testing/web-platform/tests/resources/chromium/webusb-child-test.js.headers b/testing/web-platform/tests/resources/chromium/webusb-child-test.js.headers new file mode 100644 index 0000000000..6805c323df --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/webusb-child-test.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/testing/web-platform/tests/resources/chromium/webusb-test.js b/testing/web-platform/tests/resources/chromium/webusb-test.js new file mode 100644 index 0000000000..7cca63d919 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/webusb-test.js @@ -0,0 +1,583 @@ +'use strict'; + +// This polyfill library implements the WebUSB Test API as specified here: +// https://wicg.github.io/webusb/test/ + +(() => { + +// These variables are logically members of the USBTest class but are defined +// here to hide them from being visible as fields of navigator.usb.test. +let internal = { + intialized: false, + + webUsbService: null, + webUsbServiceInterceptor: null, + + messagePort: null, +}; + +let mojom = {}; + +async function loadMojomDefinitions() { + const deviceMojom = + await import('/gen/services/device/public/mojom/usb_device.mojom.m.js'); + const serviceMojom = await import( + '/gen/third_party/blink/public/mojom/usb/web_usb_service.mojom.m.js'); + return { + ...deviceMojom, + ...serviceMojom, + }; +} + +function getMessagePort(target) { + return new Promise(resolve => { + target.addEventListener('message', messageEvent => { + if (messageEvent.data.type === 'ReadyForAttachment') { + if (internal.messagePort === null) { + internal.messagePort = messageEvent.data.port; + } + resolve(); + } + }, {once: true}); + }); +} + +// Converts an ECMAScript String object to an instance of +// mojo_base.mojom.String16. +function mojoString16ToString(string16) { + return String.fromCharCode.apply(null, string16.data); +} + +// Converts an instance of mojo_base.mojom.String16 to an ECMAScript String. +function stringToMojoString16(string) { + let array = new Array(string.length); + for (var i = 0; i < string.length; ++i) { + array[i] = string.charCodeAt(i); + } + return { data: array } +} + +function fakeDeviceInitToDeviceInfo(guid, init) { + let deviceInfo = { + guid: guid + "", + usbVersionMajor: init.usbVersionMajor, + usbVersionMinor: init.usbVersionMinor, + usbVersionSubminor: init.usbVersionSubminor, + classCode: init.deviceClass, + subclassCode: init.deviceSubclass, + protocolCode: init.deviceProtocol, + vendorId: init.vendorId, + productId: init.productId, + deviceVersionMajor: init.deviceVersionMajor, + deviceVersionMinor: init.deviceVersionMinor, + deviceVersionSubminor: init.deviceVersionSubminor, + manufacturerName: stringToMojoString16(init.manufacturerName), + productName: stringToMojoString16(init.productName), + serialNumber: stringToMojoString16(init.serialNumber), + activeConfiguration: init.activeConfigurationValue, + configurations: [] + }; + init.configurations.forEach(config => { + var configInfo = { + configurationValue: config.configurationValue, + configurationName: stringToMojoString16(config.configurationName), + selfPowered: false, + remoteWakeup: false, + maximumPower: 0, + interfaces: [], + extraData: new Uint8Array() + }; + config.interfaces.forEach(iface => { + var interfaceInfo = { + interfaceNumber: iface.interfaceNumber, + alternates: [] + }; + iface.alternates.forEach(alternate => { + var alternateInfo = { + alternateSetting: alternate.alternateSetting, + classCode: alternate.interfaceClass, + subclassCode: alternate.interfaceSubclass, + protocolCode: alternate.interfaceProtocol, + interfaceName: stringToMojoString16(alternate.interfaceName), + endpoints: [], + extraData: new Uint8Array() + }; + alternate.endpoints.forEach(endpoint => { + var endpointInfo = { + endpointNumber: endpoint.endpointNumber, + packetSize: endpoint.packetSize, + synchronizationType: mojom.UsbSynchronizationType.NONE, + usageType: mojom.UsbUsageType.DATA, + pollingInterval: 0, + extraData: new Uint8Array() + }; + switch (endpoint.direction) { + case "in": + endpointInfo.direction = mojom.UsbTransferDirection.INBOUND; + break; + case "out": + endpointInfo.direction = mojom.UsbTransferDirection.OUTBOUND; + break; + } + switch (endpoint.type) { + case "bulk": + endpointInfo.type = mojom.UsbTransferType.BULK; + break; + case "interrupt": + endpointInfo.type = mojom.UsbTransferType.INTERRUPT; + break; + case "isochronous": + endpointInfo.type = mojom.UsbTransferType.ISOCHRONOUS; + break; + } + alternateInfo.endpoints.push(endpointInfo); + }); + interfaceInfo.alternates.push(alternateInfo); + }); + configInfo.interfaces.push(interfaceInfo); + }); + deviceInfo.configurations.push(configInfo); + }); + return deviceInfo; +} + +function convertMojoDeviceFilters(input) { + let output = []; + input.forEach(filter => { + output.push(convertMojoDeviceFilter(filter)); + }); + return output; +} + +function convertMojoDeviceFilter(input) { + let output = {}; + if (input.hasVendorId) + output.vendorId = input.vendorId; + if (input.hasProductId) + output.productId = input.productId; + if (input.hasClassCode) + output.classCode = input.classCode; + if (input.hasSubclassCode) + output.subclassCode = input.subclassCode; + if (input.hasProtocolCode) + output.protocolCode = input.protocolCode; + if (input.serialNumber) + output.serialNumber = mojoString16ToString(input.serialNumber); + return output; +} + +class FakeDevice { + constructor(deviceInit) { + this.info_ = deviceInit; + this.opened_ = false; + this.currentConfiguration_ = null; + this.claimedInterfaces_ = new Map(); + } + + getConfiguration() { + if (this.currentConfiguration_) { + return Promise.resolve({ + value: this.currentConfiguration_.configurationValue }); + } else { + return Promise.resolve({ value: 0 }); + } + } + + open() { + assert_false(this.opened_); + this.opened_ = true; + return Promise.resolve({result: {success: mojom.UsbOpenDeviceSuccess.OK}}); + } + + close() { + assert_true(this.opened_); + this.opened_ = false; + return Promise.resolve(); + } + + setConfiguration(value) { + assert_true(this.opened_); + + let selectedConfiguration = this.info_.configurations.find( + configuration => configuration.configurationValue == value); + // Blink should never request an invalid configuration. + assert_not_equals(selectedConfiguration, undefined); + this.currentConfiguration_ = selectedConfiguration; + return Promise.resolve({ success: true }); + } + + async claimInterface(interfaceNumber) { + assert_true(this.opened_); + assert_false(this.currentConfiguration_ == null, 'device configured'); + assert_false(this.claimedInterfaces_.has(interfaceNumber), + 'interface already claimed'); + + const protectedInterfaces = new Set([ + mojom.USB_AUDIO_CLASS, + mojom.USB_HID_CLASS, + mojom.USB_MASS_STORAGE_CLASS, + mojom.USB_SMART_CARD_CLASS, + mojom.USB_VIDEO_CLASS, + mojom.USB_AUDIO_VIDEO_CLASS, + mojom.USB_WIRELESS_CLASS, + ]); + + let iface = this.currentConfiguration_.interfaces.find( + iface => iface.interfaceNumber == interfaceNumber); + // Blink should never request an invalid interface or alternate. + assert_false(iface == undefined); + if (iface.alternates.some( + alt => protectedInterfaces.has(alt.interfaceClass))) { + return {result: mojom.UsbClaimInterfaceResult.kProtectedClass}; + } + + this.claimedInterfaces_.set(interfaceNumber, 0); + return {result: mojom.UsbClaimInterfaceResult.kSuccess}; + } + + releaseInterface(interfaceNumber) { + assert_true(this.opened_); + assert_false(this.currentConfiguration_ == null, 'device configured'); + assert_true(this.claimedInterfaces_.has(interfaceNumber)); + this.claimedInterfaces_.delete(interfaceNumber); + return Promise.resolve({ success: true }); + } + + setInterfaceAlternateSetting(interfaceNumber, alternateSetting) { + assert_true(this.opened_); + assert_false(this.currentConfiguration_ == null, 'device configured'); + assert_true(this.claimedInterfaces_.has(interfaceNumber)); + + let iface = this.currentConfiguration_.interfaces.find( + iface => iface.interfaceNumber == interfaceNumber); + // Blink should never request an invalid interface or alternate. + assert_false(iface == undefined); + assert_true(iface.alternates.some( + x => x.alternateSetting == alternateSetting)); + this.claimedInterfaces_.set(interfaceNumber, alternateSetting); + return Promise.resolve({ success: true }); + } + + reset() { + assert_true(this.opened_); + return Promise.resolve({ success: true }); + } + + clearHalt(endpoint) { + assert_true(this.opened_); + assert_false(this.currentConfiguration_ == null, 'device configured'); + // TODO(reillyg): Assert that endpoint is valid. + return Promise.resolve({ success: true }); + } + + async controlTransferIn(params, length, timeout) { + assert_true(this.opened_); + + if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE || + params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) && + this.currentConfiguration_ == null) { + return { + status: mojom.UsbTransferStatus.PERMISSION_DENIED, + }; + } + + return { + status: mojom.UsbTransferStatus.OK, + data: { + buffer: [ + length >> 8, length & 0xff, params.request, params.value >> 8, + params.value & 0xff, params.index >> 8, params.index & 0xff + ] + } + }; + } + + async controlTransferOut(params, data, timeout) { + assert_true(this.opened_); + + if ((params.recipient == mojom.UsbControlTransferRecipient.INTERFACE || + params.recipient == mojom.UsbControlTransferRecipient.ENDPOINT) && + this.currentConfiguration_ == null) { + return { + status: mojom.UsbTransferStatus.PERMISSION_DENIED, + }; + } + + return {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength}; + } + + genericTransferIn(endpointNumber, length, timeout) { + assert_true(this.opened_); + assert_false(this.currentConfiguration_ == null, 'device configured'); + // TODO(reillyg): Assert that endpoint is valid. + let data = new Array(length); + for (let i = 0; i < length; ++i) + data[i] = i & 0xff; + return Promise.resolve( + {status: mojom.UsbTransferStatus.OK, data: {buffer: data}}); + } + + genericTransferOut(endpointNumber, data, timeout) { + assert_true(this.opened_); + assert_false(this.currentConfiguration_ == null, 'device configured'); + // TODO(reillyg): Assert that endpoint is valid. + return Promise.resolve( + {status: mojom.UsbTransferStatus.OK, bytesWritten: data.byteLength}); + } + + isochronousTransferIn(endpointNumber, packetLengths, timeout) { + assert_true(this.opened_); + assert_false(this.currentConfiguration_ == null, 'device configured'); + // TODO(reillyg): Assert that endpoint is valid. + let data = new Array(packetLengths.reduce((a, b) => a + b, 0)); + let dataOffset = 0; + let packets = new Array(packetLengths.length); + for (let i = 0; i < packetLengths.length; ++i) { + for (let j = 0; j < packetLengths[i]; ++j) + data[dataOffset++] = j & 0xff; + packets[i] = { + length: packetLengths[i], + transferredLength: packetLengths[i], + status: mojom.UsbTransferStatus.OK + }; + } + return Promise.resolve({data: {buffer: data}, packets: packets}); + } + + isochronousTransferOut(endpointNumber, data, packetLengths, timeout) { + assert_true(this.opened_); + assert_false(this.currentConfiguration_ == null, 'device configured'); + // TODO(reillyg): Assert that endpoint is valid. + let packets = new Array(packetLengths.length); + for (let i = 0; i < packetLengths.length; ++i) { + packets[i] = { + length: packetLengths[i], + transferredLength: packetLengths[i], + status: mojom.UsbTransferStatus.OK + }; + } + return Promise.resolve({ packets: packets }); + } +} + +class FakeWebUsbService { + constructor() { + this.receiver_ = new mojom.WebUsbServiceReceiver(this); + this.devices_ = new Map(); + this.devicesByGuid_ = new Map(); + this.client_ = null; + this.nextGuid_ = 0; + } + + addBinding(handle) { + this.receiver_.$.bindHandle(handle); + } + + addDevice(fakeDevice, info) { + let device = { + fakeDevice: fakeDevice, + guid: (this.nextGuid_++).toString(), + info: info, + receivers: [], + }; + this.devices_.set(fakeDevice, device); + this.devicesByGuid_.set(device.guid, device); + if (this.client_) + this.client_.onDeviceAdded(fakeDeviceInitToDeviceInfo(device.guid, info)); + } + + async forgetDevice(guid) { + // Permissions are currently untestable through WPT. + } + + removeDevice(fakeDevice) { + let device = this.devices_.get(fakeDevice); + if (!device) + throw new Error('Cannot remove unknown device.'); + + for (const receiver of device.receivers) + receiver.$.close(); + this.devices_.delete(device.fakeDevice); + this.devicesByGuid_.delete(device.guid); + if (this.client_) { + this.client_.onDeviceRemoved( + fakeDeviceInitToDeviceInfo(device.guid, device.info)); + } + } + + removeAllDevices() { + this.devices_.forEach(device => { + for (const receiver of device.receivers) + receiver.$.close(); + this.client_.onDeviceRemoved( + fakeDeviceInitToDeviceInfo(device.guid, device.info)); + }); + this.devices_.clear(); + this.devicesByGuid_.clear(); + } + + getDevices() { + let devices = []; + this.devices_.forEach(device => { + devices.push(fakeDeviceInitToDeviceInfo(device.guid, device.info)); + }); + return Promise.resolve({ results: devices }); + } + + getDevice(guid, request) { + let retrievedDevice = this.devicesByGuid_.get(guid); + if (retrievedDevice) { + const receiver = + new mojom.UsbDeviceReceiver(new FakeDevice(retrievedDevice.info)); + receiver.$.bindHandle(request.handle); + receiver.onConnectionError.addListener(() => { + if (retrievedDevice.fakeDevice.onclose) + retrievedDevice.fakeDevice.onclose(); + }); + retrievedDevice.receivers.push(receiver); + } else { + request.handle.close(); + } + } + + getPermission(options) { + return new Promise(resolve => { + if (navigator.usb.test.onrequestdevice) { + navigator.usb.test.onrequestdevice( + new USBDeviceRequestEvent(options, resolve)); + } else { + resolve({ result: null }); + } + }); + } + + setClient(client) { + this.client_ = client; + } +} + +class USBDeviceRequestEvent { + constructor(options, resolve) { + this.filters = convertMojoDeviceFilters(options.filters); + this.exclusionFilters = convertMojoDeviceFilters(options.exclusionFilters); + this.resolveFunc_ = resolve; + } + + respondWith(value) { + // Wait until |value| resolves (if it is a Promise). This function returns + // no value. + Promise.resolve(value).then(fakeDevice => { + let device = internal.webUsbService.devices_.get(fakeDevice); + let result = null; + if (device) { + result = fakeDeviceInitToDeviceInfo(device.guid, device.info); + } + this.resolveFunc_({ result: result }); + }, () => { + this.resolveFunc_({ result: null }); + }); + } +} + +// Unlike FakeDevice this class is exported to callers of USBTest.addFakeDevice. +class FakeUSBDevice { + constructor() { + this.onclose = null; + } + + disconnect() { + setTimeout(() => internal.webUsbService.removeDevice(this), 0); + } +} + +class USBTest { + constructor() { + this.onrequestdevice = undefined; + } + + async initialize() { + if (internal.initialized) + return; + + // Be ready to handle 'ReadyForAttachment' message from child iframes. + if ('window' in self) { + getMessagePort(window); + } + + mojom = await loadMojomDefinitions(); + internal.webUsbService = new FakeWebUsbService(); + internal.webUsbServiceInterceptor = + new MojoInterfaceInterceptor(mojom.WebUsbService.$interfaceName); + internal.webUsbServiceInterceptor.oninterfacerequest = + e => internal.webUsbService.addBinding(e.handle); + internal.webUsbServiceInterceptor.start(); + + // Wait for a call to GetDevices() to pass between the renderer and the + // mock in order to establish that everything is set up. + await navigator.usb.getDevices(); + internal.initialized = true; + } + + // Returns a promise that is resolved when the implementation of |usb| in the + // global scope for |context| is controlled by the current context. + attachToContext(context) { + if (!internal.initialized) + throw new Error('Call initialize() before attachToContext()'); + + let target = context.constructor.name === 'Worker' ? context : window; + return getMessagePort(target).then(() => { + return new Promise(resolve => { + internal.messagePort.onmessage = channelEvent => { + switch (channelEvent.data.type) { + case mojom.WebUsbService.$interfaceName: + internal.webUsbService.addBinding(channelEvent.data.handle); + break; + case 'Complete': + resolve(); + break; + } + }; + internal.messagePort.postMessage({ + type: 'Attach', + interfaces: [ + mojom.WebUsbService.$interfaceName, + ] + }); + }); + }); + } + + addFakeDevice(deviceInit) { + if (!internal.initialized) + throw new Error('Call initialize() before addFakeDevice().'); + + // |addDevice| and |removeDevice| are called in a setTimeout callback so + // that tests do not rely on the device being immediately available which + // may not be true for all implementations of this test API. + let fakeDevice = new FakeUSBDevice(); + setTimeout( + () => internal.webUsbService.addDevice(fakeDevice, deviceInit), 0); + return fakeDevice; + } + + reset() { + if (!internal.initialized) + throw new Error('Call initialize() before reset().'); + + // Reset the mocks in a setTimeout callback so that tests do not rely on + // the fact that this polyfill can do this synchronously. + return new Promise(resolve => { + setTimeout(() => { + if (internal.messagePort !== null) + internal.messagePort.close(); + internal.messagePort = null; + internal.webUsbService.removeAllDevices(); + resolve(); + }, 0); + }); + } +} + +navigator.usb.test = new USBTest(); + +})(); diff --git a/testing/web-platform/tests/resources/chromium/webusb-test.js.headers b/testing/web-platform/tests/resources/chromium/webusb-test.js.headers new file mode 100644 index 0000000000..6805c323df --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/webusb-test.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js b/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js new file mode 100644 index 0000000000..22c6c12d08 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js @@ -0,0 +1,298 @@ +'use strict'; + +// Math helper - used mainly in hit test implementation done by webxr-test.js +class XRMathHelper { + static toString(p) { + return "[" + p.x + "," + p.y + "," + p.z + "," + p.w + "]"; + } + + static transform_by_matrix(matrix, point) { + return { + x : matrix[0] * point.x + matrix[4] * point.y + matrix[8] * point.z + matrix[12] * point.w, + y : matrix[1] * point.x + matrix[5] * point.y + matrix[9] * point.z + matrix[13] * point.w, + z : matrix[2] * point.x + matrix[6] * point.y + matrix[10] * point.z + matrix[14] * point.w, + w : matrix[3] * point.x + matrix[7] * point.y + matrix[11] * point.z + matrix[15] * point.w, + }; + } + + static neg(p) { + return {x : -p.x, y : -p.y, z : -p.z, w : p.w}; + } + + static sub(lhs, rhs) { + // .w is treated here like an entity type, 1 signifies points, 0 signifies vectors. + // point - point, point - vector, vector - vector are ok, vector - point is not. + if (lhs.w != rhs.w && lhs.w == 0.0) { + throw new Error("vector - point not allowed: " + toString(lhs) + "-" + toString(rhs)); + } + + return {x : lhs.x - rhs.x, y : lhs.y - rhs.y, z : lhs.z - rhs.z, w : lhs.w - rhs.w}; + } + + static add(lhs, rhs) { + if (lhs.w == rhs.w && lhs.w == 1.0) { + throw new Error("point + point not allowed", p1, p2); + } + + return {x : lhs.x + rhs.x, y : lhs.y + rhs.y, z : lhs.z + rhs.z, w : lhs.w + rhs.w}; + } + + static cross(lhs, rhs) { + if (lhs.w != 0.0 || rhs.w != 0.0) { + throw new Error("cross product not allowed: " + toString(lhs) + "x" + toString(rhs)); + } + + return { + x : lhs.y * rhs.z - lhs.z * rhs.y, + y : lhs.z * rhs.x - lhs.x * rhs.z, + z : lhs.x * rhs.y - lhs.y * rhs.x, + w : 0 + }; + } + + static dot(lhs, rhs) { + if (lhs.w != 0 || rhs.w != 0) { + throw new Error("dot product not allowed: " + toString(lhs) + "x" + toString(rhs)); + } + + return lhs.x * rhs.x + lhs.y * rhs.y + lhs.z * rhs.z; + } + + static mul(scalar, vector) { + if (vector.w != 0) { + throw new Error("scalar * vector not allowed", scalar, vector); + } + + return {x : vector.x * scalar, y : vector.y * scalar, z : vector.z * scalar, w : vector.w}; + } + + static length(vector) { + return Math.sqrt(XRMathHelper.dot(vector, vector)); + } + + static normalize(vector) { + const l = XRMathHelper.length(vector); + return XRMathHelper.mul(1.0/l, vector); + } + + // All |face|'s points and |point| must be co-planar. + static pointInFace(point, face) { + const normalize = XRMathHelper.normalize; + const sub = XRMathHelper.sub; + const length = XRMathHelper.length; + const cross = XRMathHelper.cross; + + let onTheRight = null; + let previous_point = face[face.length - 1]; + + // |point| is in |face| if it's on the same side of all the edges. + for (let i = 0; i < face.length; ++i) { + const current_point = face[i]; + + const edge_direction = normalize(sub(current_point, previous_point)); + const turn_direction = normalize(sub(point, current_point)); + + const sin_turn_angle = length(cross(edge_direction, turn_direction)); + + if (onTheRight == null) { + onTheRight = sin_turn_angle >= 0; + } else { + if (onTheRight && sin_turn_angle < 0) return false; + if (!onTheRight && sin_turn_angle > 0) return false; + } + + previous_point = current_point; + } + + return true; + } + + static det2x2(m00, m01, m10, m11) { + return m00 * m11 - m01 * m10; + } + + static det3x3( + m00, m01, m02, + m10, m11, m12, + m20, m21, m22 + ){ + const det2x2 = XRMathHelper.det2x2; + + return m00 * det2x2(m11, m12, m21, m22) + - m01 * det2x2(m10, m12, m20, m22) + + m02 * det2x2(m10, m11, m20, m21); + } + + static det4x4( + m00, m01, m02, m03, + m10, m11, m12, m13, + m20, m21, m22, m23, + m30, m31, m32, m33 + ) { + const det3x3 = XRMathHelper.det3x3; + + return m00 * det3x3(m11, m12, m13, + m21, m22, m23, + m31, m32, m33) + - m01 * det3x3(m10, m12, m13, + m20, m22, m23, + m30, m32, m33) + + m02 * det3x3(m10, m11, m13, + m20, m21, m23, + m30, m31, m33) + - m03 * det3x3(m10, m11, m12, + m20, m21, m22, + m30, m31, m32); + } + + static inv2(m) { + // mij - i-th column, j-th row + const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3]; + const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7]; + const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11]; + const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15]; + + const det = det4x4( + m00, m01, m02, m03, + m10, m11, m12, m13, + m20, m21, m22, m23, + m30, m31, m32, m33 + ); + } + + static transpose(m) { + const result = Array(16); + for (let i = 0; i < 4; i++) { + for (let j = 0; j < 4; j++) { + result[i * 4 + j] = m[j * 4 + i]; + } + } + return result; + } + + // Inverts the matrix, ported from transformation_matrix.cc. + static inverse(m) { + const det3x3 = XRMathHelper.det3x3; + + // mij - i-th column, j-th row + const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3]; + const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7]; + const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11]; + const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15]; + + const det = XRMathHelper.det4x4( + m00, m01, m02, m03, + m10, m11, m12, m13, + m20, m21, m22, m23, + m30, m31, m32, m33 + ); + + if (Math.abs(det) < 0.0001) { + return null; + } + + const invDet = 1.0 / det; + // Calculate `comatrix * 1/det`: + const result2 = [ + // First column (m0r): + invDet * det3x3(m11, m12, m13, m21, m22, m23, m32, m32, m33), + -invDet * det3x3(m10, m12, m13, m20, m22, m23, m30, m32, m33), + invDet * det3x3(m10, m11, m13, m20, m21, m23, m30, m31, m33), + -invDet * det3x3(m10, m11, m12, m20, m21, m22, m30, m31, m32), + // Second column (m1r): + -invDet * det3x3(m01, m02, m03, m21, m22, m23, m32, m32, m33), + invDet * det3x3(m00, m02, m03, m20, m22, m23, m30, m32, m33), + -invDet * det3x3(m00, m01, m03, m20, m21, m23, m30, m31, m33), + invDet * det3x3(m00, m01, m02, m20, m21, m22, m30, m31, m32), + // Third column (m2r): + invDet * det3x3(m01, m02, m03, m11, m12, m13, m31, m32, m33), + -invDet * det3x3(m00, m02, m03, m10, m12, m13, m30, m32, m33), + invDet * det3x3(m00, m01, m03, m10, m11, m13, m30, m31, m33), + -invDet * det3x3(m00, m01, m02, m10, m11, m12, m30, m31, m32), + // Fourth column (m3r): + -invDet * det3x3(m01, m02, m03, m11, m12, m13, m21, m22, m23), + invDet * det3x3(m00, m02, m03, m10, m12, m13, m20, m22, m23), + -invDet * det3x3(m00, m01, m03, m10, m11, m13, m20, m21, m23), + invDet * det3x3(m00, m01, m02, m10, m11, m12, m20, m21, m22), + ]; + + // Actual inverse is `1/det * transposed(comatrix)`: + return XRMathHelper.transpose(result2); + } + + static mul4x4(m1, m2) { + if (m1 == null || m2 == null) { + return null; + } + + const result = Array(16); + + for (let row = 0; row < 4; row++) { + for (let col = 0; col < 4; col++) { + result[4 * col + row] = 0; + for(let i = 0; i < 4; i++) { + result[4 * col + row] += m1[4 * i + row] * m2[4 * col + i]; + } + } + } + + return result; + } + + // Decomposes a matrix, with the assumption that the passed in matrix is + // a rigid transformation (i.e. position and rotation *only*!). + // The result is an object with `position` and `orientation` keys, which should + // be compatible with FakeXRRigidTransformInit. + // The implementation should match the behavior of gfx::Transform, but assumes + // that scale, skew & perspective are not present in the matrix so it could be + // simplified. + static decomposeRigidTransform(m) { + const m00 = m[0], m01 = m[1], m02 = m[2], m03 = m[3]; + const m10 = m[4], m11 = m[5], m12 = m[6], m13 = m[7]; + const m20 = m[8], m21 = m[9], m22 = m[10], m23 = m[11]; + const m30 = m[12], m31 = m[13], m32 = m[14], m33 = m[15]; + + const position = [m30, m31, m32]; + const orientation = [0, 0, 0, 0]; + + const trace = m00 + m11 + m22; + if (trace > 0) { + const S = Math.sqrt(trace + 1) * 2; + orientation[3] = 0.25 * S; + orientation[0] = (m12 - m21) / S; + orientation[1] = (m20 - m02) / S; + orientation[2] = (m01 - m10) / S; + } else if (m00 > m11 && m00 > m22) { + const S = Math.sqrt(1.0 + m00 - m11 - m22) * 2; + orientation[3] = (m12 - m21) / S; + orientation[0] = 0.25 * S; + orientation[1] = (m01 + m10) / S; + orientation[2] = (m20 + m02) / S; + } else if (m11 > m22) { + const S = Math.sqrt(1.0 + m11 - m00 - m22) * 2; + orientation[3] = (m20 - m02) / S; + orientation[0] = (m01 + m10) / S; + orientation[1] = 0.25 * S; + orientation[2] = (m12 + m21) / S; + } else { + const S = Math.sqrt(1.0 + m22 - m00 - m11) * 2; + orientation[3] = (m01 - m10) / S; + orientation[0] = (m20 + m02) / S; + orientation[1] = (m12 + m21) / S; + orientation[2] = 0.25 * S; + } + + return { position, orientation }; + } + + static identity() { + return [ + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ]; + }; +} + +XRMathHelper.EPSILON = 0.001; diff --git a/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js.headers b/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js.headers new file mode 100644 index 0000000000..6805c323df --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/webxr-test-math-helper.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/testing/web-platform/tests/resources/chromium/webxr-test.js b/testing/web-platform/tests/resources/chromium/webxr-test.js new file mode 100644 index 0000000000..aba3447982 --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/webxr-test.js @@ -0,0 +1,2130 @@ +import * as vrMojom from '/gen/device/vr/public/mojom/vr_service.mojom.m.js'; +import * as xrSessionMojom from '/gen/device/vr/public/mojom/xr_session.mojom.m.js'; +import {GamepadHand, GamepadMapping} from '/gen/device/gamepad/public/mojom/gamepad.mojom.m.js'; + +// This polyfill library implements the WebXR Test API as specified here: +// https://github.com/immersive-web/webxr-test-api + +const defaultMojoFromFloor = { + matrix: [1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, -1.65, 0, 1] +}; +const default_stage_parameters = { + mojoFromFloor: defaultMojoFromFloor, + bounds: null +}; + +const default_framebuffer_scale = 0.7; + +function getMatrixFromTransform(transform) { + const x = transform.orientation[0]; + const y = transform.orientation[1]; + const z = transform.orientation[2]; + const w = transform.orientation[3]; + + const m11 = 1.0 - 2.0 * (y * y + z * z); + const m21 = 2.0 * (x * y + z * w); + const m31 = 2.0 * (x * z - y * w); + + const m12 = 2.0 * (x * y - z * w); + const m22 = 1.0 - 2.0 * (x * x + z * z); + const m32 = 2.0 * (y * z + x * w); + + const m13 = 2.0 * (x * z + y * w); + const m23 = 2.0 * (y * z - x * w); + const m33 = 1.0 - 2.0 * (x * x + y * y); + + const m14 = transform.position[0]; + const m24 = transform.position[1]; + const m34 = transform.position[2]; + + // Column-major linearized order is expected. + return [m11, m21, m31, 0, + m12, m22, m32, 0, + m13, m23, m33, 0, + m14, m24, m34, 1]; +} + +function getPoseFromTransform(transform) { + const [px, py, pz] = transform.position; + const [ox, oy, oz, ow] = transform.orientation; + return { + position: {x: px, y: py, z: pz}, + orientation: {x: ox, y: oy, z: oz, w: ow}, + }; +} + +function composeGFXTransform(fakeTransformInit) { + return {matrix: getMatrixFromTransform(fakeTransformInit)}; +} + +// Value equality for camera image init objects - they must contain `width` & +// `height` properties and may contain `pixels` property. +function isSameCameraImageInit(rhs, lhs) { + return lhs.width === rhs.width && lhs.height === rhs.height && lhs.pixels === rhs.pixels; +} + +class ChromeXRTest { + constructor() { + this.mockVRService_ = new MockVRService(); + } + + // WebXR Test API + simulateDeviceConnection(init_params) { + return Promise.resolve(this.mockVRService_._addRuntime(init_params)); + } + + disconnectAllDevices() { + this.mockVRService_._removeAllRuntimes(); + return Promise.resolve(); + } + + simulateUserActivation(callback) { + if (window.top !== window) { + // test_driver.click only works for the toplevel frame. This alternate + // Chrome-specific method is sufficient for starting an XR session in an + // iframe, and is used in platform-specific tests. + // + // TODO(https://github.com/web-platform-tests/wpt/issues/20282): use + // a cross-platform method if available. + xr_debug('simulateUserActivation', 'use eventSender'); + document.addEventListener('click', callback); + eventSender.mouseMoveTo(0, 0); + eventSender.mouseDown(); + eventSender.mouseUp(); + document.removeEventListener('click', callback); + return; + } + const button = document.createElement('button'); + button.textContent = 'click to continue test'; + button.style.display = 'block'; + button.style.fontSize = '20px'; + button.style.padding = '10px'; + button.onclick = () => { + callback(); + document.body.removeChild(button); + }; + document.body.appendChild(button); + test_driver.click(button); + } + + // Helper method leveraged by chrome-specific setups. + Debug(name, msg) { + console.log(new Date().toISOString() + ' DEBUG[' + name + '] ' + msg); + } +} + +// Mocking class definitions + +// Mock service implements the VRService mojo interface. +class MockVRService { + constructor() { + this.receiver_ = new vrMojom.VRServiceReceiver(this); + this.runtimes_ = []; + + this.interceptor_ = + new MojoInterfaceInterceptor(vrMojom.VRService.$interfaceName); + this.interceptor_.oninterfacerequest = + e => this.receiver_.$.bindHandle(e.handle); + this.interceptor_.start(); + } + + // WebXR Test API Implementation Helpers + _addRuntime(fakeDeviceInit) { + const runtime = new MockRuntime(fakeDeviceInit, this); + this.runtimes_.push(runtime); + + if (this.client_) { + this.client_.onDeviceChanged(); + } + + return runtime; + } + + _removeAllRuntimes() { + if (this.client_) { + this.client_.onDeviceChanged(); + } + + this.runtimes_ = []; + } + + _removeRuntime(device) { + const index = this.runtimes_.indexOf(device); + if (index >= 0) { + this.runtimes_.splice(index, 1); + if (this.client_) { + this.client_.onDeviceChanged(); + } + } + } + + // VRService overrides + setClient(client) { + if (this.client_) { + throw new Error("setClient should only be called once"); + } + + this.client_ = client; + } + + requestSession(sessionOptions) { + const requests = []; + // Request a session from all the runtimes. + for (let i = 0; i < this.runtimes_.length; i++) { + requests[i] = this.runtimes_[i]._requestRuntimeSession(sessionOptions); + } + + return Promise.all(requests).then((results) => { + // Find and return the first successful result. + for (let i = 0; i < results.length; i++) { + if (results[i].session) { + // Construct a dummy metrics recorder + const metricsRecorderPtr = new vrMojom.XRSessionMetricsRecorderRemote(); + metricsRecorderPtr.$.bindNewPipeAndPassReceiver().handle.close(); + + const success = { + session: results[i].session, + metricsRecorder: metricsRecorderPtr, + }; + + return {result: {success}}; + } + } + + // If there were no successful results, returns a null session. + return { + result: {failureReason: xrSessionMojom.RequestSessionError.NO_RUNTIME_FOUND} + }; + }); + } + + supportsSession(sessionOptions) { + const requests = []; + // Check supports on all the runtimes. + for (let i = 0; i < this.runtimes_.length; i++) { + requests[i] = this.runtimes_[i]._runtimeSupportsSession(sessionOptions); + } + + return Promise.all(requests).then((results) => { + // Find and return the first successful result. + for (let i = 0; i < results.length; i++) { + if (results[i].supportsSession) { + return results[i]; + } + } + + // If there were no successful results, returns false. + return {supportsSession: false}; + }); + } + + exitPresent() { + return Promise.resolve(); + } + + setFramesThrottled(throttled) { + this.setFramesThrottledImpl(throttled); + } + + // We cannot override the mojom interceptors via the prototype; so this method + // and the above indirection exist to allow overrides by internal code. + setFramesThrottledImpl(throttled) {} + + // Only handles asynchronous calls to makeXrCompatible. Synchronous calls are + // not supported in Javascript. + makeXrCompatible() { + if (this.runtimes_.length == 0) { + return { + xrCompatibleResult: vrMojom.XrCompatibleResult.kNoDeviceAvailable + }; + } + return {xrCompatibleResult: vrMojom.XrCompatibleResult.kAlreadyCompatible}; + } +} + +class FakeXRAnchorController { + constructor() { + // Private properties. + this.device_ = null; + this.id_ = null; + this.dirty_ = true; + + // Properties backing up public attributes / methods. + this.deleted_ = false; + this.paused_ = false; + this.anchorOrigin_ = XRMathHelper.identity(); + } + + // WebXR Test API (Anchors Extension) + get deleted() { + return this.deleted_; + } + + pauseTracking() { + if(!this.paused_) { + this.paused_ = true; + this.dirty_ = true; + } + } + + resumeTracking() { + if(this.paused_) { + this.paused_ = false; + this.dirty_ = true; + } + } + + stopTracking() { + if(!this.deleted_) { + this.device_._deleteAnchorController(this.id_); + + this.deleted_ = true; + this.dirty_ = true; + } + } + + setAnchorOrigin(anchorOrigin) { + this.anchorOrigin_ = getMatrixFromTransform(anchorOrigin); + this.dirty_ = true; + } + + // Internal implementation: + set id(value) { + this.id_ = value; + } + + set device(value) { + this.device_ = value; + } + + get dirty() { + return this.dirty_; + } + + get paused() { + return this.paused_; + } + + _markProcessed() { + this.dirty_ = false; + } + + _getAnchorOrigin() { + return this.anchorOrigin_; + } +} + +// Implements XRFrameDataProvider and XRPresentationProvider. Maintains a mock +// for XRPresentationProvider. Implements FakeXRDevice test API. +class MockRuntime { + // Mapping from string feature names to the corresponding mojo types. + // This is exposed as a member for extensibility. + static _featureToMojoMap = { + 'viewer': xrSessionMojom.XRSessionFeature.REF_SPACE_VIEWER, + 'local': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL, + 'local-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_LOCAL_FLOOR, + 'bounded-floor': xrSessionMojom.XRSessionFeature.REF_SPACE_BOUNDED_FLOOR, + 'unbounded': xrSessionMojom.XRSessionFeature.REF_SPACE_UNBOUNDED, + 'hit-test': xrSessionMojom.XRSessionFeature.HIT_TEST, + 'dom-overlay': xrSessionMojom.XRSessionFeature.DOM_OVERLAY, + 'light-estimation': xrSessionMojom.XRSessionFeature.LIGHT_ESTIMATION, + 'anchors': xrSessionMojom.XRSessionFeature.ANCHORS, + 'depth-sensing': xrSessionMojom.XRSessionFeature.DEPTH, + 'secondary-views': xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS, + 'camera-access': xrSessionMojom.XRSessionFeature.CAMERA_ACCESS, + 'layers': xrSessionMojom.XRSessionFeature.LAYERS, + }; + + static _sessionModeToMojoMap = { + "inline": xrSessionMojom.XRSessionMode.kInline, + "immersive-vr": xrSessionMojom.XRSessionMode.kImmersiveVr, + "immersive-ar": xrSessionMojom.XRSessionMode.kImmersiveAr, + }; + + static _environmentBlendModeToMojoMap = { + "opaque": vrMojom.XREnvironmentBlendMode.kOpaque, + "alpha-blend": vrMojom.XREnvironmentBlendMode.kAlphaBlend, + "additive": vrMojom.XREnvironmentBlendMode.kAdditive, + }; + + static _interactionModeToMojoMap = { + "screen-space": vrMojom.XRInteractionMode.kScreenSpace, + "world-space": vrMojom.XRInteractionMode.kWorldSpace, + }; + + constructor(fakeDeviceInit, service) { + this.sessionClient_ = null; + this.presentation_provider_ = new MockXRPresentationProvider(); + + this.pose_ = null; + this.next_frame_id_ = 0; + this.bounds_ = null; + this.send_mojo_space_reset_ = false; + this.stageParameters_ = null; + this.stageParametersId_ = 1; + + this.service_ = service; + + this.framesOfReference = {}; + + this.input_sources_ = new Map(); + this.next_input_source_index_ = 1; + + // Currently active hit test subscriptons. + this.hitTestSubscriptions_ = new Map(); + // Currently active transient hit test subscriptions. + this.transientHitTestSubscriptions_ = new Map(); + // ID of the next subscription to be assigned. + this.next_hit_test_id_ = 1n; + + this.anchor_controllers_ = new Map(); + // ID of the next anchor to be assigned. + this.next_anchor_id_ = 1n; + // Anchor creation callback (initially null, can be set by tests). + this.anchor_creation_callback_ = null; + + this.depthSensingData_ = null; + this.depthSensingDataDirty_ = false; + + let supportedModes = []; + if (fakeDeviceInit.supportedModes) { + supportedModes = fakeDeviceInit.supportedModes.slice(); + if (fakeDeviceInit.supportedModes.length === 0) { + supportedModes = ["inline"]; + } + } else { + // Back-compat mode. + console.warn("Please use `supportedModes` to signal which modes are supported by this device."); + if (fakeDeviceInit.supportsImmersive == null) { + throw new TypeError("'supportsImmersive' must be set"); + } + + supportedModes = ["inline"]; + if (fakeDeviceInit.supportsImmersive) { + supportedModes.push("immersive-vr"); + } + } + + this.supportedModes_ = this._convertModesToEnum(supportedModes); + if (this.supportedModes_.length == 0) { + console.error("Device has empty supported modes array!"); + throw new InvalidStateError(); + } + + if (fakeDeviceInit.viewerOrigin != null) { + this.setViewerOrigin(fakeDeviceInit.viewerOrigin); + } + + if (fakeDeviceInit.floorOrigin != null) { + this.setFloorOrigin(fakeDeviceInit.floorOrigin); + } + + if (fakeDeviceInit.world) { + this.setWorld(fakeDeviceInit.world); + } + + if (fakeDeviceInit.depthSensingData) { + this.setDepthSensingData(fakeDeviceInit.depthSensingData); + } + + this.defaultFramebufferScale_ = default_framebuffer_scale; + this.enviromentBlendMode_ = this._convertBlendModeToEnum(fakeDeviceInit.environmentBlendMode); + this.interactionMode_ = this._convertInteractionModeToEnum(fakeDeviceInit.interactionMode); + + // This appropriately handles if the coordinates are null + this.setBoundsGeometry(fakeDeviceInit.boundsCoordinates); + + this.setViews(fakeDeviceInit.views, fakeDeviceInit.secondaryViews); + + // Need to support webVR which doesn't have a notion of features + this._setFeatures(fakeDeviceInit.supportedFeatures || []); + } + + // WebXR Test API + setViews(primaryViews, secondaryViews) { + this.cameraImage_ = null; + this.primaryViews_ = []; + this.secondaryViews_ = []; + let xOffset = 0; + if (primaryViews) { + this.primaryViews_ = []; + xOffset = this._setViews(primaryViews, xOffset, this.primaryViews_); + const cameraImage = this._findCameraImage(primaryViews); + + if (cameraImage) { + this.cameraImage_ = cameraImage; + } + } + + if (secondaryViews) { + this.secondaryViews_ = []; + this._setViews(secondaryViews, xOffset, this.secondaryViews_); + const cameraImage = this._findCameraImage(secondaryViews); + + if (cameraImage) { + if (!isSameCameraImageInit(this.cameraImage_, cameraImage)) { + throw new Error("If present, camera resolutions on each view must match each other!" + + " Secondary views' camera doesn't match primary views."); + } + + this.cameraImage_ = cameraImage; + } + } + } + + disconnect() { + this.service_._removeRuntime(this); + this.presentation_provider_._close(); + if (this.sessionClient_) { + this.sessionClient_.$.close(); + this.sessionClient_ = null; + } + + return Promise.resolve(); + } + + setViewerOrigin(origin, emulatedPosition = false) { + const p = origin.position; + const q = origin.orientation; + this.pose_ = { + orientation: { x: q[0], y: q[1], z: q[2], w: q[3] }, + position: { x: p[0], y: p[1], z: p[2] }, + emulatedPosition: emulatedPosition, + angularVelocity: null, + linearVelocity: null, + angularAcceleration: null, + linearAcceleration: null, + inputState: null, + poseIndex: 0 + }; + } + + clearViewerOrigin() { + this.pose_ = null; + } + + setFloorOrigin(floorOrigin) { + if (!this.stageParameters_) { + this.stageParameters_ = default_stage_parameters; + this.stageParameters_.bounds = this.bounds_; + } + + // floorOrigin is passed in as mojoFromFloor. + this.stageParameters_.mojoFromFloor = + {matrix: getMatrixFromTransform(floorOrigin)}; + + this._onStageParametersUpdated(); + } + + clearFloorOrigin() { + if (this.stageParameters_) { + this.stageParameters_ = null; + this._onStageParametersUpdated(); + } + } + + setBoundsGeometry(bounds) { + if (bounds == null) { + this.bounds_ = null; + } else if (bounds.length < 3) { + throw new Error("Bounds must have a length of at least 3"); + } else { + this.bounds_ = bounds; + } + + // We can only set bounds if we have stageParameters set; otherwise, we + // don't know the transform from local space to bounds space. + // We'll cache the bounds so that they can be set in the future if the + // floorLevel transform is set, but we won't update them just yet. + if (this.stageParameters_) { + this.stageParameters_.bounds = this.bounds_; + this._onStageParametersUpdated(); + } + } + + simulateResetPose() { + this.send_mojo_space_reset_ = true; + } + + simulateVisibilityChange(visibilityState) { + let mojoState = null; + switch (visibilityState) { + case "visible": + mojoState = vrMojom.XRVisibilityState.VISIBLE; + break; + case "visible-blurred": + mojoState = vrMojom.XRVisibilityState.VISIBLE_BLURRED; + break; + case "hidden": + mojoState = vrMojom.XRVisibilityState.HIDDEN; + break; + } + if (mojoState && this.sessionClient_) { + this.sessionClient_.onVisibilityStateChanged(mojoState); + } + } + + simulateInputSourceConnection(fakeInputSourceInit) { + const index = this.next_input_source_index_; + this.next_input_source_index_++; + + const source = new MockXRInputSource(fakeInputSourceInit, index, this); + this.input_sources_.set(index, source); + return source; + } + + // WebXR Test API Hit Test extensions + setWorld(world) { + this.world_ = world; + } + + clearWorld() { + this.world_ = null; + } + + // WebXR Test API Anchor extensions + setAnchorCreationCallback(callback) { + this.anchor_creation_callback_ = callback; + } + + setHitTestSourceCreationCallback(callback) { + this.hit_test_source_creation_callback_ = callback; + } + + // WebXR Test API Lighting estimation extensions + setLightEstimate(fakeXrLightEstimateInit) { + if (!fakeXrLightEstimateInit.sphericalHarmonicsCoefficients) { + throw new TypeError("sphericalHarmonicsCoefficients must be set"); + } + + if (fakeXrLightEstimateInit.sphericalHarmonicsCoefficients.length != 27) { + throw new TypeError("Must supply all 27 sphericalHarmonicsCoefficients"); + } + + if (fakeXrLightEstimateInit.primaryLightDirection && fakeXrLightEstimateInit.primaryLightDirection.w != 0) { + throw new TypeError("W component of primaryLightDirection must be 0"); + } + + if (fakeXrLightEstimateInit.primaryLightIntensity && fakeXrLightEstimateInit.primaryLightIntensity.w != 1) { + throw new TypeError("W component of primaryLightIntensity must be 1"); + } + + // If the primaryLightDirection or primaryLightIntensity aren't set, we need to set them + // to the defaults that the spec expects. ArCore will either give us everything or nothing, + // so these aren't nullable on the mojom. + if (!fakeXrLightEstimateInit.primaryLightDirection) { + fakeXrLightEstimateInit.primaryLightDirection = { x: 0.0, y: 1.0, z: 0.0, w: 0.0 }; + } + + if (!fakeXrLightEstimateInit.primaryLightIntensity) { + fakeXrLightEstimateInit.primaryLightIntensity = { x: 0.0, y: 0.0, z: 0.0, w: 1.0 }; + } + + let c = fakeXrLightEstimateInit.sphericalHarmonicsCoefficients; + + this.light_estimate_ = { + lightProbe: { + // XRSphereicalHarmonics + sphericalHarmonics: { + coefficients: [ + { red: c[0], green: c[1], blue: c[2] }, + { red: c[3], green: c[4], blue: c[5] }, + { red: c[6], green: c[7], blue: c[8] }, + { red: c[9], green: c[10], blue: c[11] }, + { red: c[12], green: c[13], blue: c[14] }, + { red: c[15], green: c[16], blue: c[17] }, + { red: c[18], green: c[19], blue: c[20] }, + { red: c[21], green: c[22], blue: c[23] }, + { red: c[24], green: c[25], blue: c[26] } + ] + }, + // Vector3dF + mainLightDirection: { + x: fakeXrLightEstimateInit.primaryLightDirection.x, + y: fakeXrLightEstimateInit.primaryLightDirection.y, + z: fakeXrLightEstimateInit.primaryLightDirection.z + }, + // RgbTupleF32 + mainLightIntensity: { + red: fakeXrLightEstimateInit.primaryLightIntensity.x, + green: fakeXrLightEstimateInit.primaryLightIntensity.y, + blue: fakeXrLightEstimateInit.primaryLightIntensity.z + } + } + } + } + + // WebXR Test API depth Sensing Extensions + setDepthSensingData(depthSensingData) { + for(const key of ["depthData", "normDepthBufferFromNormView", "rawValueToMeters", "width", "height"]) { + if(!(key in depthSensingData)) { + throw new TypeError("Required key not present. Key: " + key); + } + } + + if(depthSensingData.depthData != null) { + // Create new object w/ properties based on the depthSensingData, but + // convert the FakeXRRigidTransformInit into a transformation matrix object. + this.depthSensingData_ = Object.assign({}, + depthSensingData, { + normDepthBufferFromNormView: composeGFXTransform(depthSensingData.normDepthBufferFromNormView), + }); + } else { + throw new TypeError("`depthData` is not set"); + } + + this.depthSensingDataDirty_ = true; + } + + clearDepthSensingData() { + this.depthSensingData_ = null; + this.depthSensingDataDirty_ = true; + } + + // Internal Implementation/Helper Methods + _convertModeToEnum(sessionMode) { + if (sessionMode in MockRuntime._sessionModeToMojoMap) { + return MockRuntime._sessionModeToMojoMap[sessionMode]; + } + + throw new TypeError("Unrecognized value for XRSessionMode enum: " + sessionMode); + } + + _convertModesToEnum(sessionModes) { + return sessionModes.map(mode => this._convertModeToEnum(mode)); + } + + _convertBlendModeToEnum(blendMode) { + if (blendMode in MockRuntime._environmentBlendModeToMojoMap) { + return MockRuntime._environmentBlendModeToMojoMap[blendMode]; + } else { + if (this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { + return vrMojom.XREnvironmentBlendMode.kAdditive; + } else if (this.supportedModes_.includes( + xrSessionMojom.XRSessionMode.kImmersiveVr)) { + return vrMojom.XREnvironmentBlendMode.kOpaque; + } + } + } + + _convertInteractionModeToEnum(interactionMode) { + if (interactionMode in MockRuntime._interactionModeToMojoMap) { + return MockRuntime._interactionModeToMojoMap[interactionMode]; + } else { + return vrMojom.XRInteractionMode.kWorldSpace; + } + } + + _setViews(deviceViews, xOffset, views) { + for (let i = 0; i < deviceViews.length; i++) { + views[i] = this._getView(deviceViews[i], xOffset); + xOffset += deviceViews[i].resolution.width; + } + + return xOffset; + } + + _findCameraImage(views) { + const viewWithCamera = views.find(view => view.cameraImageInit); + if (viewWithCamera) { + //If we have one view with a camera resolution, all views should have the same camera resolution. + const allViewsHaveSameCamera = views.every( + view => isSameCameraImageInit(view.cameraImageInit, viewWithCamera.cameraImageInit)); + + if (!allViewsHaveSameCamera) { + throw new Error("If present, camera resolutions on each view must match each other!"); + } + + return viewWithCamera.cameraImageInit; + } + + return null; + } + + _onStageParametersUpdated() { + // Indicate for the frame loop that the stage parameters have been updated. + this.stageParametersId_++; + } + + _getDefaultViews() { + if (this.primaryViews_) { + return this.primaryViews_; + } + + const viewport_size = 20; + return [{ + eye: vrMojom.XREye.kLeft, + fieldOfView: { + upDegrees: 48.316, + downDegrees: 50.099, + leftDegrees: 50.899, + rightDegrees: 35.197 + }, + mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({ + position: [-0.032, 0, 0], + orientation: [0, 0, 0, 1] + })), + viewport: { x: 0, y: 0, width: viewport_size, height: viewport_size } + }, + { + eye: vrMojom.XREye.kRight, + fieldOfView: { + upDegrees: 48.316, + downDegrees: 50.099, + leftDegrees: 50.899, + rightDegrees: 35.197 + }, + mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform({ + position: [0.032, 0, 0], + orientation: [0, 0, 0, 1] + })), + viewport: { x: viewport_size, y: 0, width: viewport_size, height: viewport_size } + }]; + } + + // This function converts between the matrix provided by the WebXR test API + // and the internal data representation. + _getView(fakeXRViewInit, xOffset) { + let fov = null; + + if (fakeXRViewInit.fieldOfView) { + fov = { + upDegrees: fakeXRViewInit.fieldOfView.upDegrees, + downDegrees: fakeXRViewInit.fieldOfView.downDegrees, + leftDegrees: fakeXRViewInit.fieldOfView.leftDegrees, + rightDegrees: fakeXRViewInit.fieldOfView.rightDegrees + }; + } else { + const m = fakeXRViewInit.projectionMatrix; + + function toDegrees(tan) { + return Math.atan(tan) * 180 / Math.PI; + } + + const leftTan = (1 - m[8]) / m[0]; + const rightTan = (1 + m[8]) / m[0]; + const upTan = (1 + m[9]) / m[5]; + const downTan = (1 - m[9]) / m[5]; + + fov = { + upDegrees: toDegrees(upTan), + downDegrees: toDegrees(downTan), + leftDegrees: toDegrees(leftTan), + rightDegrees: toDegrees(rightTan) + }; + } + + let viewEye = vrMojom.XREye.kNone; + // The eye passed in corresponds to the values in the WebXR spec, which are + // the strings "none", "left", and "right". They should be converted to the + // corresponding values of XREye in vr_service.mojom. + switch(fakeXRViewInit.eye) { + case "none": + viewEye = vrMojom.XREye.kNone; + break; + case "left": + viewEye = vrMojom.XREye.kLeft; + break; + case "right": + viewEye = vrMojom.XREye.kRight; + break; + } + + return { + eye: viewEye, + fieldOfView: fov, + mojoFromView: this._getMojoFromViewerWithOffset(composeGFXTransform(fakeXRViewInit.viewOffset)), + viewport: { + x: xOffset, + y: 0, + width: fakeXRViewInit.resolution.width, + height: fakeXRViewInit.resolution.height + }, + isFirstPersonObserver: fakeXRViewInit.isFirstPersonObserver ? true : false, + viewOffset: composeGFXTransform(fakeXRViewInit.viewOffset) + }; + } + + _setFeatures(supportedFeatures) { + function convertFeatureToMojom(feature) { + if (feature in MockRuntime._featureToMojoMap) { + return MockRuntime._featureToMojoMap[feature]; + } else { + return xrSessionMojom.XRSessionFeature.INVALID; + } + } + + this.supportedFeatures_ = []; + + for (let i = 0; i < supportedFeatures.length; i++) { + const feature = convertFeatureToMojom(supportedFeatures[i]); + if (feature !== xrSessionMojom.XRSessionFeature.INVALID) { + this.supportedFeatures_.push(feature); + } + } + } + + // These methods are intended to be used by MockXRInputSource only. + _addInputSource(source) { + if (!this.input_sources_.has(source.source_id_)) { + this.input_sources_.set(source.source_id_, source); + } + } + + _removeInputSource(source) { + this.input_sources_.delete(source.source_id_); + } + + // These methods are intended to be used by FakeXRAnchorController only. + _deleteAnchorController(controllerId) { + this.anchor_controllers_.delete(controllerId); + } + + // Extension point for non-standard modules. + _injectAdditionalFrameData(options, frameData) { + } + + // Mojo function implementations. + + // XRFrameDataProvider implementation. + getFrameData(options) { + return new Promise((resolve) => { + + const populatePose = () => { + const mojo_space_reset = this.send_mojo_space_reset_; + this.send_mojo_space_reset_ = false; + + if (this.pose_) { + this.pose_.poseIndex++; + } + + // Setting the input_state to null tests a slightly different path than + // the browser tests where if the last input source is removed, the device + // code always sends up an empty array, but it's also valid mojom to send + // up a null array. + let input_state = null; + if (this.input_sources_.size > 0) { + input_state = []; + for (const input_source of this.input_sources_.values()) { + input_state.push(input_source._getInputSourceState()); + } + } + + let frame_views = this.primaryViews_; + for (let i = 0; i < this.primaryViews_.length; i++) { + this.primaryViews_[i].mojoFromView = + this._getMojoFromViewerWithOffset(this.primaryViews_[i].viewOffset); + } + if (this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.SECONDARY_VIEWS)) { + for (let i = 0; i < this.secondaryViews_.length; i++) { + this.secondaryViews_[i].mojoFromView = + this._getMojoFromViewerWithOffset(this.secondaryViews_[i].viewOffset); + } + + frame_views = frame_views.concat(this.secondaryViews_); + } + + const frameData = { + mojoFromViewer: this.pose_, + views: frame_views, + mojoSpaceReset: mojo_space_reset, + inputState: input_state, + timeDelta: { + // window.performance.now() is in milliseconds, so convert to microseconds. + microseconds: BigInt(Math.floor(window.performance.now() * 1000)), + }, + frameId: this.next_frame_id_, + bufferHolder: null, + cameraImageSize: this.cameraImage_ ? { + width: this.cameraImage_.width, + height: this.cameraImage_.height + } : null, + renderingTimeRatio: 0, + stageParameters: this.stageParameters_, + stageParametersId: this.stageParametersId_, + lightEstimationData: this.light_estimate_ + }; + + this.next_frame_id_++; + + this._calculateHitTestResults(frameData); + + this._calculateAnchorInformation(frameData); + + this._calculateDepthInformation(frameData); + + this._injectAdditionalFrameData(options, frameData); + + resolve({frameData}); + }; + + if(this.sessionOptions_.mode == xrSessionMojom.XRSessionMode.kInline) { + // Inline sessions should not have a delay introduced since it causes them + // to miss a vsync blink-side and delays propagation of changes that happened + // within a rAFcb by one frame (e.g. setViewerOrigin() calls would take 2 frames + // to propagate). + populatePose(); + } else { + // For immerive sessions, add additional delay to allow for anchor creation + // promises to run. + setTimeout(populatePose, 3); // note: according to MDN, the timeout is not exact + } + }); + } + + getEnvironmentIntegrationProvider(environmentProviderRequest) { + if (this.environmentProviderReceiver_) { + this.environmentProviderReceiver_.$.close(); + } + this.environmentProviderReceiver_ = + new vrMojom.XREnvironmentIntegrationProviderReceiver(this); + this.environmentProviderReceiver_.$.bindHandle( + environmentProviderRequest.handle); + } + + // XREnvironmentIntegrationProvider implementation: + subscribeToHitTest(nativeOriginInformation, entityTypes, ray) { + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { + // Reject outside of AR. + return Promise.resolve({ + result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC, + subscriptionId : 0n + }); + } + + if (!this._nativeOriginKnown(nativeOriginInformation)) { + return Promise.resolve({ + result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC, + subscriptionId : 0n + }); + } + + // Reserve the id for hit test source: + const id = this.next_hit_test_id_++; + const hitTestParameters = { isTransient: false, profileName: null }; + const controller = new FakeXRHitTestSourceController(id); + + + return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller) + .then((succeeded) => { + if(succeeded) { + // Store the subscription information as-is (including controller): + this.hitTestSubscriptions_.set(id, { nativeOriginInformation, entityTypes, ray, controller }); + + return Promise.resolve({ + result : vrMojom.SubscribeToHitTestResult.SUCCESS, + subscriptionId : id + }); + } else { + return Promise.resolve({ + result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC, + subscriptionId : 0n + }); + } + }); + } + + subscribeToHitTestForTransientInput(profileName, entityTypes, ray){ + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { + // Reject outside of AR. + return Promise.resolve({ + result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC, + subscriptionId : 0n + }); + } + + const id = this.next_hit_test_id_++; + const hitTestParameters = { isTransient: true, profileName: profileName }; + const controller = new FakeXRHitTestSourceController(id); + + // Check if we have hit test source creation callback. + // If yes, ask it if the hit test source creation should succeed. + // If no, for back-compat, assume the hit test source creation succeeded. + return this._shouldHitTestSourceCreationSucceed(hitTestParameters, controller) + .then((succeeded) => { + if(succeeded) { + // Store the subscription information as-is (including controller): + this.transientHitTestSubscriptions_.set(id, { profileName, entityTypes, ray, controller }); + + return Promise.resolve({ + result : vrMojom.SubscribeToHitTestResult.SUCCESS, + subscriptionId : id + }); + } else { + return Promise.resolve({ + result : vrMojom.SubscribeToHitTestResult.FAILURE_GENERIC, + subscriptionId : 0n + }); + } + }); + } + + unsubscribeFromHitTest(subscriptionId) { + let controller = null; + if(this.transientHitTestSubscriptions_.has(subscriptionId)){ + controller = this.transientHitTestSubscriptions_.get(subscriptionId).controller; + this.transientHitTestSubscriptions_.delete(subscriptionId); + } else if(this.hitTestSubscriptions_.has(subscriptionId)){ + controller = this.hitTestSubscriptions_.get(subscriptionId).controller; + this.hitTestSubscriptions_.delete(subscriptionId); + } + + if(controller) { + controller.deleted = true; + } + } + + createAnchor(nativeOriginInformation, nativeOriginFromAnchor) { + return new Promise((resolve) => { + if(this.anchor_creation_callback_ == null) { + resolve({ + result : vrMojom.CreateAnchorResult.FAILURE, + anchorId : 0n + }); + + return; + } + + const mojoFromNativeOrigin = this._getMojoFromNativeOrigin(nativeOriginInformation); + if(mojoFromNativeOrigin == null) { + resolve({ + result : vrMojom.CreateAnchorResult.FAILURE, + anchorId : 0n + }); + + return; + } + + const mojoFromAnchor = XRMathHelper.mul4x4(mojoFromNativeOrigin, nativeOriginFromAnchor); + + const anchorCreationParameters = { + requestedAnchorOrigin: mojoFromAnchor, + isAttachedToEntity: false, + }; + + const anchorController = new FakeXRAnchorController(); + + this.anchor_creation_callback_(anchorCreationParameters, anchorController) + .then((result) => { + if(result) { + // If the test allowed the anchor creation, + // store the anchor controller & return success. + + const anchor_id = this.next_anchor_id_; + this.next_anchor_id_++; + + this.anchor_controllers_.set(anchor_id, anchorController); + anchorController.device = this; + anchorController.id = anchor_id; + + resolve({ + result : vrMojom.CreateAnchorResult.SUCCESS, + anchorId : anchor_id + }); + } else { + // The test has rejected anchor creation. + resolve({ + result : vrMojom.CreateAnchorResult.FAILURE, + anchorId : 0n + }); + } + }) + .catch(() => { + // The test threw an error, treat anchor creation as failed. + resolve({ + result : vrMojom.CreateAnchorResult.FAILURE, + anchorId : 0n + }); + }); + }); + } + + createPlaneAnchor(planeFromAnchor, planeId) { + return new Promise((resolve) => { + + // Not supported yet. + + resolve({ + result : vrMojom.CreateAnchorResult.FAILURE, + anchorId : 0n, + }); + }); + } + + detachAnchor(anchorId) {} + + // Utility function + _requestRuntimeSession(sessionOptions) { + return this._runtimeSupportsSession(sessionOptions).then((result) => { + // The JavaScript bindings convert c_style_names to camelCase names. + const options = { + transportMethod: + vrMojom.XRPresentationTransportMethod.SUBMIT_AS_MAILBOX_HOLDER, + waitForTransferNotification: true, + waitForRenderNotification: true, + waitForGpuFence: false, + }; + + let submit_frame_sink; + if (result.supportsSession) { + submit_frame_sink = { + clientReceiver: this.presentation_provider_._getClientReceiver(), + provider: this.presentation_provider_._bindProvider(sessionOptions), + transportOptions: options + }; + + const dataProviderPtr = new vrMojom.XRFrameDataProviderRemote(); + this.dataProviderReceiver_ = + new vrMojom.XRFrameDataProviderReceiver(this); + this.dataProviderReceiver_.$.bindHandle( + dataProviderPtr.$.bindNewPipeAndPassReceiver().handle); + this.sessionOptions_ = sessionOptions; + + this.sessionClient_ = new vrMojom.XRSessionClientRemote(); + const clientReceiver = this.sessionClient_.$.bindNewPipeAndPassReceiver(); + + const enabled_features = []; + for (let i = 0; i < sessionOptions.requiredFeatures.length; i++) { + if (this.supportedFeatures_.indexOf(sessionOptions.requiredFeatures[i]) !== -1) { + enabled_features.push(sessionOptions.requiredFeatures[i]); + } else { + return Promise.resolve({session: null}); + } + } + + for (let i =0; i < sessionOptions.optionalFeatures.length; i++) { + if (this.supportedFeatures_.indexOf(sessionOptions.optionalFeatures[i]) !== -1) { + enabled_features.push(sessionOptions.optionalFeatures[i]); + } + } + + this.enabledFeatures_ = enabled_features; + + return Promise.resolve({ + session: { + submitFrameSink: submit_frame_sink, + dataProvider: dataProviderPtr, + clientReceiver: clientReceiver, + enabledFeatures: enabled_features, + deviceConfig: { + defaultFramebufferScale: this.defaultFramebufferScale_, + supportsViewportScaling: true, + depthConfiguration: enabled_features.includes( + xrSessionMojom.XRSessionFeature.DEPTH) ? + { + depthUsage: xrSessionMojom.XRDepthUsage.kCPUOptimized, + depthDataFormat: + xrSessionMojom.XRDepthDataFormat.kLuminanceAlpha, + } : + null, + views: this._getDefaultViews(), + }, + enviromentBlendMode: this.enviromentBlendMode_, + interactionMode: this.interactionMode_ + } + }); + } else { + return Promise.resolve({session: null}); + } + }); + } + + _runtimeSupportsSession(options) { + let result = this.supportedModes_.includes(options.mode); + + if (options.requiredFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH) + || options.optionalFeatures.includes(xrSessionMojom.XRSessionFeature.DEPTH)) { + result &= options.depthOptions.usagePreferences.includes( + xrSessionMojom.XRDepthUsage.kCPUOptimized); + result &= options.depthOptions.dataFormatPreferences.includes( + xrSessionMojom.XRDepthDataFormat.kLuminanceAlpha); + } + + return Promise.resolve({ + supportsSession: result, + }); + } + + // Private functions - utilities: + _nativeOriginKnown(nativeOriginInformation){ + + if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) { + if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) { + // Unknown input source. + return false; + } + + return true; + } else if (nativeOriginInformation.referenceSpaceType !== undefined) { + // Bounded_floor & unbounded ref spaces are not yet supported for AR: + if (nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kUnbounded + || nativeOriginInformation.referenceSpaceType == vrMojom.XRReferenceSpaceType.kBoundedFloor) { + return false; + } + + return true; + } else { + // Planes and anchors are not yet supported by the mock interface. + return false; + } + } + + // Private functions - anchors implementation: + + // Modifies passed in frameData to add anchor information. + _calculateAnchorInformation(frameData) { + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { + return; + } + + frameData.anchorsData = {allAnchorsIds: [], updatedAnchorsData: []}; + for(const [id, controller] of this.anchor_controllers_) { + frameData.anchorsData.allAnchorsIds.push(id); + + // Send the entire anchor data over if there was a change since last GetFrameData(). + if(controller.dirty) { + const anchorData = {id}; + if(!controller.paused) { + anchorData.mojoFromAnchor = getPoseFromTransform( + XRMathHelper.decomposeRigidTransform( + controller._getAnchorOrigin())); + } + + controller._markProcessed(); + + frameData.anchorsData.updatedAnchorsData.push(anchorData); + } + } + } + + // Private functions - depth sensing implementation: + + // Modifies passed in frameData to add anchor information. + _calculateDepthInformation(frameData) { + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { + return; + } + + if (!this.enabledFeatures_.includes(xrSessionMojom.XRSessionFeature.DEPTH)) { + return; + } + + // If we don't have a current depth data, we'll return null + // (i.e. no data is not a valid data, so it cannot be "StillValid"). + if (this.depthSensingData_ == null) { + frameData.depthData = null; + return; + } + + if(!this.depthSensingDataDirty_) { + frameData.depthData = { dataStillValid: {}}; + return; + } + + frameData.depthData = { + updatedDepthData: { + timeDelta: frameData.timeDelta, + normTextureFromNormView: this.depthSensingData_.normDepthBufferFromNormView, + rawValueToMeters: this.depthSensingData_.rawValueToMeters, + size: { width: this.depthSensingData_.width, height: this.depthSensingData_.height }, + pixelData: { bytes: this.depthSensingData_.depthData } + } + }; + + this.depthSensingDataDirty_ = false; + } + + // Private functions - hit test implementation: + + // Returns a Promise<bool> that signifies whether hit test source creation should succeed. + // If we have a hit test source creation callback installed, invoke it and return its result. + // If it's not installed, for back-compat just return a promise that resolves to true. + _shouldHitTestSourceCreationSucceed(hitTestParameters, controller) { + if(this.hit_test_source_creation_callback_) { + return this.hit_test_source_creation_callback_(hitTestParameters, controller); + } else { + return Promise.resolve(true); + } + } + + // Modifies passed in frameData to add hit test results. + _calculateHitTestResults(frameData) { + if (!this.supportedModes_.includes(xrSessionMojom.XRSessionMode.kImmersiveAr)) { + return; + } + + frameData.hitTestSubscriptionResults = {results: [], + transientInputResults: []}; + if (!this.world_) { + return; + } + + // Non-transient hit test: + for (const [id, subscription] of this.hitTestSubscriptions_) { + const mojo_from_native_origin = this._getMojoFromNativeOrigin(subscription.nativeOriginInformation); + if (!mojo_from_native_origin) continue; + + const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace( + subscription.ray, + mojo_from_native_origin + ); + + const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes); + frameData.hitTestSubscriptionResults.results.push( + {subscriptionId: id, hitTestResults: results}); + } + + // Transient hit test: + const mojo_from_viewer = this._getMojoFromViewer(); + + for (const [id, subscription] of this.transientHitTestSubscriptions_) { + const result = {subscriptionId: id, + inputSourceIdToHitTestResults: new Map()}; + + // Find all input sources that match the profile name: + const matching_input_sources = Array.from(this.input_sources_.values()) + .filter(input_source => input_source.profiles_.includes(subscription.profileName)); + + for (const input_source of matching_input_sources) { + const mojo_from_native_origin = input_source._getMojoFromInputSource(mojo_from_viewer); + + const [mojo_ray_origin, mojo_ray_direction] = this._transformRayToMojoSpace( + subscription.ray, + mojo_from_native_origin + ); + + const results = this._hitTestWorld(mojo_ray_origin, mojo_ray_direction, subscription.entityTypes); + + result.inputSourceIdToHitTestResults.set(input_source.source_id_, results); + } + + frameData.hitTestSubscriptionResults.transientInputResults.push(result); + } + } + + // Returns 2-element array [origin, direction] of a ray in mojo space. + // |ray| is expressed relative to native origin. + _transformRayToMojoSpace(ray, mojo_from_native_origin) { + const ray_origin = { + x: ray.origin.x, + y: ray.origin.y, + z: ray.origin.z, + w: 1 + }; + const ray_direction = { + x: ray.direction.x, + y: ray.direction.y, + z: ray.direction.z, + w: 0 + }; + + const mojo_ray_origin = XRMathHelper.transform_by_matrix( + mojo_from_native_origin, + ray_origin); + const mojo_ray_direction = XRMathHelper.transform_by_matrix( + mojo_from_native_origin, + ray_direction); + + return [mojo_ray_origin, mojo_ray_direction]; + } + + // Hit tests the passed in ray (expressed as origin and direction) against the mocked world data. + _hitTestWorld(origin, direction, entityTypes) { + let result = []; + + for (const region of this.world_.hitTestRegions) { + const partial_result = this._hitTestRegion( + region, + origin, direction, + entityTypes); + + result = result.concat(partial_result); + } + + return result.sort((lhs, rhs) => lhs.distance - rhs.distance).map((hitTest) => { + delete hitTest.distance; + return hitTest; + }); + } + + // Hit tests the passed in ray (expressed as origin and direction) against world region. + // |entityTypes| is a set of FakeXRRegionTypes. + // |region| is FakeXRRegion. + // Returns array of XRHitResults, each entry will be decorated with the distance from the ray origin (along the ray). + _hitTestRegion(region, origin, direction, entityTypes) { + const regionNameToMojoEnum = { + "point": vrMojom.EntityTypeForHitTest.POINT, + "plane": vrMojom.EntityTypeForHitTest.PLANE, + "mesh":null + }; + + if (!entityTypes.includes(regionNameToMojoEnum[region.type])) { + return []; + } + + const result = []; + for (const face of region.faces) { + const maybe_hit = this._hitTestFace(face, origin, direction); + if (maybe_hit) { + result.push(maybe_hit); + } + } + + // The results should be sorted by distance and there should be no 2 entries with + // the same distance from ray origin - that would mean they are the same point. + // This situation is possible when a ray intersects the region through an edge shared + // by 2 faces. + return result.sort((lhs, rhs) => lhs.distance - rhs.distance) + .filter((val, index, array) => index === 0 || val.distance !== array[index - 1].distance); + } + + // Hit tests the passed in ray (expressed as origin and direction) against a single face. + // |face|, |origin|, and |direction| are specified in world (aka mojo) coordinates. + // |face| is an array of DOMPointInits. + // Returns null if the face does not intersect with the ray, otherwise the result is + // an XRHitResult with matrix describing the pose of the intersection point. + _hitTestFace(face, origin, direction) { + const add = XRMathHelper.add; + const sub = XRMathHelper.sub; + const mul = XRMathHelper.mul; + const normalize = XRMathHelper.normalize; + const dot = XRMathHelper.dot; + const cross = XRMathHelper.cross; + const neg = XRMathHelper.neg; + + //1. Calculate plane normal in world coordinates. + const point_A = face.vertices[0]; + const point_B = face.vertices[1]; + const point_C = face.vertices[2]; + + const edge_AB = sub(point_B, point_A); + const edge_AC = sub(point_C, point_A); + + const normal = normalize(cross(edge_AB, edge_AC)); + + const numerator = dot(sub(point_A, origin), normal); + const denominator = dot(direction, normal); + + if (Math.abs(denominator) < XRMathHelper.EPSILON) { + // Planes are nearly parallel - there's either infinitely many intersection points or 0. + // Both cases signify a "no hit" for us. + return null; + } else { + // Single intersection point between the infinite plane and the line (*not* ray). + // Need to calculate the hit test matrix taking into account the face vertices. + const distance = numerator / denominator; + if (distance < 0) { + // Line - plane intersection exists, but not the half-line - plane does not. + return null; + } else { + const intersection_point = add(origin, mul(distance, direction)); + // Since we are treating the face as a solid, flip the normal so that its + // half-space will contain the ray origin. + const y_axis = denominator > 0 ? neg(normal) : normal; + + let z_axis = null; + const cos_direction_and_y_axis = dot(direction, y_axis); + if (Math.abs(cos_direction_and_y_axis) > (1 - XRMathHelper.EPSILON)) { + // Ray and the hit test normal are co-linear - try using the 'up' or 'right' vector's projection on the face plane as the Z axis. + // Note: this edge case is currently not covered by the spec. + const up = {x: 0.0, y: 1.0, z: 0.0, w: 0.0}; + const right = {x: 1.0, y: 0.0, z: 0.0, w: 0.0}; + + z_axis = Math.abs(dot(up, y_axis)) > (1 - XRMathHelper.EPSILON) + ? sub(up, mul(dot(right, y_axis), y_axis)) // `up is also co-linear with hit test normal, use `right` + : sub(up, mul(dot(up, y_axis), y_axis)); // `up` is not co-linear with hit test normal, use it + } else { + // Project the ray direction onto the plane, negate it and use as a Z axis. + z_axis = neg(sub(direction, mul(cos_direction_and_y_axis, y_axis))); // Z should point towards the ray origin, not away. + } + + z_axis = normalize(z_axis); + const x_axis = normalize(cross(y_axis, z_axis)); + + // Filter out the points not in polygon. + if (!XRMathHelper.pointInFace(intersection_point, face)) { + return null; + } + + const hitResult = {planeId: 0n}; + hitResult.distance = distance; // Extend the object with additional information used by higher layers. + // It will not be serialized over mojom. + + const matrix = new Array(16); + + matrix[0] = x_axis.x; + matrix[1] = x_axis.y; + matrix[2] = x_axis.z; + matrix[3] = 0; + + matrix[4] = y_axis.x; + matrix[5] = y_axis.y; + matrix[6] = y_axis.z; + matrix[7] = 0; + + matrix[8] = z_axis.x; + matrix[9] = z_axis.y; + matrix[10] = z_axis.z; + matrix[11] = 0; + + matrix[12] = intersection_point.x; + matrix[13] = intersection_point.y; + matrix[14] = intersection_point.z; + matrix[15] = 1; + + hitResult.mojoFromResult = getPoseFromTransform( + XRMathHelper.decomposeRigidTransform(matrix)); + return hitResult; + } + } + } + + _getMojoFromViewer() { + if (!this.pose_) { + return XRMathHelper.identity(); + } + const transform = { + position: [ + this.pose_.position.x, + this.pose_.position.y, + this.pose_.position.z], + orientation: [ + this.pose_.orientation.x, + this.pose_.orientation.y, + this.pose_.orientation.z, + this.pose_.orientation.w], + }; + + return getMatrixFromTransform(transform); + } + + _getMojoFromViewerWithOffset(viewOffset) { + return { matrix: XRMathHelper.mul4x4(this._getMojoFromViewer(), viewOffset.matrix) }; + } + + _getMojoFromNativeOrigin(nativeOriginInformation) { + const mojo_from_viewer = this._getMojoFromViewer(); + + if (nativeOriginInformation.inputSourceSpaceInfo !== undefined) { + if (!this.input_sources_.has(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId)) { + return null; + } else { + const inputSource = this.input_sources_.get(nativeOriginInformation.inputSourceSpaceInfo.inputSourceId); + return inputSource._getMojoFromInputSource(mojo_from_viewer); + } + } else if (nativeOriginInformation.referenceSpaceType !== undefined) { + switch (nativeOriginInformation.referenceSpaceType) { + case vrMojom.XRReferenceSpaceType.kLocal: + return XRMathHelper.identity(); + case vrMojom.XRReferenceSpaceType.kLocalFloor: + if (this.stageParameters_ == null || this.stageParameters_.mojoFromFloor == null) { + console.warn("Standing transform not available."); + return null; + } + return this.stageParameters_.mojoFromFloor.matrix; + case vrMojom.XRReferenceSpaceType.kViewer: + return mojo_from_viewer; + case vrMojom.XRReferenceSpaceType.kBoundedFloor: + return null; + case vrMojom.XRReferenceSpaceType.kUnbounded: + return null; + default: + throw new TypeError("Unrecognized XRReferenceSpaceType!"); + } + } else { + // Anchors & planes are not yet supported for hit test. + return null; + } + } +} + +class MockXRInputSource { + constructor(fakeInputSourceInit, id, pairedDevice) { + this.source_id_ = id; + this.pairedDevice_ = pairedDevice; + this.handedness_ = fakeInputSourceInit.handedness; + this.target_ray_mode_ = fakeInputSourceInit.targetRayMode; + + if (fakeInputSourceInit.pointerOrigin == null) { + throw new TypeError("FakeXRInputSourceInit.pointerOrigin is required."); + } + + this.setPointerOrigin(fakeInputSourceInit.pointerOrigin); + this.setProfiles(fakeInputSourceInit.profiles); + + this.primary_input_pressed_ = false; + if (fakeInputSourceInit.selectionStarted != null) { + this.primary_input_pressed_ = fakeInputSourceInit.selectionStarted; + } + + this.primary_input_clicked_ = false; + if (fakeInputSourceInit.selectionClicked != null) { + this.primary_input_clicked_ = fakeInputSourceInit.selectionClicked; + } + + this.primary_squeeze_pressed_ = false; + this.primary_squeeze_clicked_ = false; + + this.mojo_from_input_ = null; + if (fakeInputSourceInit.gripOrigin != null) { + this.setGripOrigin(fakeInputSourceInit.gripOrigin); + } + + // This properly handles if supportedButtons were not specified. + this.setSupportedButtons(fakeInputSourceInit.supportedButtons); + + this.emulated_position_ = false; + this.desc_dirty_ = true; + } + + // WebXR Test API + setHandedness(handedness) { + if (this.handedness_ != handedness) { + this.desc_dirty_ = true; + this.handedness_ = handedness; + } + } + + setTargetRayMode(targetRayMode) { + if (this.target_ray_mode_ != targetRayMode) { + this.desc_dirty_ = true; + this.target_ray_mode_ = targetRayMode; + } + } + + setProfiles(profiles) { + this.desc_dirty_ = true; + this.profiles_ = profiles; + } + + setGripOrigin(transform, emulatedPosition = false) { + // grip_origin was renamed to mojo_from_input in mojo + this.mojo_from_input_ = composeGFXTransform(transform); + this.emulated_position_ = emulatedPosition; + + // Technically, setting the grip shouldn't make the description dirty, but + // the webxr-test-api sets our pointer as mojoFromPointer; however, we only + // support it across mojom as inputFromPointer, so we need to recalculate it + // whenever the grip moves. + this.desc_dirty_ = true; + } + + clearGripOrigin() { + // grip_origin was renamed to mojo_from_input in mojo + if (this.mojo_from_input_ != null) { + this.mojo_from_input_ = null; + this.emulated_position_ = false; + this.desc_dirty_ = true; + } + } + + setPointerOrigin(transform, emulatedPosition = false) { + // pointer_origin is mojo_from_pointer. + this.desc_dirty_ = true; + this.mojo_from_pointer_ = composeGFXTransform(transform); + this.emulated_position_ = emulatedPosition; + } + + disconnect() { + this.pairedDevice_._removeInputSource(this); + } + + reconnect() { + this.pairedDevice_._addInputSource(this); + } + + startSelection() { + this.primary_input_pressed_ = true; + if (this.gamepad_) { + this.gamepad_.buttons[0].pressed = true; + this.gamepad_.buttons[0].touched = true; + } + } + + endSelection() { + if (!this.primary_input_pressed_) { + throw new Error("Attempted to end selection which was not started"); + } + + this.primary_input_pressed_ = false; + this.primary_input_clicked_ = true; + + if (this.gamepad_) { + this.gamepad_.buttons[0].pressed = false; + this.gamepad_.buttons[0].touched = false; + } + } + + simulateSelect() { + this.primary_input_clicked_ = true; + } + + setSupportedButtons(supportedButtons) { + this.gamepad_ = null; + this.supported_buttons_ = []; + + // If there are no supported buttons, we can stop now. + if (supportedButtons == null || supportedButtons.length < 1) { + return; + } + + const supported_button_map = {}; + this.gamepad_ = this._getEmptyGamepad(); + for (let i = 0; i < supportedButtons.length; i++) { + const buttonType = supportedButtons[i].buttonType; + this.supported_buttons_.push(buttonType); + supported_button_map[buttonType] = supportedButtons[i]; + } + + // Let's start by building the button state in order of priority: + // Primary button is index 0. + this.gamepad_.buttons.push({ + pressed: this.primary_input_pressed_, + touched: this.primary_input_pressed_, + value: this.primary_input_pressed_ ? 1.0 : 0.0 + }); + + // Now add the rest of our buttons + this._addGamepadButton(supported_button_map['grip']); + this._addGamepadButton(supported_button_map['touchpad']); + this._addGamepadButton(supported_button_map['thumbstick']); + this._addGamepadButton(supported_button_map['optional-button']); + this._addGamepadButton(supported_button_map['optional-thumbstick']); + + // Finally, back-fill placeholder buttons/axes + for (let i = 0; i < this.gamepad_.buttons.length; i++) { + if (this.gamepad_.buttons[i] == null) { + this.gamepad_.buttons[i] = { + pressed: false, + touched: false, + value: 0 + }; + } + } + + for (let i=0; i < this.gamepad_.axes.length; i++) { + if (this.gamepad_.axes[i] == null) { + this.gamepad_.axes[i] = 0; + } + } + } + + updateButtonState(buttonState) { + if (this.supported_buttons_.indexOf(buttonState.buttonType) == -1) { + throw new Error("Tried to update state on an unsupported button"); + } + + const buttonIndex = this._getButtonIndex(buttonState.buttonType); + const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType); + + if (buttonIndex == -1) { + throw new Error("Unknown Button Type!"); + } + + // is this a 'squeeze' button? + if (buttonIndex === this._getButtonIndex('grip')) { + // squeeze + if (buttonState.pressed) { + this.primary_squeeze_pressed_ = true; + } else if (this.gamepad_.buttons[buttonIndex].pressed) { + this.primary_squeeze_clicked_ = true; + this.primary_squeeze_pressed_ = false; + } else { + this.primary_squeeze_clicked_ = false; + this.primary_squeeze_pressed_ = false; + } + } + + this.gamepad_.buttons[buttonIndex].pressed = buttonState.pressed; + this.gamepad_.buttons[buttonIndex].touched = buttonState.touched; + this.gamepad_.buttons[buttonIndex].value = buttonState.pressedValue; + + if (axesStartIndex != -1) { + this.gamepad_.axes[axesStartIndex] = buttonState.xValue == null ? 0.0 : buttonState.xValue; + this.gamepad_.axes[axesStartIndex + 1] = buttonState.yValue == null ? 0.0 : buttonState.yValue; + } + } + + // DOM Overlay Extensions + setOverlayPointerPosition(x, y) { + this.overlay_pointer_position_ = {x: x, y: y}; + } + + // Helpers for Mojom + _getInputSourceState() { + const input_state = {}; + + input_state.sourceId = this.source_id_; + input_state.isAuxiliary = false; + + input_state.primaryInputPressed = this.primary_input_pressed_; + input_state.primaryInputClicked = this.primary_input_clicked_; + + input_state.primarySqueezePressed = this.primary_squeeze_pressed_; + input_state.primarySqueezeClicked = this.primary_squeeze_clicked_; + // Setting the input source's "clicked" state should generate one "select" + // event. Reset the input value to prevent it from continuously generating + // events. + this.primary_input_clicked_ = false; + // Setting the input source's "clicked" state should generate one "squeeze" + // event. Reset the input value to prevent it from continuously generating + // events. + this.primary_squeeze_clicked_ = false; + + input_state.mojoFromInput = this.mojo_from_input_; + + input_state.gamepad = this.gamepad_; + + input_state.emulatedPosition = this.emulated_position_; + + if (this.desc_dirty_) { + const input_desc = {}; + + switch (this.target_ray_mode_) { + case 'gaze': + input_desc.targetRayMode = vrMojom.XRTargetRayMode.GAZING; + break; + case 'tracked-pointer': + input_desc.targetRayMode = vrMojom.XRTargetRayMode.POINTING; + break; + case 'screen': + input_desc.targetRayMode = vrMojom.XRTargetRayMode.TAPPING; + break; + default: + throw new Error('Unhandled target ray mode ' + this.target_ray_mode_); + } + + switch (this.handedness_) { + case 'left': + input_desc.handedness = vrMojom.XRHandedness.LEFT; + break; + case 'right': + input_desc.handedness = vrMojom.XRHandedness.RIGHT; + break; + default: + input_desc.handedness = vrMojom.XRHandedness.NONE; + break; + } + + // Mojo requires us to send the pointerOrigin as relative to the grip + // space. If we don't have a grip space, we'll just assume that there + // is a grip at identity. This allows tests to simulate controllers that + // are really just a pointer with no tracked grip, though we will end up + // exposing that grip space. + let mojo_from_input = XRMathHelper.identity(); + switch (this.target_ray_mode_) { + case 'gaze': + case 'screen': + // For gaze and screen space, we won't have a mojo_from_input; however + // the "input" position is just the viewer, so use mojo_from_viewer. + mojo_from_input = this.pairedDevice_._getMojoFromViewer(); + break; + case 'tracked-pointer': + // If we have a tracked grip position (e.g. mojo_from_input), then use + // that. If we don't, then we'll just set the pointer offset directly, + // using identity as set above. + if (this.mojo_from_input_) { + mojo_from_input = this.mojo_from_input_.matrix; + } + break; + default: + throw new Error('Unhandled target ray mode ' + this.target_ray_mode_); + } + + // To convert mojo_from_pointer to input_from_pointer, we need: + // input_from_pointer = input_from_mojo * mojo_from_pointer + // Since we store mojo_from_input, we need to invert it here before + // multiplying. + let input_from_mojo = XRMathHelper.inverse(mojo_from_input); + input_desc.inputFromPointer = {}; + input_desc.inputFromPointer.matrix = + XRMathHelper.mul4x4(input_from_mojo, this.mojo_from_pointer_.matrix); + + input_desc.profiles = this.profiles_; + + input_state.description = input_desc; + + this.desc_dirty_ = false; + } + + // Pointer data for DOM Overlay, set by setOverlayPointerPosition() + if (this.overlay_pointer_position_) { + input_state.overlayPointerPosition = this.overlay_pointer_position_; + this.overlay_pointer_position_ = null; + } + + return input_state; + } + + _getEmptyGamepad() { + // Mojo complains if some of the properties on Gamepad are null, so set + // everything to reasonable defaults that tests can override. + const gamepad = { + connected: true, + id: [], + timestamp: 0n, + axes: [], + buttons: [], + touchEvents: [], + mapping: GamepadMapping.GamepadMappingStandard, + displayId: 0, + }; + + switch (this.handedness_) { + case 'left': + gamepad.hand = GamepadHand.GamepadHandLeft; + break; + case 'right': + gamepad.hand = GamepadHand.GamepadHandRight; + break; + default: + gamepad.hand = GamepadHand.GamepadHandNone; + break; + } + + return gamepad; + } + + _addGamepadButton(buttonState) { + if (buttonState == null) { + return; + } + + const buttonIndex = this._getButtonIndex(buttonState.buttonType); + const axesStartIndex = this._getAxesStartIndex(buttonState.buttonType); + + if (buttonIndex == -1) { + throw new Error("Unknown Button Type!"); + } + + this.gamepad_.buttons[buttonIndex] = { + pressed: buttonState.pressed, + touched: buttonState.touched, + value: buttonState.pressedValue + }; + + // Add x/y value if supported. + if (axesStartIndex != -1) { + this.gamepad_.axes[axesStartIndex] = (buttonState.xValue == null ? 0.0 : buttonSate.xValue); + this.gamepad_.axes[axesStartIndex + 1] = (buttonState.yValue == null ? 0.0 : buttonSate.yValue); + } + } + + // General Helper methods + _getButtonIndex(buttonType) { + switch (buttonType) { + case 'grip': + return 1; + case 'touchpad': + return 2; + case 'thumbstick': + return 3; + case 'optional-button': + return 4; + case 'optional-thumbstick': + return 5; + default: + return -1; + } + } + + _getAxesStartIndex(buttonType) { + switch (buttonType) { + case 'touchpad': + return 0; + case 'thumbstick': + return 2; + case 'optional-thumbstick': + return 4; + default: + return -1; + } + } + + _getMojoFromInputSource(mojo_from_viewer) { + return this.mojo_from_pointer_.matrix; + } +} + +// Mojo helper classes +class FakeXRHitTestSourceController { + constructor(id) { + this.id_ = id; + this.deleted_ = false; + } + + get deleted() { + return this.deleted_; + } + + // Internal setter: + set deleted(value) { + this.deleted_ = value; + } +} + +class MockXRPresentationProvider { + constructor() { + this.receiver_ = null; + this.submit_frame_count_ = 0; + this.missing_frame_count_ = 0; + } + + _bindProvider() { + const provider = new vrMojom.XRPresentationProviderRemote(); + + if (this.receiver_) { + this.receiver_.$.close(); + } + this.receiver_ = new vrMojom.XRPresentationProviderReceiver(this); + this.receiver_.$.bindHandle(provider.$.bindNewPipeAndPassReceiver().handle); + return provider; + } + + _getClientReceiver() { + this.submitFrameClient_ = new vrMojom.XRPresentationClientRemote(); + return this.submitFrameClient_.$.bindNewPipeAndPassReceiver(); + } + + // XRPresentationProvider mojo implementation + updateLayerBounds(frameId, leftBounds, rightBounds, sourceSize) {} + + submitFrameMissing(frameId, mailboxHolder, timeWaited) { + this.missing_frame_count_++; + } + + submitFrame(frameId, mailboxHolder, timeWaited) { + this.submit_frame_count_++; + + // Trigger the submit completion callbacks here. WARNING: The + // Javascript-based mojo mocks are *not* re-entrant. It's OK to + // wait for these notifications on the next frame, but waiting + // within the current frame would never finish since the incoming + // calls would be queued until the current execution context finishes. + this.submitFrameClient_.onSubmitFrameTransferred(true); + this.submitFrameClient_.onSubmitFrameRendered(); + } + + submitFrameWithTextureHandle(frameId, texture, syncToken) {} + + submitFrameDrawnIntoTexture(frameId, syncToken, timeWaited) {} + + // Utility methods + _close() { + if (this.receiver_) { + this.receiver_.$.close(); + } + } +} + +// Export these into the global object as a side effect of importing this +// module. +self.ChromeXRTest = ChromeXRTest; +self.MockRuntime = MockRuntime; +self.MockVRService = MockVRService; +self.SubscribeToHitTestResult = vrMojom.SubscribeToHitTestResult; + +navigator.xr.test = new ChromeXRTest(); diff --git a/testing/web-platform/tests/resources/chromium/webxr-test.js.headers b/testing/web-platform/tests/resources/chromium/webxr-test.js.headers new file mode 100644 index 0000000000..6805c323df --- /dev/null +++ b/testing/web-platform/tests/resources/chromium/webxr-test.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 diff --git a/testing/web-platform/tests/resources/idlharness-shadowrealm.js b/testing/web-platform/tests/resources/idlharness-shadowrealm.js new file mode 100644 index 0000000000..9484ca6f51 --- /dev/null +++ b/testing/web-platform/tests/resources/idlharness-shadowrealm.js @@ -0,0 +1,61 @@ +// TODO: it would be nice to support `idl_array.add_objects` +function fetch_text(url) { + return fetch(url).then(function (r) { + if (!r.ok) { + throw new Error("Error fetching " + url + "."); + } + return r.text(); + }); +} + +/** + * idl_test_shadowrealm is a promise_test wrapper that handles the fetching of the IDL, and + * running the code in a `ShadowRealm`, avoiding repetitive boilerplate. + * + * @see https://github.com/tc39/proposal-shadowrealm + * @param {String[]} srcs Spec name(s) for source idl files (fetched from + * /interfaces/{name}.idl). + * @param {String[]} deps Spec name(s) for dependency idl files (fetched + * from /interfaces/{name}.idl). Order is important - dependencies from + * each source will only be included if they're already know to be a + * dependency (i.e. have already been seen). + */ +function idl_test_shadowrealm(srcs, deps) { + promise_setup(async t => { + const realm = new ShadowRealm(); + // https://github.com/web-platform-tests/wpt/issues/31996 + realm.evaluate("globalThis.self = globalThis; undefined;"); + + realm.evaluate(` + globalThis.self.GLOBAL = { + isWindow: function() { return false; }, + isWorker: function() { return false; }, + isShadowRealm: function() { return true; }, + }; undefined; + `); + const specs = await Promise.all(srcs.concat(deps).map(spec => { + return fetch_text("/interfaces/" + spec + ".idl"); + })); + const idls = JSON.stringify(specs); + await new Promise( + realm.evaluate(`(resolve,reject) => { + (async () => { + await import("/resources/testharness.js"); + await import("/resources/WebIDLParser.js"); + await import("/resources/idlharness.js"); + const idls = ${idls}; + const idl_array = new IdlArray(); + for (let i = 0; i < ${srcs.length}; i++) { + idl_array.add_idls(idls[i]); + } + for (let i = ${srcs.length}; i < ${srcs.length + deps.length}; i++) { + idl_array.add_dependency_idls(idls[i]); + } + idl_array.test(); + })().then(resolve, (e) => reject(e.toString())); + }`) + ); + await fetch_tests_from_shadow_realm(realm); + }); +} +// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker: diff --git a/testing/web-platform/tests/resources/idlharness.js b/testing/web-platform/tests/resources/idlharness.js new file mode 100644 index 0000000000..8f741b09b2 --- /dev/null +++ b/testing/web-platform/tests/resources/idlharness.js @@ -0,0 +1,3554 @@ +/* For user documentation see docs/_writing-tests/idlharness.md */ + +/** + * Notes for people who want to edit this file (not just use it as a library): + * + * Most of the interesting stuff happens in the derived classes of IdlObject, + * especially IdlInterface. The entry point for all IdlObjects is .test(), + * which is called by IdlArray.test(). An IdlObject is conceptually just + * "thing we want to run tests on", and an IdlArray is an array of IdlObjects + * with some additional data thrown in. + * + * The object model is based on what WebIDLParser.js produces, which is in turn + * based on its pegjs grammar. If you want to figure out what properties an + * object will have from WebIDLParser.js, the best way is to look at the + * grammar: + * + * https://github.com/darobin/webidl.js/blob/master/lib/grammar.peg + * + * So for instance: + * + * // interface definition + * interface + * = extAttrs:extendedAttributeList? S? "interface" S name:identifier w herit:ifInheritance? w "{" w mem:ifMember* w "}" w ";" w + * { return { type: "interface", name: name, inheritance: herit, members: mem, extAttrs: extAttrs }; } + * + * This means that an "interface" object will have a .type property equal to + * the string "interface", a .name property equal to the identifier that the + * parser found, an .inheritance property equal to either null or the result of + * the "ifInheritance" production found elsewhere in the grammar, and so on. + * After each grammatical production is a JavaScript function in curly braces + * that gets called with suitable arguments and returns some JavaScript value. + * + * (Note that the version of WebIDLParser.js we use might sometimes be + * out-of-date or forked.) + * + * The members and methods of the classes defined by this file are all at least + * briefly documented, hopefully. + */ +(function(){ +"use strict"; +// Support subsetTestByKey from /common/subset-tests-by-key.js, but make it optional +if (!('subsetTestByKey' in self)) { + self.subsetTestByKey = function(key, callback, ...args) { + return callback(...args); + } + self.shouldRunSubTest = () => true; +} +/// Helpers /// +function constValue (cnt) +{ + if (cnt.type === "null") return null; + if (cnt.type === "NaN") return NaN; + if (cnt.type === "Infinity") return cnt.negative ? -Infinity : Infinity; + if (cnt.type === "number") return +cnt.value; + return cnt.value; +} + +function minOverloadLength(overloads) +{ + // "The value of the Function object’s “length” property is + // a Number determined as follows: + // ". . . + // "Return the length of the shortest argument list of the + // entries in S." + if (!overloads.length) { + return 0; + } + + return overloads.map(function(attr) { + return attr.arguments ? attr.arguments.filter(function(arg) { + return !arg.optional && !arg.variadic; + }).length : 0; + }) + .reduce(function(m, n) { return Math.min(m, n); }); +} + +// A helper to get the global of a Function object. This is needed to determine +// which global exceptions the function throws will come from. +function globalOf(func) +{ + try { + // Use the fact that .constructor for a Function object is normally the + // Function constructor, which can be used to mint a new function in the + // right global. + return func.constructor("return this;")(); + } catch (e) { + } + // If the above fails, because someone gave us a non-function, or a function + // with a weird proto chain or weird .constructor property, just fall back + // to 'self'. + return self; +} + +// https://esdiscuss.org/topic/isconstructor#content-11 +function isConstructor(o) { + try { + new (new Proxy(o, {construct: () => ({})})); + return true; + } catch(e) { + return false; + } +} + +function throwOrReject(a_test, operation, fn, obj, args, message, cb) +{ + if (operation.idlType.generic !== "Promise") { + assert_throws_js(globalOf(fn).TypeError, function() { + fn.apply(obj, args); + }, message); + cb(); + } else { + try { + promise_rejects_js(a_test, TypeError, fn.apply(obj, args), message).then(cb, cb); + } catch (e){ + a_test.step(function() { + assert_unreached("Throws \"" + e + "\" instead of rejecting promise"); + cb(); + }); + } + } +} + +function awaitNCallbacks(n, cb, ctx) +{ + var counter = 0; + return function() { + counter++; + if (counter >= n) { + cb(); + } + }; +} + +/// IdlHarnessError /// +// Entry point +self.IdlHarnessError = function(message) +{ + /** + * Message to be printed as the error's toString invocation. + */ + this.message = message; +}; + +IdlHarnessError.prototype = Object.create(Error.prototype); + +IdlHarnessError.prototype.toString = function() +{ + return this.message; +}; + + +/// IdlArray /// +// Entry point +self.IdlArray = function() +{ + /** + * A map from strings to the corresponding named IdlObject, such as + * IdlInterface or IdlException. These are the things that test() will run + * tests on. + */ + this.members = {}; + + /** + * A map from strings to arrays of strings. The keys are interface or + * exception names, and are expected to also exist as keys in this.members + * (otherwise they'll be ignored). This is populated by add_objects() -- + * see documentation at the start of the file. The actual tests will be + * run by calling this.members[name].test_object(obj) for each obj in + * this.objects[name]. obj is a string that will be eval'd to produce a + * JavaScript value, which is supposed to be an object implementing the + * given IdlObject (interface, exception, etc.). + */ + this.objects = {}; + + /** + * When adding multiple collections of IDLs one at a time, an earlier one + * might contain a partial interface or includes statement that depends + * on a later one. Save these up and handle them right before we run + * tests. + * + * Both this.partials and this.includes will be the objects as parsed by + * WebIDLParser.js, not wrapped in IdlInterface or similar. + */ + this.partials = []; + this.includes = []; + + /** + * Record of skipped IDL items, in case we later realize that they are a + * dependency (to retroactively process them). + */ + this.skipped = new Map(); +}; + +IdlArray.prototype.add_idls = function(raw_idls, options) +{ + /** Entry point. See documentation at beginning of file. */ + this.internal_add_idls(WebIDL2.parse(raw_idls), options); +}; + +IdlArray.prototype.add_untested_idls = function(raw_idls, options) +{ + /** Entry point. See documentation at beginning of file. */ + var parsed_idls = WebIDL2.parse(raw_idls); + this.mark_as_untested(parsed_idls); + this.internal_add_idls(parsed_idls, options); +}; + +IdlArray.prototype.mark_as_untested = function (parsed_idls) +{ + for (var i = 0; i < parsed_idls.length; i++) { + parsed_idls[i].untested = true; + if ("members" in parsed_idls[i]) { + for (var j = 0; j < parsed_idls[i].members.length; j++) { + parsed_idls[i].members[j].untested = true; + } + } + } +}; + +IdlArray.prototype.is_excluded_by_options = function (name, options) +{ + return options && + (options.except && options.except.includes(name) + || options.only && !options.only.includes(name)); +}; + +IdlArray.prototype.add_dependency_idls = function(raw_idls, options) +{ + return this.internal_add_dependency_idls(WebIDL2.parse(raw_idls), options); +}; + +IdlArray.prototype.internal_add_dependency_idls = function(parsed_idls, options) +{ + const new_options = { only: [] } + + const all_deps = new Set(); + Object.values(this.members).forEach(v => { + if (v.base) { + all_deps.add(v.base); + } + }); + // Add both 'A' and 'B' for each 'A includes B' entry. + this.includes.forEach(i => { + all_deps.add(i.target); + all_deps.add(i.includes); + }); + this.partials.forEach(p => all_deps.add(p.name)); + // Add 'TypeOfType' for each "typedef TypeOfType MyType;" entry. + Object.entries(this.members).forEach(([k, v]) => { + if (v instanceof IdlTypedef) { + let defs = v.idlType.union + ? v.idlType.idlType.map(t => t.idlType) + : [v.idlType.idlType]; + defs.forEach(d => all_deps.add(d)); + } + }); + + // Add the attribute idlTypes of all the nested members of idls. + const attrDeps = parsedIdls => { + return parsedIdls.reduce((deps, parsed) => { + if (parsed.members) { + for (const attr of Object.values(parsed.members).filter(m => m.type === 'attribute')) { + let attrType = attr.idlType; + // Check for generic members (e.g. FrozenArray<MyType>) + if (attrType.generic) { + deps.add(attrType.generic); + attrType = attrType.idlType; + } + deps.add(attrType.idlType); + } + } + if (parsed.base in this.members) { + attrDeps([this.members[parsed.base]]).forEach(dep => deps.add(dep)); + } + return deps; + }, new Set()); + }; + + const testedMembers = Object.values(this.members).filter(m => !m.untested && m.members); + attrDeps(testedMembers).forEach(dep => all_deps.add(dep)); + + const testedPartials = this.partials.filter(m => !m.untested && m.members); + attrDeps(testedPartials).forEach(dep => all_deps.add(dep)); + + + if (options && options.except && options.only) { + throw new IdlHarnessError("The only and except options can't be used together."); + } + + const defined_or_untested = name => { + // NOTE: Deps are untested, so we're lenient, and skip re-encountered definitions. + // e.g. for 'idl' containing A:B, B:C, C:D + // array.add_idls(idl, {only: ['A','B']}). + // array.add_dependency_idls(idl); + // B would be encountered as tested, and encountered as a dep, so we ignore. + return name in this.members + || this.is_excluded_by_options(name, options); + } + // Maps name -> [parsed_idl, ...] + const process = function(parsed) { + var deps = []; + if (parsed.name) { + deps.push(parsed.name); + } else if (parsed.type === "includes") { + deps.push(parsed.target); + deps.push(parsed.includes); + } + + deps = deps.filter(function(name) { + if (!name + || name === parsed.name && defined_or_untested(name) + || !all_deps.has(name)) { + // Flag as skipped, if it's not already processed, so we can + // come back to it later if we retrospectively call it a dep. + if (name && !(name in this.members)) { + this.skipped.has(name) + ? this.skipped.get(name).push(parsed) + : this.skipped.set(name, [parsed]); + } + return false; + } + return true; + }.bind(this)); + + deps.forEach(function(name) { + if (!new_options.only.includes(name)) { + new_options.only.push(name); + } + + const follow_up = new Set(); + for (const dep_type of ["inheritance", "includes"]) { + if (parsed[dep_type]) { + const inheriting = parsed[dep_type]; + const inheritor = parsed.name || parsed.target; + const deps = [inheriting]; + // For A includes B, we can ignore A, unless B (or some of its + // members) is being tested. + if (dep_type !== "includes" + || inheriting in this.members && !this.members[inheriting].untested + || this.partials.some(function(p) { + return p.name === inheriting; + })) { + deps.push(inheritor); + } + for (const dep of deps) { + if (!new_options.only.includes(dep)) { + new_options.only.push(dep); + } + all_deps.add(dep); + follow_up.add(dep); + } + } + } + + for (const deferred of follow_up) { + if (this.skipped.has(deferred)) { + const next = this.skipped.get(deferred); + this.skipped.delete(deferred); + next.forEach(process); + } + } + }.bind(this)); + }.bind(this); + + for (let parsed of parsed_idls) { + process(parsed); + } + + this.mark_as_untested(parsed_idls); + + if (new_options.only.length) { + this.internal_add_idls(parsed_idls, new_options); + } +} + +IdlArray.prototype.internal_add_idls = function(parsed_idls, options) +{ + /** + * Internal helper called by add_idls() and add_untested_idls(). + * + * parsed_idls is an array of objects that come from WebIDLParser.js's + * "definitions" production. The add_untested_idls() entry point + * additionally sets an .untested property on each object (and its + * .members) so that they'll be skipped by test() -- they'll only be + * used for base interfaces of tested interfaces, return types, etc. + * + * options is a dictionary that can have an only or except member which are + * arrays. If only is given then only members, partials and interface + * targets listed will be added, and if except is given only those that + * aren't listed will be added. Only one of only and except can be used. + */ + + if (options && options.only && options.except) + { + throw new IdlHarnessError("The only and except options can't be used together."); + } + + var should_skip = name => { + return this.is_excluded_by_options(name, options); + } + + parsed_idls.forEach(function(parsed_idl) + { + var partial_types = [ + "interface", + "interface mixin", + "dictionary", + "namespace", + ]; + if (parsed_idl.partial && partial_types.includes(parsed_idl.type)) + { + if (should_skip(parsed_idl.name)) + { + return; + } + this.partials.push(parsed_idl); + return; + } + + if (parsed_idl.type == "includes") + { + if (should_skip(parsed_idl.target)) + { + return; + } + this.includes.push(parsed_idl); + return; + } + + parsed_idl.array = this; + if (should_skip(parsed_idl.name)) + { + return; + } + if (parsed_idl.name in this.members) + { + throw new IdlHarnessError("Duplicate identifier " + parsed_idl.name); + } + + switch(parsed_idl.type) + { + case "interface": + this.members[parsed_idl.name] = + new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ false); + break; + + case "interface mixin": + this.members[parsed_idl.name] = + new IdlInterface(parsed_idl, /* is_callback = */ false, /* is_mixin = */ true); + break; + + case "dictionary": + // Nothing to test, but we need the dictionary info around for type + // checks + this.members[parsed_idl.name] = new IdlDictionary(parsed_idl); + break; + + case "typedef": + this.members[parsed_idl.name] = new IdlTypedef(parsed_idl); + break; + + case "callback": + this.members[parsed_idl.name] = new IdlCallback(parsed_idl); + break; + + case "enum": + this.members[parsed_idl.name] = new IdlEnum(parsed_idl); + break; + + case "callback interface": + this.members[parsed_idl.name] = + new IdlInterface(parsed_idl, /* is_callback = */ true, /* is_mixin = */ false); + break; + + case "namespace": + this.members[parsed_idl.name] = new IdlNamespace(parsed_idl); + break; + + default: + throw parsed_idl.name + ": " + parsed_idl.type + " not yet supported"; + } + }.bind(this)); +}; + +IdlArray.prototype.add_objects = function(dict) +{ + /** Entry point. See documentation at beginning of file. */ + for (var k in dict) + { + if (k in this.objects) + { + this.objects[k] = this.objects[k].concat(dict[k]); + } + else + { + this.objects[k] = dict[k]; + } + } +}; + +IdlArray.prototype.prevent_multiple_testing = function(name) +{ + /** Entry point. See documentation at beginning of file. */ + this.members[name].prevent_multiple_testing = true; +}; + +IdlArray.prototype.is_json_type = function(type) +{ + /** + * Checks whether type is a JSON type as per + * https://webidl.spec.whatwg.org/#dfn-json-types + */ + + var idlType = type.idlType; + + if (type.generic == "Promise") { return false; } + + // nullable and annotated types don't need to be handled separately, + // as webidl2 doesn't represent them wrapped-up (as they're described + // in WebIDL). + + // union and record types + if (type.union || type.generic == "record") { + return idlType.every(this.is_json_type, this); + } + + // sequence types + if (type.generic == "sequence" || type.generic == "FrozenArray") { + return this.is_json_type(idlType[0]); + } + + if (typeof idlType != "string") { throw new Error("Unexpected type " + JSON.stringify(idlType)); } + + switch (idlType) + { + // Numeric types + case "byte": + case "octet": + case "short": + case "unsigned short": + case "long": + case "unsigned long": + case "long long": + case "unsigned long long": + case "float": + case "double": + case "unrestricted float": + case "unrestricted double": + // boolean + case "boolean": + // string types + case "DOMString": + case "ByteString": + case "USVString": + // object type + case "object": + return true; + case "Error": + case "DOMException": + case "Int8Array": + case "Int16Array": + case "Int32Array": + case "Uint8Array": + case "Uint16Array": + case "Uint32Array": + case "Uint8ClampedArray": + case "BigInt64Array": + case "BigUint64Array": + case "Float32Array": + case "Float64Array": + case "ArrayBuffer": + case "DataView": + case "any": + return false; + default: + var thing = this.members[idlType]; + if (!thing) { throw new Error("Type " + idlType + " not found"); } + if (thing instanceof IdlEnum) { return true; } + + if (thing instanceof IdlTypedef) { + return this.is_json_type(thing.idlType); + } + + // dictionaries where all of their members are JSON types + if (thing instanceof IdlDictionary) { + const map = new Map(); + for (const dict of thing.get_reverse_inheritance_stack()) { + for (const m of dict.members) { + map.set(m.name, m.idlType); + } + } + return Array.from(map.values()).every(this.is_json_type, this); + } + + // interface types that have a toJSON operation declared on themselves or + // one of their inherited interfaces. + if (thing instanceof IdlInterface) { + var base; + while (thing) + { + if (thing.has_to_json_regular_operation()) { return true; } + var mixins = this.includes[thing.name]; + if (mixins) { + mixins = mixins.map(function(id) { + var mixin = this.members[id]; + if (!mixin) { + throw new Error("Interface " + id + " not found (implemented by " + thing.name + ")"); + } + return mixin; + }, this); + if (mixins.some(function(m) { return m.has_to_json_regular_operation() } )) { return true; } + } + if (!thing.base) { return false; } + base = this.members[thing.base]; + if (!base) { + throw new Error("Interface " + thing.base + " not found (inherited by " + thing.name + ")"); + } + thing = base; + } + return false; + } + return false; + } +}; + +function exposure_set(object, default_set) { + var exposed = object.extAttrs && object.extAttrs.filter(a => a.name === "Exposed"); + if (exposed && exposed.length > 1) { + throw new IdlHarnessError( + `Multiple 'Exposed' extended attributes on ${object.name}`); + } + + let result = default_set || ["Window"]; + if (result && !(result instanceof Set)) { + result = new Set(result); + } + if (exposed && exposed.length) { + const { rhs } = exposed[0]; + // Could be a list or a string. + const set = + rhs.type === "*" ? + [ "*" ] : + rhs.type === "identifier-list" ? + rhs.value.map(id => id.value) : + [ rhs.value ]; + result = new Set(set); + } + if (result && result.has("*")) { + return "*"; + } + if (result && result.has("Worker")) { + result.delete("Worker"); + result.add("DedicatedWorker"); + result.add("ServiceWorker"); + result.add("SharedWorker"); + } + return result; +} + +function exposed_in(globals) { + if (globals === "*") { + return true; + } + if ('Window' in self) { + return globals.has("Window"); + } + if ('DedicatedWorkerGlobalScope' in self && + self instanceof DedicatedWorkerGlobalScope) { + return globals.has("DedicatedWorker"); + } + if ('SharedWorkerGlobalScope' in self && + self instanceof SharedWorkerGlobalScope) { + return globals.has("SharedWorker"); + } + if ('ServiceWorkerGlobalScope' in self && + self instanceof ServiceWorkerGlobalScope) { + return globals.has("ServiceWorker"); + } + if (Object.getPrototypeOf(self) === Object.prototype) { + // ShadowRealm - only exposed with `"*"`. + return false; + } + throw new IdlHarnessError("Unexpected global object"); +} + +/** + * Asserts that the given error message is thrown for the given function. + * @param {string|IdlHarnessError} error Expected Error message. + * @param {Function} idlArrayFunc Function operating on an IdlArray that should throw. + */ +IdlArray.prototype.assert_throws = function(error, idlArrayFunc) +{ + try { + idlArrayFunc.call(this, this); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + // Assertions for behaviour of the idlharness.js engine. + if (error instanceof IdlHarnessError) { + error = error.message; + } + if (e.message !== error) { + throw new IdlHarnessError(`${idlArrayFunc} threw "${e}", not the expected IdlHarnessError "${error}"`); + } + return; + } + throw new IdlHarnessError(`${idlArrayFunc} did not throw the expected IdlHarnessError`); +} + +IdlArray.prototype.test = function() +{ + /** Entry point. See documentation at beginning of file. */ + + // First merge in all partial definitions and interface mixins. + this.merge_partials(); + this.merge_mixins(); + + // Assert B defined for A : B + for (const member of Object.values(this.members).filter(m => m.base)) { + const lhs = member.name; + const rhs = member.base; + if (!(rhs in this.members)) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is undefined.`); + const lhs_is_interface = this.members[lhs] instanceof IdlInterface; + const rhs_is_interface = this.members[rhs] instanceof IdlInterface; + if (rhs_is_interface != lhs_is_interface) { + if (!lhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${lhs} is not an interface.`); + if (!rhs_is_interface) throw new IdlHarnessError(`${lhs} inherits ${rhs}, but ${rhs} is not an interface.`); + } + // Check for circular dependencies. + member.get_reverse_inheritance_stack(); + } + + Object.getOwnPropertyNames(this.members).forEach(function(memberName) { + var member = this.members[memberName]; + if (!(member instanceof IdlInterface)) { + return; + } + + var globals = exposure_set(member); + member.exposed = exposed_in(globals); + member.exposureSet = globals; + }.bind(this)); + + // Now run test() on every member, and test_object() for every object. + for (var name in this.members) + { + this.members[name].test(); + if (name in this.objects) + { + const objects = this.objects[name]; + if (!objects || !Array.isArray(objects)) { + throw new IdlHarnessError(`Invalid or empty objects for member ${name}`); + } + objects.forEach(function(str) + { + if (!this.members[name] || !(this.members[name] instanceof IdlInterface)) { + throw new IdlHarnessError(`Invalid object member name ${name}`); + } + this.members[name].test_object(str); + }.bind(this)); + } + } +}; + +IdlArray.prototype.merge_partials = function() +{ + const testedPartials = new Map(); + this.partials.forEach(function(parsed_idl) + { + const originalExists = parsed_idl.name in this.members + && (this.members[parsed_idl.name] instanceof IdlInterface + || this.members[parsed_idl.name] instanceof IdlDictionary + || this.members[parsed_idl.name] instanceof IdlNamespace); + + // Ensure unique test name in case of multiple partials. + let partialTestName = parsed_idl.name; + let partialTestCount = 1; + if (testedPartials.has(parsed_idl.name)) { + partialTestCount += testedPartials.get(parsed_idl.name); + partialTestName = `${partialTestName}[${partialTestCount}]`; + } + testedPartials.set(parsed_idl.name, partialTestCount); + + if (!parsed_idl.untested) { + test(function () { + assert_true(originalExists, `Original ${parsed_idl.type} should be defined`); + + var expected; + switch (parsed_idl.type) { + case 'dictionary': expected = IdlDictionary; break; + case 'namespace': expected = IdlNamespace; break; + case 'interface': + case 'interface mixin': + default: + expected = IdlInterface; break; + } + assert_true( + expected.prototype.isPrototypeOf(this.members[parsed_idl.name]), + `Original ${parsed_idl.name} definition should have type ${parsed_idl.type}`); + }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: original ${parsed_idl.type} defined`); + } + if (!originalExists) { + // Not good.. but keep calm and carry on. + return; + } + + if (parsed_idl.extAttrs) + { + // Special-case "Exposed". Must be a subset of original interface's exposure. + // Exposed on a partial is the equivalent of having the same Exposed on all nested members. + // See https://github.com/heycam/webidl/issues/154 for discrepency between Exposed and + // other extended attributes on partial interfaces. + const exposureAttr = parsed_idl.extAttrs.find(a => a.name === "Exposed"); + if (exposureAttr) { + if (!parsed_idl.untested) { + test(function () { + const partialExposure = exposure_set(parsed_idl); + const memberExposure = exposure_set(this.members[parsed_idl.name]); + if (memberExposure === "*") { + return; + } + if (partialExposure === "*") { + throw new IdlHarnessError( + `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed everywhere, the original ${parsed_idl.type} is not.`); + } + partialExposure.forEach(name => { + if (!memberExposure || !memberExposure.has(name)) { + throw new IdlHarnessError( + `Partial ${parsed_idl.name} ${parsed_idl.type} is exposed to '${name}', the original ${parsed_idl.type} is not.`); + } + }); + }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: valid exposure set`); + } + parsed_idl.members.forEach(function (member) { + member.extAttrs.push(exposureAttr); + }.bind(this)); + } + + parsed_idl.extAttrs.forEach(function(extAttr) + { + // "Exposed" already handled above. + if (extAttr.name === "Exposed") { + return; + } + this.members[parsed_idl.name].extAttrs.push(extAttr); + }.bind(this)); + } + if (parsed_idl.members.length) { + test(function () { + var clash = parsed_idl.members.find(function(member) { + return this.members[parsed_idl.name].members.find(function(m) { + return this.are_duplicate_members(m, member); + }.bind(this)); + }.bind(this)); + parsed_idl.members.forEach(function(member) + { + this.members[parsed_idl.name].members.push(new IdlInterfaceMember(member)); + }.bind(this)); + assert_true(!clash, "member " + (clash && clash.name) + " is unique"); + }.bind(this), `Partial ${parsed_idl.type} ${partialTestName}: member names are unique`); + } + }.bind(this)); + this.partials = []; +} + +IdlArray.prototype.merge_mixins = function() +{ + for (const parsed_idl of this.includes) + { + const lhs = parsed_idl.target; + const rhs = parsed_idl.includes; + + var errStr = lhs + " includes " + rhs + ", but "; + if (!(lhs in this.members)) throw errStr + lhs + " is undefined."; + if (!(this.members[lhs] instanceof IdlInterface)) throw errStr + lhs + " is not an interface."; + if (!(rhs in this.members)) throw errStr + rhs + " is undefined."; + if (!(this.members[rhs] instanceof IdlInterface)) throw errStr + rhs + " is not an interface."; + + if (this.members[rhs].members.length) { + test(function () { + var clash = this.members[rhs].members.find(function(member) { + return this.members[lhs].members.find(function(m) { + return this.are_duplicate_members(m, member); + }.bind(this)); + }.bind(this)); + this.members[rhs].members.forEach(function(member) { + assert_true( + this.members[lhs].members.every(m => !this.are_duplicate_members(m, member)), + "member " + member.name + " is unique"); + this.members[lhs].members.push(new IdlInterfaceMember(member)); + }.bind(this)); + assert_true(!clash, "member " + (clash && clash.name) + " is unique"); + }.bind(this), lhs + " includes " + rhs + ": member names are unique"); + } + } + this.includes = []; +} + +IdlArray.prototype.are_duplicate_members = function(m1, m2) { + if (m1.name !== m2.name) { + return false; + } + if (m1.type === 'operation' && m2.type === 'operation' + && m1.arguments.length !== m2.arguments.length) { + // Method overload. TODO: Deep comparison of arguments. + return false; + } + return true; +} + +IdlArray.prototype.assert_type_is = function(value, type) +{ + if (type.idlType in this.members + && this.members[type.idlType] instanceof IdlTypedef) { + this.assert_type_is(value, this.members[type.idlType].idlType); + return; + } + + if (type.nullable && value === null) + { + // This is fine + return; + } + + if (type.union) { + for (var i = 0; i < type.idlType.length; i++) { + try { + this.assert_type_is(value, type.idlType[i]); + // No AssertionError, so we match one type in the union + return; + } catch(e) { + if (e instanceof AssertionError) { + // We didn't match this type, let's try some others + continue; + } + throw e; + } + } + // TODO: Is there a nice way to list the union's types in the message? + assert_true(false, "Attribute has value " + format_value(value) + + " which doesn't match any of the types in the union"); + + } + + /** + * Helper function that tests that value is an instance of type according + * to the rules of WebIDL. value is any JavaScript value, and type is an + * object produced by WebIDLParser.js' "type" production. That production + * is fairly elaborate due to the complexity of WebIDL's types, so it's + * best to look at the grammar to figure out what properties it might have. + */ + if (type.idlType == "any") + { + // No assertions to make + return; + } + + if (type.array) + { + // TODO: not supported yet + return; + } + + if (type.generic === "sequence" || type.generic == "ObservableArray") + { + assert_true(Array.isArray(value), "should be an Array"); + if (!value.length) + { + // Nothing we can do. + return; + } + this.assert_type_is(value[0], type.idlType[0]); + return; + } + + if (type.generic === "Promise") { + assert_true("then" in value, "Attribute with a Promise type should have a then property"); + // TODO: Ideally, we would check on project fulfillment + // that we get the right type + // but that would require making the type check async + return; + } + + if (type.generic === "FrozenArray") { + assert_true(Array.isArray(value), "Value should be array"); + assert_true(Object.isFrozen(value), "Value should be frozen"); + if (!value.length) + { + // Nothing we can do. + return; + } + this.assert_type_is(value[0], type.idlType[0]); + return; + } + + type = Array.isArray(type.idlType) ? type.idlType[0] : type.idlType; + + switch(type) + { + case "undefined": + assert_equals(value, undefined); + return; + + case "boolean": + assert_equals(typeof value, "boolean"); + return; + + case "byte": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(-128 <= value && value <= 127, "byte " + value + " should be in range [-128, 127]"); + return; + + case "octet": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(0 <= value && value <= 255, "octet " + value + " should be in range [0, 255]"); + return; + + case "short": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(-32768 <= value && value <= 32767, "short " + value + " should be in range [-32768, 32767]"); + return; + + case "unsigned short": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(0 <= value && value <= 65535, "unsigned short " + value + " should be in range [0, 65535]"); + return; + + case "long": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(-2147483648 <= value && value <= 2147483647, "long " + value + " should be in range [-2147483648, 2147483647]"); + return; + + case "unsigned long": + assert_equals(typeof value, "number"); + assert_equals(value, Math.floor(value), "should be an integer"); + assert_true(0 <= value && value <= 4294967295, "unsigned long " + value + " should be in range [0, 4294967295]"); + return; + + case "long long": + assert_equals(typeof value, "number"); + return; + + case "unsigned long long": + case "DOMTimeStamp": + assert_equals(typeof value, "number"); + assert_true(0 <= value, "unsigned long long should be positive"); + return; + + case "float": + assert_equals(typeof value, "number"); + assert_equals(value, Math.fround(value), "float rounded to 32-bit float should be itself"); + assert_not_equals(value, Infinity); + assert_not_equals(value, -Infinity); + assert_not_equals(value, NaN); + return; + + case "DOMHighResTimeStamp": + case "double": + assert_equals(typeof value, "number"); + assert_not_equals(value, Infinity); + assert_not_equals(value, -Infinity); + assert_not_equals(value, NaN); + return; + + case "unrestricted float": + assert_equals(typeof value, "number"); + assert_equals(value, Math.fround(value), "unrestricted float rounded to 32-bit float should be itself"); + return; + + case "unrestricted double": + assert_equals(typeof value, "number"); + return; + + case "DOMString": + assert_equals(typeof value, "string"); + return; + + case "ByteString": + assert_equals(typeof value, "string"); + assert_regexp_match(value, /^[\x00-\x7F]*$/); + return; + + case "USVString": + assert_equals(typeof value, "string"); + assert_regexp_match(value, /^([\x00-\ud7ff\ue000-\uffff]|[\ud800-\udbff][\udc00-\udfff])*$/); + return; + + case "ArrayBufferView": + assert_true(ArrayBuffer.isView(value)); + return; + + case "object": + assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function"); + return; + } + + // This is a catch-all for any IDL type name which follows JS class + // semantics. This includes some non-interface IDL types (e.g. Int8Array, + // Function, ...), as well as any interface types that are not in the IDL + // that is fed to the harness. If an IDL type does not follow JS class + // semantics then it should go in the switch statement above. If an IDL + // type needs full checking, then the test should include it in the IDL it + // feeds to the harness. + if (!(type in this.members)) + { + assert_true(value instanceof self[type], "wrong type: not a " + type); + return; + } + + if (this.members[type] instanceof IdlInterface) + { + // We don't want to run the full + // IdlInterface.prototype.test_instance_of, because that could result + // in an infinite loop. TODO: This means we don't have tests for + // LegacyNoInterfaceObject interfaces, and we also can't test objects + // that come from another self. + assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function"); + if (value instanceof Object + && !this.members[type].has_extended_attribute("LegacyNoInterfaceObject") + && type in self) + { + assert_true(value instanceof self[type], "instanceof " + type); + } + } + else if (this.members[type] instanceof IdlEnum) + { + assert_equals(typeof value, "string"); + } + else if (this.members[type] instanceof IdlDictionary) + { + // TODO: Test when we actually have something to test this on + } + else if (this.members[type] instanceof IdlCallback) + { + assert_equals(typeof value, "function"); + } + else + { + throw new IdlHarnessError("Type " + type + " isn't an interface, callback or dictionary"); + } +}; + +/// IdlObject /// +function IdlObject() {} +IdlObject.prototype.test = function() +{ + /** + * By default, this does nothing, so no actual tests are run for IdlObjects + * that don't define any (e.g., IdlDictionary at the time of this writing). + */ +}; + +IdlObject.prototype.has_extended_attribute = function(name) +{ + /** + * This is only meaningful for things that support extended attributes, + * such as interfaces, exceptions, and members. + */ + return this.extAttrs.some(function(o) + { + return o.name == name; + }); +}; + + +/// IdlDictionary /// +// Used for IdlArray.prototype.assert_type_is +function IdlDictionary(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "dictionary" + * production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** A back-reference to our IdlArray. */ + this.array = obj.array; + + /** An array of objects produced by the "dictionaryMember" production. */ + this.members = obj.members; + + /** + * The name (as a string) of the dictionary type we inherit from, or null + * if there is none. + */ + this.base = obj.inheritance; +} + +IdlDictionary.prototype = Object.create(IdlObject.prototype); + +IdlDictionary.prototype.get_reverse_inheritance_stack = function() { + return IdlInterface.prototype.get_reverse_inheritance_stack.call(this); +}; + +/// IdlInterface /// +function IdlInterface(obj, is_callback, is_mixin) +{ + /** + * obj is an object produced by the WebIDLParser.js "interface" production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** A back-reference to our IdlArray. */ + this.array = obj.array; + + /** + * An indicator of whether we should run tests on the interface object and + * interface prototype object. Tests on members are controlled by .untested + * on each member, not this. + */ + this.untested = obj.untested; + + /** An array of objects produced by the "ExtAttr" production. */ + this.extAttrs = obj.extAttrs; + + /** An array of IdlInterfaceMembers. */ + this.members = obj.members.map(function(m){return new IdlInterfaceMember(m); }); + if (this.has_extended_attribute("LegacyUnforgeable")) { + this.members + .filter(function(m) { return m.special !== "static" && (m.type == "attribute" || m.type == "operation"); }) + .forEach(function(m) { return m.isUnforgeable = true; }); + } + + /** + * The name (as a string) of the type we inherit from, or null if there is + * none. + */ + this.base = obj.inheritance; + + this._is_callback = is_callback; + this._is_mixin = is_mixin; +} +IdlInterface.prototype = Object.create(IdlObject.prototype); +IdlInterface.prototype.is_callback = function() +{ + return this._is_callback; +}; + +IdlInterface.prototype.is_mixin = function() +{ + return this._is_mixin; +}; + +IdlInterface.prototype.has_constants = function() +{ + return this.members.some(function(member) { + return member.type === "const"; + }); +}; + +IdlInterface.prototype.get_unscopables = function() +{ + return this.members.filter(function(member) { + return member.isUnscopable; + }); +}; + +IdlInterface.prototype.is_global = function() +{ + return this.extAttrs.some(function(attribute) { + return attribute.name === "Global"; + }); +}; + +/** + * Value of the LegacyNamespace extended attribute, if any. + * + * https://webidl.spec.whatwg.org/#LegacyNamespace + */ +IdlInterface.prototype.get_legacy_namespace = function() +{ + var legacyNamespace = this.extAttrs.find(function(attribute) { + return attribute.name === "LegacyNamespace"; + }); + return legacyNamespace ? legacyNamespace.rhs.value : undefined; +}; + +IdlInterface.prototype.get_interface_object_owner = function() +{ + var legacyNamespace = this.get_legacy_namespace(); + return legacyNamespace ? self[legacyNamespace] : self; +}; + +IdlInterface.prototype.should_have_interface_object = function() +{ + // "For every interface that is exposed in a given ECMAScript global + // environment and: + // * is a callback interface that has constants declared on it, or + // * is a non-callback interface that is not declared with the + // [LegacyNoInterfaceObject] extended attribute, + // a corresponding property MUST exist on the ECMAScript global object. + + return this.is_callback() ? this.has_constants() : !this.has_extended_attribute("LegacyNoInterfaceObject"); +}; + +IdlInterface.prototype.assert_interface_object_exists = function() +{ + var owner = this.get_legacy_namespace() || "self"; + assert_own_property(self[owner], this.name, owner + " does not have own property " + format_value(this.name)); +}; + +IdlInterface.prototype.get_interface_object = function() { + if (!this.should_have_interface_object()) { + var reason = this.is_callback() ? "lack of declared constants" : "declared [LegacyNoInterfaceObject] attribute"; + throw new IdlHarnessError(this.name + " has no interface object due to " + reason); + } + + return this.get_interface_object_owner()[this.name]; +}; + +IdlInterface.prototype.get_qualified_name = function() { + // https://webidl.spec.whatwg.org/#qualified-name + var legacyNamespace = this.get_legacy_namespace(); + if (legacyNamespace) { + return legacyNamespace + "." + this.name; + } + return this.name; +}; + +IdlInterface.prototype.has_to_json_regular_operation = function() { + return this.members.some(function(m) { + return m.is_to_json_regular_operation(); + }); +}; + +IdlInterface.prototype.has_default_to_json_regular_operation = function() { + return this.members.some(function(m) { + return m.is_to_json_regular_operation() && m.has_extended_attribute("Default"); + }); +}; + +/** + * Implementation of https://webidl.spec.whatwg.org/#create-an-inheritance-stack + * with the order reversed. + * + * The order is reversed so that the base class comes first in the list, because + * this is what all call sites need. + * + * So given: + * + * A : B {}; + * B : C {}; + * C {}; + * + * then A.get_reverse_inheritance_stack() returns [C, B, A], + * and B.get_reverse_inheritance_stack() returns [C, B]. + * + * Note: as dictionary inheritance is expressed identically by the AST, + * this works just as well for getting a stack of inherited dictionaries. + */ +IdlInterface.prototype.get_reverse_inheritance_stack = function() { + const stack = [this]; + let idl_interface = this; + while (idl_interface.base) { + const base = this.array.members[idl_interface.base]; + if (!base) { + throw new Error(idl_interface.type + " " + idl_interface.base + " not found (inherited by " + idl_interface.name + ")"); + } else if (stack.indexOf(base) > -1) { + stack.unshift(base); + const dep_chain = stack.map(i => i.name).join(','); + throw new IdlHarnessError(`${this.name} has a circular dependency: ${dep_chain}`); + } + idl_interface = base; + stack.unshift(idl_interface); + } + return stack; +}; + +/** + * Implementation of + * https://webidl.spec.whatwg.org/#default-tojson-operation + * for testing purposes. + * + * Collects the IDL types of the attributes that meet the criteria + * for inclusion in the default toJSON operation for easy + * comparison with actual value + */ +IdlInterface.prototype.default_to_json_operation = function() { + const map = new Map() + let isDefault = false; + for (const I of this.get_reverse_inheritance_stack()) { + if (I.has_default_to_json_regular_operation()) { + isDefault = true; + for (const m of I.members) { + if (m.special !== "static" && m.type == "attribute" && I.array.is_json_type(m.idlType)) { + map.set(m.name, m.idlType); + } + } + } else if (I.has_to_json_regular_operation()) { + isDefault = false; + } + } + return isDefault ? map : null; +}; + +IdlInterface.prototype.test = function() +{ + if (this.has_extended_attribute("LegacyNoInterfaceObject") || this.is_mixin()) + { + // No tests to do without an instance. TODO: We should still be able + // to run tests on the prototype object, if we obtain one through some + // other means. + return; + } + + // If the interface object is not exposed, only test that. Members can't be + // tested either, but objects could still be tested in |test_object|. + if (!this.exposed) + { + if (!this.untested) + { + subsetTestByKey(this.name, test, function() { + assert_false(this.name in self); + }.bind(this), this.name + " interface: existence and properties of interface object"); + } + return; + } + + if (!this.untested) + { + // First test things to do with the exception/interface object and + // exception/interface prototype object. + this.test_self(); + } + // Then test things to do with its members (constants, fields, attributes, + // operations, . . .). These are run even if .untested is true, because + // members might themselves be marked as .untested. This might happen to + // interfaces if the interface itself is untested but a partial interface + // that extends it is tested -- then the interface itself and its initial + // members will be marked as untested, but the members added by the partial + // interface are still tested. + this.test_members(); +}; + +IdlInterface.prototype.constructors = function() +{ + return this.members + .filter(function(m) { return m.type == "constructor"; }); +} + +IdlInterface.prototype.test_self = function() +{ + subsetTestByKey(this.name, test, function() + { + if (!this.should_have_interface_object()) { + return; + } + + // The name of the property is the identifier of the interface, and its + // value is an object called the interface object. + // The property has the attributes { [[Writable]]: true, + // [[Enumerable]]: false, [[Configurable]]: true }." + // TODO: Should we test here that the property is actually writable + // etc., or trust getOwnPropertyDescriptor? + this.assert_interface_object_exists(); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object_owner(), this.name); + assert_false("get" in desc, "self's property " + format_value(this.name) + " should not have a getter"); + assert_false("set" in desc, "self's property " + format_value(this.name) + " should not have a setter"); + assert_true(desc.writable, "self's property " + format_value(this.name) + " should be writable"); + assert_false(desc.enumerable, "self's property " + format_value(this.name) + " should not be enumerable"); + assert_true(desc.configurable, "self's property " + format_value(this.name) + " should be configurable"); + + if (this.is_callback()) { + // "The internal [[Prototype]] property of an interface object for + // a callback interface must be the Function.prototype object." + assert_equals(Object.getPrototypeOf(this.get_interface_object()), Function.prototype, + "prototype of self's property " + format_value(this.name) + " is not Object.prototype"); + + return; + } + + // "The interface object for a given non-callback interface is a + // function object." + // "If an object is defined to be a function object, then it has + // characteristics as follows:" + + // Its [[Prototype]] internal property is otherwise specified (see + // below). + + // "* Its [[Get]] internal property is set as described in ECMA-262 + // section 9.1.8." + // Not much to test for this. + + // "* Its [[Construct]] internal property is set as described in + // ECMA-262 section 19.2.2.3." + + // "* Its @@hasInstance property is set as described in ECMA-262 + // section 19.2.3.8, unless otherwise specified." + // TODO + + // ES6 (rev 30) 19.1.3.6: + // "Else, if O has a [[Call]] internal method, then let builtinTag be + // "Function"." + assert_class_string(this.get_interface_object(), "Function", "class string of " + this.name); + + // "The [[Prototype]] internal property of an interface object for a + // non-callback interface is determined as follows:" + var prototype = Object.getPrototypeOf(this.get_interface_object()); + if (this.base) { + // "* If the interface inherits from some other interface, the + // value of [[Prototype]] is the interface object for that other + // interface." + var inherited_interface = this.array.members[this.base]; + if (!inherited_interface.has_extended_attribute("LegacyNoInterfaceObject")) { + inherited_interface.assert_interface_object_exists(); + assert_equals(prototype, inherited_interface.get_interface_object(), + 'prototype of ' + this.name + ' is not ' + + this.base); + } + } else { + // "If the interface doesn't inherit from any other interface, the + // value of [[Prototype]] is %FunctionPrototype% ([ECMA-262], + // section 6.1.7.4)." + assert_equals(prototype, Function.prototype, + "prototype of self's property " + format_value(this.name) + " is not Function.prototype"); + } + + // Always test for [[Construct]]: + // https://github.com/heycam/webidl/issues/698 + assert_true(isConstructor(this.get_interface_object()), "interface object must pass IsConstructor check"); + + var interface_object = this.get_interface_object(); + assert_throws_js(globalOf(interface_object).TypeError, function() { + interface_object(); + }, "interface object didn't throw TypeError when called as a function"); + + if (!this.constructors().length) { + assert_throws_js(globalOf(interface_object).TypeError, function() { + new interface_object(); + }, "interface object didn't throw TypeError when called as a constructor"); + } + }.bind(this), this.name + " interface: existence and properties of interface object"); + + if (this.should_have_interface_object() && !this.is_callback()) { + subsetTestByKey(this.name, test, function() { + // This function tests WebIDL as of 2014-10-25. + // https://webidl.spec.whatwg.org/#es-interface-call + + this.assert_interface_object_exists(); + + // "Interface objects for non-callback interfaces MUST have a + // property named “length” with attributes { [[Writable]]: false, + // [[Enumerable]]: false, [[Configurable]]: true } whose value is + // a Number." + assert_own_property(this.get_interface_object(), "length"); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "length"); + assert_false("get" in desc, this.name + ".length should not have a getter"); + assert_false("set" in desc, this.name + ".length should not have a setter"); + assert_false(desc.writable, this.name + ".length should not be writable"); + assert_false(desc.enumerable, this.name + ".length should not be enumerable"); + assert_true(desc.configurable, this.name + ".length should be configurable"); + + var constructors = this.constructors(); + var expected_length = minOverloadLength(constructors); + assert_equals(this.get_interface_object().length, expected_length, "wrong value for " + this.name + ".length"); + }.bind(this), this.name + " interface object length"); + } + + if (this.should_have_interface_object()) { + subsetTestByKey(this.name, test, function() { + // This function tests WebIDL as of 2015-11-17. + // https://webidl.spec.whatwg.org/#interface-object + + this.assert_interface_object_exists(); + + // "All interface objects must have a property named “name” with + // attributes { [[Writable]]: false, [[Enumerable]]: false, + // [[Configurable]]: true } whose value is the identifier of the + // corresponding interface." + + assert_own_property(this.get_interface_object(), "name"); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "name"); + assert_false("get" in desc, this.name + ".name should not have a getter"); + assert_false("set" in desc, this.name + ".name should not have a setter"); + assert_false(desc.writable, this.name + ".name should not be writable"); + assert_false(desc.enumerable, this.name + ".name should not be enumerable"); + assert_true(desc.configurable, this.name + ".name should be configurable"); + assert_equals(this.get_interface_object().name, this.name, "wrong value for " + this.name + ".name"); + }.bind(this), this.name + " interface object name"); + } + + + if (this.has_extended_attribute("LegacyWindowAlias")) { + subsetTestByKey(this.name, test, function() + { + var aliasAttrs = this.extAttrs.filter(function(o) { return o.name === "LegacyWindowAlias"; }); + if (aliasAttrs.length > 1) { + throw new IdlHarnessError("Invalid IDL: multiple LegacyWindowAlias extended attributes on " + this.name); + } + if (this.is_callback()) { + throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on non-interface " + this.name); + } + if (!(this.exposureSet === "*" || this.exposureSet.has("Window"))) { + throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " which is not exposed in Window"); + } + // TODO: when testing of [LegacyNoInterfaceObject] interfaces is supported, + // check that it's not specified together with LegacyWindowAlias. + + // TODO: maybe check that [LegacyWindowAlias] is not specified on a partial interface. + + var rhs = aliasAttrs[0].rhs; + if (!rhs) { + throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " without identifier"); + } + var aliases; + if (rhs.type === "identifier-list") { + aliases = rhs.value.map(id => id.value); + } else { // rhs.type === identifier + aliases = [ rhs.value ]; + } + + // OK now actually check the aliases... + var alias; + if (exposed_in(exposure_set(this, this.exposureSet)) && 'document' in self) { + for (alias of aliases) { + assert_true(alias in self, alias + " should exist"); + assert_equals(self[alias], this.get_interface_object(), "self." + alias + " should be the same value as self." + this.get_qualified_name()); + var desc = Object.getOwnPropertyDescriptor(self, alias); + assert_equals(desc.value, this.get_interface_object(), "wrong value in " + alias + " property descriptor"); + assert_true(desc.writable, alias + " should be writable"); + assert_false(desc.enumerable, alias + " should not be enumerable"); + assert_true(desc.configurable, alias + " should be configurable"); + assert_false('get' in desc, alias + " should not have a getter"); + assert_false('set' in desc, alias + " should not have a setter"); + } + } else { + for (alias of aliases) { + assert_false(alias in self, alias + " should not exist"); + } + } + + }.bind(this), this.name + " interface: legacy window alias"); + } + + if (this.has_extended_attribute("LegacyFactoryFunction")) { + var constructors = this.extAttrs + .filter(function(attr) { return attr.name == "LegacyFactoryFunction"; }); + if (constructors.length !== 1) { + throw new IdlHarnessError("Internal error: missing support for multiple LegacyFactoryFunction extended attributes"); + } + var constructor = constructors[0]; + var min_length = minOverloadLength([constructor]); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "for every [LegacyFactoryFunction] extended attribute on an exposed + // interface, a corresponding property must exist on the ECMAScript + // global object. The name of the property is the + // [LegacyFactoryFunction]'s identifier, and its value is an object + // called a named constructor, ... . The property has the attributes + // { [[Writable]]: true, [[Enumerable]]: false, + // [[Configurable]]: true }." + var name = constructor.rhs.value; + assert_own_property(self, name); + var desc = Object.getOwnPropertyDescriptor(self, name); + assert_equals(desc.value, self[name], "wrong value in " + name + " property descriptor"); + assert_true(desc.writable, name + " should be writable"); + assert_false(desc.enumerable, name + " should not be enumerable"); + assert_true(desc.configurable, name + " should be configurable"); + assert_false("get" in desc, name + " should not have a getter"); + assert_false("set" in desc, name + " should not have a setter"); + }.bind(this), this.name + " interface: named constructor"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "2. Let F be ! CreateBuiltinFunction(realm, steps, + // realm.[[Intrinsics]].[[%FunctionPrototype%]])." + var name = constructor.rhs.value; + var value = self[name]; + assert_equals(typeof value, "function", "type of value in " + name + " property descriptor"); + assert_not_equals(value, this.get_interface_object(), "wrong value in " + name + " property descriptor"); + assert_equals(Object.getPrototypeOf(value), Function.prototype, "wrong value for " + name + "'s prototype"); + }.bind(this), this.name + " interface: named constructor object"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "7. Let proto be the interface prototype object of interface I + // in realm. + // "8. Perform ! DefinePropertyOrThrow(F, "prototype", + // PropertyDescriptor{ + // [[Value]]: proto, [[Writable]]: false, + // [[Enumerable]]: false, [[Configurable]]: false + // })." + var name = constructor.rhs.value; + var expected = this.get_interface_object().prototype; + var desc = Object.getOwnPropertyDescriptor(self[name], "prototype"); + assert_equals(desc.value, expected, "wrong value for " + name + ".prototype"); + assert_false(desc.writable, "prototype should not be writable"); + assert_false(desc.enumerable, "prototype should not be enumerable"); + assert_false(desc.configurable, "prototype should not be configurable"); + assert_false("get" in desc, "prototype should not have a getter"); + assert_false("set" in desc, "prototype should not have a setter"); + }.bind(this), this.name + " interface: named constructor prototype property"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "3. Perform ! SetFunctionName(F, id)." + var name = constructor.rhs.value; + var desc = Object.getOwnPropertyDescriptor(self[name], "name"); + assert_equals(desc.value, name, "wrong value for " + name + ".name"); + assert_false(desc.writable, "name should not be writable"); + assert_false(desc.enumerable, "name should not be enumerable"); + assert_true(desc.configurable, "name should be configurable"); + assert_false("get" in desc, "name should not have a getter"); + assert_false("set" in desc, "name should not have a setter"); + }.bind(this), this.name + " interface: named constructor name"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "4. Initialize S to the effective overload set for constructors + // with identifier id on interface I and with argument count 0. + // "5. Let length be the length of the shortest argument list of + // the entries in S. + // "6. Perform ! SetFunctionLength(F, length)." + var name = constructor.rhs.value; + var desc = Object.getOwnPropertyDescriptor(self[name], "length"); + assert_equals(desc.value, min_length, "wrong value for " + name + ".length"); + assert_false(desc.writable, "length should not be writable"); + assert_false(desc.enumerable, "length should not be enumerable"); + assert_true(desc.configurable, "length should be configurable"); + assert_false("get" in desc, "length should not have a getter"); + assert_false("set" in desc, "length should not have a setter"); + }.bind(this), this.name + " interface: named constructor length"); + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2019-01-14. + + // "1. Let steps be the following steps: + // " 1. If NewTarget is undefined, then throw a TypeError." + var name = constructor.rhs.value; + var args = constructor.arguments.map(function(arg) { + return create_suitable_object(arg.idlType); + }); + assert_throws_js(globalOf(self[name]).TypeError, function() { + self[name](...args); + }.bind(this)); + }.bind(this), this.name + " interface: named constructor without 'new'"); + } + + subsetTestByKey(this.name, test, function() + { + // This function tests WebIDL as of 2015-01-21. + // https://webidl.spec.whatwg.org/#interface-object + + if (!this.should_have_interface_object()) { + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + // "An interface object for a non-callback interface must have a + // property named “prototype” with attributes { [[Writable]]: false, + // [[Enumerable]]: false, [[Configurable]]: false } whose value is an + // object called the interface prototype object. This object has + // properties that correspond to the regular attributes and regular + // operations defined on the interface, and is described in more detail + // in section 4.5.4 below." + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), "prototype"); + assert_false("get" in desc, this.name + ".prototype should not have a getter"); + assert_false("set" in desc, this.name + ".prototype should not have a setter"); + assert_false(desc.writable, this.name + ".prototype should not be writable"); + assert_false(desc.enumerable, this.name + ".prototype should not be enumerable"); + assert_false(desc.configurable, this.name + ".prototype should not be configurable"); + + // Next, test that the [[Prototype]] of the interface prototype object + // is correct. (This is made somewhat difficult by the existence of + // [LegacyNoInterfaceObject].) + // TODO: Aryeh thinks there's at least other place in this file where + // we try to figure out if an interface prototype object is + // correct. Consolidate that code. + + // "The interface prototype object for a given interface A must have an + // internal [[Prototype]] property whose value is returned from the + // following steps: + // "If A is declared with the [Global] extended + // attribute, and A supports named properties, then return the named + // properties object for A, as defined in §3.6.4 Named properties + // object. + // "Otherwise, if A is declared to inherit from another interface, then + // return the interface prototype object for the inherited interface. + // "Otherwise, return %ObjectPrototype%. + // + // "In the ECMAScript binding, the DOMException type has some additional + // requirements: + // + // "Unlike normal interface types, the interface prototype object + // for DOMException must have as its [[Prototype]] the intrinsic + // object %ErrorPrototype%." + // + if (this.name === "Window") { + assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype), + 'WindowProperties', + 'Class name for prototype of Window' + + '.prototype is not "WindowProperties"'); + } else { + var inherit_interface, inherit_interface_interface_object; + if (this.base) { + inherit_interface = this.base; + var parent = this.array.members[inherit_interface]; + if (!parent.has_extended_attribute("LegacyNoInterfaceObject")) { + parent.assert_interface_object_exists(); + inherit_interface_interface_object = parent.get_interface_object(); + } + } else if (this.name === "DOMException") { + inherit_interface = 'Error'; + inherit_interface_interface_object = self.Error; + } else { + inherit_interface = 'Object'; + inherit_interface_interface_object = self.Object; + } + if (inherit_interface_interface_object) { + assert_not_equals(inherit_interface_interface_object, undefined, + 'should inherit from ' + inherit_interface + ', but there is no such property'); + assert_own_property(inherit_interface_interface_object, 'prototype', + 'should inherit from ' + inherit_interface + ', but that object has no "prototype" property'); + assert_equals(Object.getPrototypeOf(this.get_interface_object().prototype), + inherit_interface_interface_object.prototype, + 'prototype of ' + this.name + '.prototype is not ' + inherit_interface + '.prototype'); + } else { + // We can't test that we get the correct object, because this is the + // only way to get our hands on it. We only test that its class + // string, at least, is correct. + assert_class_string(Object.getPrototypeOf(this.get_interface_object().prototype), + inherit_interface + 'Prototype', + 'Class name for prototype of ' + this.name + + '.prototype is not "' + inherit_interface + 'Prototype"'); + } + } + + // "The class string of an interface prototype object is the + // concatenation of the interface’s qualified identifier and the string + // “Prototype”." + + // Skip these tests for now due to a specification issue about + // prototype name. + // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28244 + + // assert_class_string(this.get_interface_object().prototype, this.get_qualified_name() + "Prototype", + // "class string of " + this.name + ".prototype"); + + // String() should end up calling {}.toString if nothing defines a + // stringifier. + if (!this.has_stringifier()) { + // assert_equals(String(this.get_interface_object().prototype), "[object " + this.get_qualified_name() + "Prototype]", + // "String(" + this.name + ".prototype)"); + } + }.bind(this), this.name + " interface: existence and properties of interface prototype object"); + + // "If the interface is declared with the [Global] + // extended attribute, or the interface is in the set of inherited + // interfaces for any other interface that is declared with one of these + // attributes, then the interface prototype object must be an immutable + // prototype exotic object." + // https://webidl.spec.whatwg.org/#interface-prototype-object + if (this.is_global()) { + this.test_immutable_prototype("interface prototype object", this.get_interface_object().prototype); + } + + subsetTestByKey(this.name, test, function() + { + if (!this.should_have_interface_object()) { + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // "If the [LegacyNoInterfaceObject] extended attribute was not specified + // on the interface, then the interface prototype object must also have a + // property named “constructor” with attributes { [[Writable]]: true, + // [[Enumerable]]: false, [[Configurable]]: true } whose value is a + // reference to the interface object for the interface." + assert_own_property(this.get_interface_object().prototype, "constructor", + this.name + '.prototype does not have own property "constructor"'); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, "constructor"); + assert_false("get" in desc, this.name + ".prototype.constructor should not have a getter"); + assert_false("set" in desc, this.name + ".prototype.constructor should not have a setter"); + assert_true(desc.writable, this.name + ".prototype.constructor should be writable"); + assert_false(desc.enumerable, this.name + ".prototype.constructor should not be enumerable"); + assert_true(desc.configurable, this.name + ".prototype.constructor should be configurable"); + assert_equals(this.get_interface_object().prototype.constructor, this.get_interface_object(), + this.name + '.prototype.constructor is not the same object as ' + this.name); + }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s "constructor" property'); + + + subsetTestByKey(this.name, test, function() + { + if (!this.should_have_interface_object()) { + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // If the interface has any member declared with the [Unscopable] extended + // attribute, then there must be a property on the interface prototype object + // whose name is the @@unscopables symbol, which has the attributes + // { [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }, + // and whose value is an object created as follows... + var unscopables = this.get_unscopables().map(m => m.name); + var proto = this.get_interface_object().prototype; + if (unscopables.length != 0) { + assert_own_property( + proto, Symbol.unscopables, + this.name + '.prototype should have an @@unscopables property'); + var desc = Object.getOwnPropertyDescriptor(proto, Symbol.unscopables); + assert_false("get" in desc, + this.name + ".prototype[Symbol.unscopables] should not have a getter"); + assert_false("set" in desc, this.name + ".prototype[Symbol.unscopables] should not have a setter"); + assert_false(desc.writable, this.name + ".prototype[Symbol.unscopables] should not be writable"); + assert_false(desc.enumerable, this.name + ".prototype[Symbol.unscopables] should not be enumerable"); + assert_true(desc.configurable, this.name + ".prototype[Symbol.unscopables] should be configurable"); + assert_equals(desc.value, proto[Symbol.unscopables], + this.name + '.prototype[Symbol.unscopables] should be in the descriptor'); + assert_equals(typeof desc.value, "object", + this.name + '.prototype[Symbol.unscopables] should be an object'); + assert_equals(Object.getPrototypeOf(desc.value), null, + this.name + '.prototype[Symbol.unscopables] should have a null prototype'); + assert_equals(Object.getOwnPropertySymbols(desc.value).length, + 0, + this.name + '.prototype[Symbol.unscopables] should have the right number of symbol-named properties'); + + // Check that we do not have _extra_ unscopables. Checking that we + // have all the ones we should will happen in the per-member tests. + var observed = Object.getOwnPropertyNames(desc.value); + for (var prop of observed) { + assert_not_equals(unscopables.indexOf(prop), + -1, + this.name + '.prototype[Symbol.unscopables] has unexpected property "' + prop + '"'); + } + } else { + assert_equals(Object.getOwnPropertyDescriptor(this.get_interface_object().prototype, Symbol.unscopables), + undefined, + this.name + '.prototype should not have @@unscopables'); + } + }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s @@unscopables property'); +}; + +IdlInterface.prototype.test_immutable_prototype = function(type, obj) +{ + if (typeof Object.setPrototypeOf !== "function") { + return; + } + + subsetTestByKey(this.name, test, function(t) { + var originalValue = Object.getPrototypeOf(obj); + var newValue = Object.create(null); + + t.add_cleanup(function() { + try { + Object.setPrototypeOf(obj, originalValue); + } catch (err) {} + }); + + assert_throws_js(TypeError, function() { + Object.setPrototypeOf(obj, newValue); + }); + + assert_equals( + Object.getPrototypeOf(obj), + originalValue, + "original value not modified" + ); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to a new value via Object.setPrototypeOf " + + "should throw a TypeError"); + + subsetTestByKey(this.name, test, function(t) { + var originalValue = Object.getPrototypeOf(obj); + var newValue = Object.create(null); + + t.add_cleanup(function() { + let setter = Object.getOwnPropertyDescriptor( + Object.prototype, '__proto__' + ).set; + + try { + setter.call(obj, originalValue); + } catch (err) {} + }); + + // We need to find the actual setter for the '__proto__' property, so we + // can determine the right global for it. Walk up the prototype chain + // looking for that property until we find it. + let setter; + { + let cur = obj; + while (cur) { + const desc = Object.getOwnPropertyDescriptor(cur, "__proto__"); + if (desc) { + setter = desc.set; + break; + } + cur = Object.getPrototypeOf(cur); + } + } + assert_throws_js(globalOf(setter).TypeError, function() { + obj.__proto__ = newValue; + }); + + assert_equals( + Object.getPrototypeOf(obj), + originalValue, + "original value not modified" + ); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to a new value via __proto__ " + + "should throw a TypeError"); + + subsetTestByKey(this.name, test, function(t) { + var originalValue = Object.getPrototypeOf(obj); + var newValue = Object.create(null); + + t.add_cleanup(function() { + try { + Reflect.setPrototypeOf(obj, originalValue); + } catch (err) {} + }); + + assert_false(Reflect.setPrototypeOf(obj, newValue)); + + assert_equals( + Object.getPrototypeOf(obj), + originalValue, + "original value not modified" + ); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to a new value via Reflect.setPrototypeOf " + + "should return false"); + + subsetTestByKey(this.name, test, function() { + var originalValue = Object.getPrototypeOf(obj); + + Object.setPrototypeOf(obj, originalValue); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to its original value via Object.setPrototypeOf " + + "should not throw"); + + subsetTestByKey(this.name, test, function() { + var originalValue = Object.getPrototypeOf(obj); + + obj.__proto__ = originalValue; + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to its original value via __proto__ " + + "should not throw"); + + subsetTestByKey(this.name, test, function() { + var originalValue = Object.getPrototypeOf(obj); + + assert_true(Reflect.setPrototypeOf(obj, originalValue)); + }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + + "of " + type + " - setting to its original value via Reflect.setPrototypeOf " + + "should return true"); +}; + +IdlInterface.prototype.test_member_const = function(member) +{ + if (!this.has_constants()) { + throw new IdlHarnessError("Internal error: test_member_const called without any constants"); + } + + subsetTestByKey(this.name, test, function() + { + this.assert_interface_object_exists(); + + // "For each constant defined on an interface A, there must be + // a corresponding property on the interface object, if it + // exists." + assert_own_property(this.get_interface_object(), member.name); + // "The value of the property is that which is obtained by + // converting the constant’s IDL value to an ECMAScript + // value." + assert_equals(this.get_interface_object()[member.name], constValue(member.value), + "property has wrong value"); + // "The property has attributes { [[Writable]]: false, + // [[Enumerable]]: true, [[Configurable]]: false }." + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name); + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_false(desc.writable, "property should not be writable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_false(desc.configurable, "property should not be configurable"); + }.bind(this), this.name + " interface: constant " + member.name + " on interface object"); + + // "In addition, a property with the same characteristics must + // exist on the interface prototype object." + subsetTestByKey(this.name, test, function() + { + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + assert_own_property(this.get_interface_object().prototype, member.name); + assert_equals(this.get_interface_object().prototype[member.name], constValue(member.value), + "property has wrong value"); + var desc = Object.getOwnPropertyDescriptor(this.get_interface_object(), member.name); + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_false(desc.writable, "property should not be writable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_false(desc.configurable, "property should not be configurable"); + }.bind(this), this.name + " interface: constant " + member.name + " on interface prototype object"); +}; + + +IdlInterface.prototype.test_member_attribute = function(member) + { + if (!shouldRunSubTest(this.name)) { + return; + } + var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: attribute " + member.name); + a_test.step(function() + { + if (!this.should_have_interface_object()) { + a_test.done(); + return; + } + + this.assert_interface_object_exists(); + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + if (member.special === "static") { + assert_own_property(this.get_interface_object(), member.name, + "The interface object must have a property " + + format_value(member.name)); + a_test.done(); + return; + } + + this.do_member_unscopable_asserts(member); + + if (this.is_global()) { + assert_own_property(self, member.name, + "The global object must have a property " + + format_value(member.name)); + assert_false(member.name in this.get_interface_object().prototype, + "The prototype object should not have a property " + + format_value(member.name)); + + var getter = Object.getOwnPropertyDescriptor(self, member.name).get; + assert_equals(typeof(getter), "function", + format_value(member.name) + " must have a getter"); + + // Try/catch around the get here, since it can legitimately throw. + // If it does, we obviously can't check for equality with direct + // invocation of the getter. + var gotValue; + var propVal; + try { + propVal = self[member.name]; + gotValue = true; + } catch (e) { + gotValue = false; + } + if (gotValue) { + assert_equals(propVal, getter.call(undefined), + "Gets on a global should not require an explicit this"); + } + + // do_interface_attribute_asserts must be the last thing we do, + // since it will call done() on a_test. + this.do_interface_attribute_asserts(self, member, a_test); + } else { + assert_true(member.name in this.get_interface_object().prototype, + "The prototype object must have a property " + + format_value(member.name)); + + if (!member.has_extended_attribute("LegacyLenientThis")) { + if (member.idlType.generic !== "Promise") { + // this.get_interface_object() returns a thing in our global + assert_throws_js(TypeError, function() { + this.get_interface_object().prototype[member.name]; + }.bind(this), "getting property on prototype object must throw TypeError"); + // do_interface_attribute_asserts must be the last thing we + // do, since it will call done() on a_test. + this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test); + } else { + promise_rejects_js(a_test, TypeError, + this.get_interface_object().prototype[member.name]) + .then(a_test.step_func(function() { + // do_interface_attribute_asserts must be the last + // thing we do, since it will call done() on a_test. + this.do_interface_attribute_asserts(this.get_interface_object().prototype, + member, a_test); + }.bind(this))); + } + } else { + assert_equals(this.get_interface_object().prototype[member.name], undefined, + "getting property on prototype object must return undefined"); + // do_interface_attribute_asserts must be the last thing we do, + // since it will call done() on a_test. + this.do_interface_attribute_asserts(this.get_interface_object().prototype, member, a_test); + } + } + }.bind(this)); +}; + +IdlInterface.prototype.test_member_operation = function(member) +{ + if (!shouldRunSubTest(this.name)) { + return; + } + var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: operation " + member); + a_test.step(function() + { + // This function tests WebIDL as of 2015-12-29. + // https://webidl.spec.whatwg.org/#es-operations + + if (!this.should_have_interface_object()) { + a_test.done(); + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + a_test.done(); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // "For each unique identifier of an exposed operation defined on the + // interface, there must exist a corresponding property, unless the + // effective overload set for that identifier and operation and with an + // argument count of 0 has no entries." + + // TODO: Consider [Exposed]. + + // "The location of the property is determined as follows:" + var memberHolderObject; + // "* If the operation is static, then the property exists on the + // interface object." + if (member.special === "static") { + assert_own_property(this.get_interface_object(), member.name, + "interface object missing static operation"); + memberHolderObject = this.get_interface_object(); + // "* Otherwise, [...] if the interface was declared with the [Global] + // extended attribute, then the property exists + // on every object that implements the interface." + } else if (this.is_global()) { + assert_own_property(self, member.name, + "global object missing non-static operation"); + memberHolderObject = self; + // "* Otherwise, the property exists solely on the interface’s + // interface prototype object." + } else { + assert_own_property(this.get_interface_object().prototype, member.name, + "interface prototype object missing non-static operation"); + memberHolderObject = this.get_interface_object().prototype; + } + this.do_member_unscopable_asserts(member); + this.do_member_operation_asserts(memberHolderObject, member, a_test); + }.bind(this)); +}; + +IdlInterface.prototype.do_member_unscopable_asserts = function(member) +{ + // Check that if the member is unscopable then it's in the + // @@unscopables object properly. + if (!member.isUnscopable) { + return; + } + + var unscopables = this.get_interface_object().prototype[Symbol.unscopables]; + var prop = member.name; + var propDesc = Object.getOwnPropertyDescriptor(unscopables, prop); + assert_equals(typeof propDesc, "object", + this.name + '.prototype[Symbol.unscopables].' + prop + ' must exist') + assert_false("get" in propDesc, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no getter'); + assert_false("set" in propDesc, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must have no setter'); + assert_true(propDesc.writable, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must be writable'); + assert_true(propDesc.enumerable, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must be enumerable'); + assert_true(propDesc.configurable, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must be configurable'); + assert_equals(propDesc.value, true, + this.name + '.prototype[Symbol.unscopables].' + prop + ' must have the value `true`'); +}; + +IdlInterface.prototype.do_member_operation_asserts = function(memberHolderObject, member, a_test) +{ + var done = a_test.done.bind(a_test); + var operationUnforgeable = member.isUnforgeable; + var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name); + // "The property has attributes { [[Writable]]: B, + // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the + // operation is unforgeable on the interface, and true otherwise". + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_equals(desc.writable, !operationUnforgeable, + "property should be writable if and only if not unforgeable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_equals(desc.configurable, !operationUnforgeable, + "property should be configurable if and only if not unforgeable"); + // "The value of the property is a Function object whose + // behavior is as follows . . ." + assert_equals(typeof memberHolderObject[member.name], "function", + "property must be a function"); + + const operationOverloads = this.members.filter(function(m) { + return m.type == "operation" && m.name == member.name && + (m.special === "static") === (member.special === "static"); + }); + assert_equals( + memberHolderObject[member.name].length, + minOverloadLength(operationOverloads), + "property has wrong .length"); + assert_equals( + memberHolderObject[member.name].name, + member.name, + "property has wrong .name"); + + // Make some suitable arguments + var args = member.arguments.map(function(arg) { + return create_suitable_object(arg.idlType); + }); + + // "Let O be a value determined as follows: + // ". . . + // "Otherwise, throw a TypeError." + // This should be hit if the operation is not static, there is + // no [ImplicitThis] attribute, and the this value is null. + // + // TODO: We currently ignore the [ImplicitThis] case. Except we manually + // check for globals, since otherwise we'll invoke window.close(). And we + // have to skip this test for anything that on the proto chain of "self", + // since that does in fact have implicit-this behavior. + if (member.special !== "static") { + var cb; + if (!this.is_global() && + memberHolderObject[member.name] != self[member.name]) + { + cb = awaitNCallbacks(2, done); + throwOrReject(a_test, member, memberHolderObject[member.name], null, args, + "calling operation with this = null didn't throw TypeError", cb); + } else { + cb = awaitNCallbacks(1, done); + } + + // ". . . If O is not null and is also not a platform object + // that implements interface I, throw a TypeError." + // + // TODO: Test a platform object that implements some other + // interface. (Have to be sure to get inheritance right.) + throwOrReject(a_test, member, memberHolderObject[member.name], {}, args, + "calling operation with this = {} didn't throw TypeError", cb); + } else { + done(); + } +} + +IdlInterface.prototype.test_to_json_operation = function(desc, memberHolderObject, member) { + var instanceName = memberHolderObject && memberHolderObject.constructor.name + || member.name + " object"; + if (member.has_extended_attribute("Default")) { + subsetTestByKey(this.name, test, function() { + var map = this.default_to_json_operation(); + var json = memberHolderObject.toJSON(); + map.forEach(function(type, k) { + assert_true(k in json, "property " + JSON.stringify(k) + " should be present in the output of " + this.name + ".prototype.toJSON()"); + var descriptor = Object.getOwnPropertyDescriptor(json, k); + assert_true(descriptor.writable, "property " + k + " should be writable"); + assert_true(descriptor.configurable, "property " + k + " should be configurable"); + assert_true(descriptor.enumerable, "property " + k + " should be enumerable"); + this.array.assert_type_is(json[k], type); + delete json[k]; + }, this); + }.bind(this), this.name + " interface: default toJSON operation on " + desc); + } else { + subsetTestByKey(this.name, test, function() { + assert_true(this.array.is_json_type(member.idlType), JSON.stringify(member.idlType) + " is not an appropriate return value for the toJSON operation of " + instanceName); + this.array.assert_type_is(memberHolderObject.toJSON(), member.idlType); + }.bind(this), this.name + " interface: toJSON operation on " + desc); + } +}; + +IdlInterface.prototype.test_member_maplike = function(member) { + subsetTestByKey(this.name, test, () => { + const proto = this.get_interface_object().prototype; + + const methods = [ + ["entries", 0], + ["keys", 0], + ["values", 0], + ["forEach", 1], + ["get", 1], + ["has", 1] + ]; + if (!member.readonly) { + methods.push( + ["set", 2], + ["delete", 1], + ["clear", 0] + ); + } + + for (const [name, length] of methods) { + const desc = Object.getOwnPropertyDescriptor(proto, name); + assert_equals(typeof desc.value, "function", `${name} should be a function`); + assert_equals(desc.enumerable, true, `${name} enumerable`); + assert_equals(desc.configurable, true, `${name} configurable`); + assert_equals(desc.writable, true, `${name} writable`); + assert_equals(desc.value.length, length, `${name} function object length should be ${length}`); + assert_equals(desc.value.name, name, `${name} function object should have the right name`); + } + + const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator); + assert_equals(iteratorDesc.value, proto.entries, `@@iterator should equal entries`); + assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`); + assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`); + assert_equals(iteratorDesc.writable, true, `@@iterator writable`); + + const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size"); + assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`); + assert_equals(sizeDesc.set, undefined, `size should not have a setter`); + assert_equals(sizeDesc.enumerable, true, `size enumerable`); + assert_equals(sizeDesc.configurable, true, `size configurable`); + assert_equals(sizeDesc.get.length, 0, `size getter length`); + assert_equals(sizeDesc.get.name, "get size", `size getter name`); + }, `${this.name} interface: maplike<${member.idlType.map(t => t.idlType).join(", ")}>`); +}; + +IdlInterface.prototype.test_member_setlike = function(member) { + subsetTestByKey(this.name, test, () => { + const proto = this.get_interface_object().prototype; + + const methods = [ + ["entries", 0], + ["keys", 0], + ["values", 0], + ["forEach", 1], + ["has", 1] + ]; + if (!member.readonly) { + methods.push( + ["add", 1], + ["delete", 1], + ["clear", 0] + ); + } + + for (const [name, length] of methods) { + const desc = Object.getOwnPropertyDescriptor(proto, name); + assert_equals(typeof desc.value, "function", `${name} should be a function`); + assert_equals(desc.enumerable, true, `${name} enumerable`); + assert_equals(desc.configurable, true, `${name} configurable`); + assert_equals(desc.writable, true, `${name} writable`); + assert_equals(desc.value.length, length, `${name} function object length should be ${length}`); + assert_equals(desc.value.name, name, `${name} function object should have the right name`); + } + + const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator); + assert_equals(iteratorDesc.value, proto.values, `@@iterator should equal values`); + assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`); + assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`); + assert_equals(iteratorDesc.writable, true, `@@iterator writable`); + + const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size"); + assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`); + assert_equals(sizeDesc.set, undefined, `size should not have a setter`); + assert_equals(sizeDesc.enumerable, true, `size enumerable`); + assert_equals(sizeDesc.configurable, true, `size configurable`); + assert_equals(sizeDesc.get.length, 0, `size getter length`); + assert_equals(sizeDesc.get.name, "get size", `size getter name`); + }, `${this.name} interface: setlike<${member.idlType.map(t => t.idlType).join(", ")}>`); +}; + +IdlInterface.prototype.test_member_iterable = function(member) { + subsetTestByKey(this.name, test, () => { + const isPairIterator = member.idlType.length === 2; + const proto = this.get_interface_object().prototype; + + const methods = [ + ["entries", 0], + ["keys", 0], + ["values", 0], + ["forEach", 1] + ]; + + for (const [name, length] of methods) { + const desc = Object.getOwnPropertyDescriptor(proto, name); + assert_equals(typeof desc.value, "function", `${name} should be a function`); + assert_equals(desc.enumerable, true, `${name} enumerable`); + assert_equals(desc.configurable, true, `${name} configurable`); + assert_equals(desc.writable, true, `${name} writable`); + assert_equals(desc.value.length, length, `${name} function object length should be ${length}`); + assert_equals(desc.value.name, name, `${name} function object should have the right name`); + + if (!isPairIterator) { + assert_equals(desc.value, Array.prototype[name], `${name} equality with Array.prototype version`); + } + } + + const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator); + assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`); + assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`); + assert_equals(iteratorDesc.writable, true, `@@iterator writable`); + + if (isPairIterator) { + assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`); + } else { + assert_equals(iteratorDesc.value, Array.prototype[Symbol.iterator], `@@iterator equality with Array.prototype version`); + } + }, `${this.name} interface: iterable<${member.idlType.map(t => t.idlType).join(", ")}>`); +}; + +IdlInterface.prototype.test_member_async_iterable = function(member) { + subsetTestByKey(this.name, test, () => { + const isPairIterator = member.idlType.length === 2; + const proto = this.get_interface_object().prototype; + + // Note that although the spec allows arguments, which will be passed to the @@asyncIterator + // method (which is either values or entries), those arguments must always be optional. So + // length of 0 is still correct for values and entries. + const methods = [ + ["values", 0], + ]; + + if (isPairIterator) { + methods.push( + ["entries", 0], + ["keys", 0] + ); + } + + for (const [name, length] of methods) { + const desc = Object.getOwnPropertyDescriptor(proto, name); + assert_equals(typeof desc.value, "function", `${name} should be a function`); + assert_equals(desc.enumerable, true, `${name} enumerable`); + assert_equals(desc.configurable, true, `${name} configurable`); + assert_equals(desc.writable, true, `${name} writable`); + assert_equals(desc.value.length, length, `${name} function object length should be ${length}`); + assert_equals(desc.value.name, name, `${name} function object should have the right name`); + } + + const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.asyncIterator); + assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`); + assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`); + assert_equals(iteratorDesc.writable, true, `@@iterator writable`); + + if (isPairIterator) { + assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`); + } else { + assert_equals(iteratorDesc.value, proto.values, `@@iterator equality with values`); + } + }, `${this.name} interface: async iterable<${member.idlType.map(t => t.idlType).join(", ")}>`); +}; + +IdlInterface.prototype.test_member_stringifier = function(member) +{ + subsetTestByKey(this.name, test, function() + { + if (!this.should_have_interface_object()) { + return; + } + + this.assert_interface_object_exists(); + + if (this.is_callback()) { + assert_false("prototype" in this.get_interface_object(), + this.name + ' should not have a "prototype" property'); + return; + } + + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // ". . . the property exists on the interface prototype object." + var interfacePrototypeObject = this.get_interface_object().prototype; + assert_own_property(interfacePrototypeObject, "toString", + "interface prototype object missing non-static operation"); + + var stringifierUnforgeable = member.isUnforgeable; + var desc = Object.getOwnPropertyDescriptor(interfacePrototypeObject, "toString"); + // "The property has attributes { [[Writable]]: B, + // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the + // stringifier is unforgeable on the interface, and true otherwise." + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_equals(desc.writable, !stringifierUnforgeable, + "property should be writable if and only if not unforgeable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_equals(desc.configurable, !stringifierUnforgeable, + "property should be configurable if and only if not unforgeable"); + // "The value of the property is a Function object, which behaves as + // follows . . ." + assert_equals(typeof interfacePrototypeObject.toString, "function", + "property must be a function"); + // "The value of the Function object’s “length” property is the Number + // value 0." + assert_equals(interfacePrototypeObject.toString.length, 0, + "property has wrong .length"); + + // "Let O be the result of calling ToObject on the this value." + assert_throws_js(globalOf(interfacePrototypeObject.toString).TypeError, function() { + interfacePrototypeObject.toString.apply(null, []); + }, "calling stringifier with this = null didn't throw TypeError"); + + // "If O is not an object that implements the interface on which the + // stringifier was declared, then throw a TypeError." + // + // TODO: Test a platform object that implements some other + // interface. (Have to be sure to get inheritance right.) + assert_throws_js(globalOf(interfacePrototypeObject.toString).TypeError, function() { + interfacePrototypeObject.toString.apply({}, []); + }, "calling stringifier with this = {} didn't throw TypeError"); + }.bind(this), this.name + " interface: stringifier"); +}; + +IdlInterface.prototype.test_members = function() +{ + var unexposed_members = new Set(); + for (var i = 0; i < this.members.length; i++) + { + var member = this.members[i]; + if (member.untested) { + continue; + } + + if (!exposed_in(exposure_set(member, this.exposureSet))) { + if (!unexposed_members.has(member.name)) { + unexposed_members.add(member.name); + subsetTestByKey(this.name, test, function() { + // It's not exposed, so we shouldn't find it anywhere. + assert_false(member.name in this.get_interface_object(), + "The interface object must not have a property " + + format_value(member.name)); + assert_false(member.name in this.get_interface_object().prototype, + "The prototype object must not have a property " + + format_value(member.name)); + }.bind(this), this.name + " interface: member " + member.name); + } + continue; + } + + switch (member.type) { + case "const": + this.test_member_const(member); + break; + + case "attribute": + // For unforgeable attributes, we do the checks in + // test_interface_of instead. + if (!member.isUnforgeable) + { + this.test_member_attribute(member); + } + if (member.special === "stringifier") { + this.test_member_stringifier(member); + } + break; + + case "operation": + // TODO: Need to correctly handle multiple operations with the same + // identifier. + // For unforgeable operations, we do the checks in + // test_interface_of instead. + if (member.name) { + if (!member.isUnforgeable) + { + this.test_member_operation(member); + } + } else if (member.special === "stringifier") { + this.test_member_stringifier(member); + } + break; + + case "iterable": + if (member.async) { + this.test_member_async_iterable(member); + } else { + this.test_member_iterable(member); + } + break; + case "maplike": + this.test_member_maplike(member); + break; + case "setlike": + this.test_member_setlike(member); + break; + default: + // TODO: check more member types. + break; + } + } +}; + +IdlInterface.prototype.test_object = function(desc) +{ + var obj, exception = null; + try + { + obj = eval(desc); + } + catch(e) + { + exception = e; + } + + var expected_typeof; + if (this.name == "HTMLAllCollection") + { + // Result of [[IsHTMLDDA]] slot + expected_typeof = "undefined"; + } + else + { + expected_typeof = "object"; + } + + if (this.is_callback()) { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + } else { + this.test_primary_interface_of(desc, obj, exception, expected_typeof); + + var current_interface = this; + while (current_interface) + { + if (!(current_interface.name in this.array.members)) + { + throw new IdlHarnessError("Interface " + current_interface.name + " not found (inherited by " + this.name + ")"); + } + if (current_interface.prevent_multiple_testing && current_interface.already_tested) + { + return; + } + current_interface.test_interface_of(desc, obj, exception, expected_typeof); + current_interface = this.array.members[current_interface.base]; + } + } +}; + +IdlInterface.prototype.test_primary_interface_of = function(desc, obj, exception, expected_typeof) +{ + // Only the object itself, not its members, are tested here, so if the + // interface is untested, there is nothing to do. + if (this.untested) + { + return; + } + + // "The internal [[SetPrototypeOf]] method of every platform object that + // implements an interface with the [Global] extended + // attribute must execute the same algorithm as is defined for the + // [[SetPrototypeOf]] internal method of an immutable prototype exotic + // object." + // https://webidl.spec.whatwg.org/#platform-object-setprototypeof + if (this.is_global()) + { + this.test_immutable_prototype("global platform object", obj); + } + + + // We can't easily test that its prototype is correct if there's no + // interface object, or the object is from a different global environment + // (not instanceof Object). TODO: test in this case that its prototype at + // least looks correct, even if we can't test that it's actually correct. + if (this.should_have_interface_object() + && (typeof obj != expected_typeof || obj instanceof Object)) + { + subsetTestByKey(this.name, test, function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + this.assert_interface_object_exists(); + assert_own_property(this.get_interface_object(), "prototype", + 'interface "' + this.name + '" does not have own property "prototype"'); + + // "The value of the internal [[Prototype]] property of the + // platform object is the interface prototype object of the primary + // interface from the platform object’s associated global + // environment." + assert_equals(Object.getPrototypeOf(obj), + this.get_interface_object().prototype, + desc + "'s prototype is not " + this.name + ".prototype"); + }.bind(this), this.name + " must be primary interface of " + desc); + } + + // "The class string of a platform object that implements one or more + // interfaces must be the qualified name of the primary interface of the + // platform object." + subsetTestByKey(this.name, test, function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + assert_class_string(obj, this.get_qualified_name(), "class string of " + desc); + if (!this.has_stringifier()) + { + assert_equals(String(obj), "[object " + this.get_qualified_name() + "]", "String(" + desc + ")"); + } + }.bind(this), "Stringification of " + desc); +}; + +IdlInterface.prototype.test_interface_of = function(desc, obj, exception, expected_typeof) +{ + // TODO: Indexed and named properties, more checks on interface members + this.already_tested = true; + if (!shouldRunSubTest(this.name)) { + return; + } + + var unexposed_properties = new Set(); + for (var i = 0; i < this.members.length; i++) + { + var member = this.members[i]; + if (member.untested) { + continue; + } + if (!exposed_in(exposure_set(member, this.exposureSet))) + { + if (!unexposed_properties.has(member.name)) + { + unexposed_properties.add(member.name); + subsetTestByKey(this.name, test, function() { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_false(member.name in obj); + }.bind(this), this.name + " interface: " + desc + ' must not have property "' + member.name + '"'); + } + continue; + } + if (member.type == "attribute" && member.isUnforgeable) + { + var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"'); + a_test.step(function() { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + // Call do_interface_attribute_asserts last, since it will call a_test.done() + this.do_interface_attribute_asserts(obj, member, a_test); + }.bind(this)); + } + else if (member.type == "operation" && + member.name && + member.isUnforgeable) + { + var a_test = subsetTestByKey(this.name, async_test, this.name + " interface: " + desc + ' must have own property "' + member.name + '"'); + a_test.step(function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + assert_own_property(obj, member.name, + "Doesn't have the unforgeable operation property"); + this.do_member_operation_asserts(obj, member, a_test); + }.bind(this)); + } + else if ((member.type == "const" + || member.type == "attribute" + || member.type == "operation") + && member.name) + { + subsetTestByKey(this.name, test, function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + if (member.special !== "static") { + if (!this.is_global()) { + assert_inherits(obj, member.name); + } else { + assert_own_property(obj, member.name); + } + + if (member.type == "const") + { + assert_equals(obj[member.name], constValue(member.value)); + } + if (member.type == "attribute") + { + // Attributes are accessor properties, so they might + // legitimately throw an exception rather than returning + // anything. + var property, thrown = false; + try + { + property = obj[member.name]; + } + catch (e) + { + thrown = true; + } + if (!thrown) + { + if (this.name == "Document" && member.name == "all") + { + // Result of [[IsHTMLDDA]] slot + assert_equals(typeof property, "undefined"); + } + else + { + this.array.assert_type_is(property, member.idlType); + } + } + } + if (member.type == "operation") + { + assert_equals(typeof obj[member.name], "function"); + } + } + }.bind(this), this.name + " interface: " + desc + ' must inherit property "' + member + '" with the proper type'); + } + // TODO: This is wrong if there are multiple operations with the same + // identifier. + // TODO: Test passing arguments of the wrong type. + if (member.type == "operation" && member.name && member.arguments.length) + { + var description = + this.name + " interface: calling " + member + " on " + desc + + " with too few arguments must throw TypeError"; + var a_test = subsetTestByKey(this.name, async_test, description); + a_test.step(function() + { + assert_equals(exception, null, "Unexpected exception when evaluating object"); + assert_equals(typeof obj, expected_typeof, "wrong typeof object"); + var fn; + if (member.special !== "static") { + if (!this.is_global() && !member.isUnforgeable) { + assert_inherits(obj, member.name); + } else { + assert_own_property(obj, member.name); + } + fn = obj[member.name]; + } + else + { + assert_own_property(obj.constructor, member.name, "interface object must have static operation as own property"); + fn = obj.constructor[member.name]; + } + + var minLength = minOverloadLength(this.members.filter(function(m) { + return m.type == "operation" && m.name == member.name; + })); + var args = []; + var cb = awaitNCallbacks(minLength, a_test.done.bind(a_test)); + for (var i = 0; i < minLength; i++) { + throwOrReject(a_test, member, fn, obj, args, "Called with " + i + " arguments", cb); + + args.push(create_suitable_object(member.arguments[i].idlType)); + } + if (minLength === 0) { + cb(); + } + }.bind(this)); + } + + if (member.is_to_json_regular_operation()) { + this.test_to_json_operation(desc, obj, member); + } + } +}; + +IdlInterface.prototype.has_stringifier = function() +{ + if (this.name === "DOMException") { + // toString is inherited from Error, so don't assume we have the + // default stringifer + return true; + } + if (this.members.some(function(member) { return member.special === "stringifier"; })) { + return true; + } + if (this.base && + this.array.members[this.base].has_stringifier()) { + return true; + } + return false; +}; + +IdlInterface.prototype.do_interface_attribute_asserts = function(obj, member, a_test) +{ + // This function tests WebIDL as of 2015-01-27. + // TODO: Consider [Exposed]. + + // This is called by test_member_attribute() with the prototype as obj if + // it is not a global, and the global otherwise, and by test_interface_of() + // with the object as obj. + + var pendingPromises = []; + + // "The name of the property is the identifier of the attribute." + assert_own_property(obj, member.name); + + // "The property has attributes { [[Get]]: G, [[Set]]: S, [[Enumerable]]: + // true, [[Configurable]]: configurable }, where: + // "configurable is false if the attribute was declared with the + // [LegacyUnforgeable] extended attribute and true otherwise; + // "G is the attribute getter, defined below; and + // "S is the attribute setter, also defined below." + var desc = Object.getOwnPropertyDescriptor(obj, member.name); + assert_false("value" in desc, 'property descriptor should not have a "value" field'); + assert_false("writable" in desc, 'property descriptor should not have a "writable" field'); + assert_true(desc.enumerable, "property should be enumerable"); + if (member.isUnforgeable) + { + assert_false(desc.configurable, "[LegacyUnforgeable] property must not be configurable"); + } + else + { + assert_true(desc.configurable, "property must be configurable"); + } + + + // "The attribute getter is a Function object whose behavior when invoked + // is as follows:" + assert_equals(typeof desc.get, "function", "getter must be Function"); + + // "If the attribute is a regular attribute, then:" + if (member.special !== "static") { + // "If O is not a platform object that implements I, then: + // "If the attribute was specified with the [LegacyLenientThis] extended + // attribute, then return undefined. + // "Otherwise, throw a TypeError." + if (!member.has_extended_attribute("LegacyLenientThis")) { + if (member.idlType.generic !== "Promise") { + assert_throws_js(globalOf(desc.get).TypeError, function() { + desc.get.call({}); + }.bind(this), "calling getter on wrong object type must throw TypeError"); + } else { + pendingPromises.push( + promise_rejects_js(a_test, TypeError, desc.get.call({}), + "calling getter on wrong object type must reject the return promise with TypeError")); + } + } else { + assert_equals(desc.get.call({}), undefined, + "calling getter on wrong object type must return undefined"); + } + } + + // "The value of the Function object’s “length” property is the Number + // value 0." + assert_equals(desc.get.length, 0, "getter length must be 0"); + + // "Let name be the string "get " prepended to attribute’s identifier." + // "Perform ! SetFunctionName(F, name)." + assert_equals(desc.get.name, "get " + member.name, + "getter must have the name 'get " + member.name + "'"); + + + // TODO: Test calling setter on the interface prototype (should throw + // TypeError in most cases). + if (member.readonly + && !member.has_extended_attribute("LegacyLenientSetter") + && !member.has_extended_attribute("PutForwards") + && !member.has_extended_attribute("Replaceable")) + { + // "The attribute setter is undefined if the attribute is declared + // readonly and has neither a [PutForwards] nor a [Replaceable] + // extended attribute declared on it." + assert_equals(desc.set, undefined, "setter must be undefined for readonly attributes"); + } + else + { + // "Otherwise, it is a Function object whose behavior when + // invoked is as follows:" + assert_equals(typeof desc.set, "function", "setter must be function for PutForwards, Replaceable, or non-readonly attributes"); + + // "If the attribute is a regular attribute, then:" + if (member.special !== "static") { + // "If /validThis/ is false and the attribute was not specified + // with the [LegacyLenientThis] extended attribute, then throw a + // TypeError." + // "If the attribute is declared with a [Replaceable] extended + // attribute, then: ..." + // "If validThis is false, then return." + if (!member.has_extended_attribute("LegacyLenientThis")) { + assert_throws_js(globalOf(desc.set).TypeError, function() { + desc.set.call({}); + }.bind(this), "calling setter on wrong object type must throw TypeError"); + } else { + assert_equals(desc.set.call({}), undefined, + "calling setter on wrong object type must return undefined"); + } + } + + // "The value of the Function object’s “length” property is the Number + // value 1." + assert_equals(desc.set.length, 1, "setter length must be 1"); + + // "Let name be the string "set " prepended to id." + // "Perform ! SetFunctionName(F, name)." + assert_equals(desc.set.name, "set " + member.name, + "The attribute setter must have the name 'set " + member.name + "'"); + } + + Promise.all(pendingPromises).then(a_test.done.bind(a_test)); +} + +/// IdlInterfaceMember /// +function IdlInterfaceMember(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "ifMember" production. + * We just forward all properties to this object without modification, + * except for special extAttrs handling. + */ + for (var k in obj.toJSON()) + { + this[k] = obj[k]; + } + if (!("extAttrs" in this)) + { + this.extAttrs = []; + } + + this.isUnforgeable = this.has_extended_attribute("LegacyUnforgeable"); + this.isUnscopable = this.has_extended_attribute("Unscopable"); +} + +IdlInterfaceMember.prototype = Object.create(IdlObject.prototype); + +IdlInterfaceMember.prototype.toJSON = function() { + return this; +}; + +IdlInterfaceMember.prototype.is_to_json_regular_operation = function() { + return this.type == "operation" && this.special !== "static" && this.name == "toJSON"; +}; + +IdlInterfaceMember.prototype.toString = function() { + function formatType(type) { + var result; + if (type.generic) { + result = type.generic + "<" + type.idlType.map(formatType).join(", ") + ">"; + } else if (type.union) { + result = "(" + type.subtype.map(formatType).join(" or ") + ")"; + } else { + result = type.idlType; + } + if (type.nullable) { + result += "?" + } + return result; + } + + if (this.type === "operation") { + var args = this.arguments.map(function(m) { + return [ + m.optional ? "optional " : "", + formatType(m.idlType), + m.variadic ? "..." : "", + ].join(""); + }).join(", "); + return this.name + "(" + args + ")"; + } + + return this.name; +} + +/// Internal helper functions /// +function create_suitable_object(type) +{ + /** + * type is an object produced by the WebIDLParser.js "type" production. We + * return a JavaScript value that matches the type, if we can figure out + * how. + */ + if (type.nullable) + { + return null; + } + switch (type.idlType) + { + case "any": + case "boolean": + return true; + + case "byte": case "octet": case "short": case "unsigned short": + case "long": case "unsigned long": case "long long": + case "unsigned long long": case "float": case "double": + case "unrestricted float": case "unrestricted double": + return 7; + + case "DOMString": + case "ByteString": + case "USVString": + return "foo"; + + case "object": + return {a: "b"}; + + case "Node": + return document.createTextNode("abc"); + } + return null; +} + +/// IdlEnum /// +// Used for IdlArray.prototype.assert_type_is +function IdlEnum(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "dictionary" + * production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** An array of values produced by the "enum" production. */ + this.values = obj.values; + +} + +IdlEnum.prototype = Object.create(IdlObject.prototype); + +/// IdlCallback /// +// Used for IdlArray.prototype.assert_type_is +function IdlCallback(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "callback" + * production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** Arguments for the callback. */ + this.arguments = obj.arguments; +} + +IdlCallback.prototype = Object.create(IdlObject.prototype); + +/// IdlTypedef /// +// Used for IdlArray.prototype.assert_type_is +function IdlTypedef(obj) +{ + /** + * obj is an object produced by the WebIDLParser.js "typedef" + * production. + */ + + /** Self-explanatory. */ + this.name = obj.name; + + /** The idlType that we are supposed to be typedeffing to. */ + this.idlType = obj.idlType; + +} + +IdlTypedef.prototype = Object.create(IdlObject.prototype); + +/// IdlNamespace /// +function IdlNamespace(obj) +{ + this.name = obj.name; + this.extAttrs = obj.extAttrs; + this.untested = obj.untested; + /** A back-reference to our IdlArray. */ + this.array = obj.array; + + /** An array of IdlInterfaceMembers. */ + this.members = obj.members.map(m => new IdlInterfaceMember(m)); +} + +IdlNamespace.prototype = Object.create(IdlObject.prototype); + +IdlNamespace.prototype.do_member_operation_asserts = function (memberHolderObject, member, a_test) +{ + var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name); + + assert_false("get" in desc, "property should not have a getter"); + assert_false("set" in desc, "property should not have a setter"); + assert_equals( + desc.writable, + !member.isUnforgeable, + "property should be writable if and only if not unforgeable"); + assert_true(desc.enumerable, "property should be enumerable"); + assert_equals( + desc.configurable, + !member.isUnforgeable, + "property should be configurable if and only if not unforgeable"); + + assert_equals( + typeof memberHolderObject[member.name], + "function", + "property must be a function"); + + assert_equals( + memberHolderObject[member.name].length, + minOverloadLength(this.members.filter(function(m) { + return m.type == "operation" && m.name == member.name; + })), + "operation has wrong .length"); + a_test.done(); +} + +IdlNamespace.prototype.test_member_operation = function(member) +{ + if (!shouldRunSubTest(this.name)) { + return; + } + var a_test = subsetTestByKey( + this.name, + async_test, + this.name + ' namespace: operation ' + member); + a_test.step(function() { + assert_own_property( + self[this.name], + member.name, + 'namespace object missing operation ' + format_value(member.name)); + + this.do_member_operation_asserts(self[this.name], member, a_test); + }.bind(this)); +}; + +IdlNamespace.prototype.test_member_attribute = function (member) +{ + if (!shouldRunSubTest(this.name)) { + return; + } + var a_test = subsetTestByKey( + this.name, + async_test, + this.name + ' namespace: attribute ' + member.name); + a_test.step(function() + { + assert_own_property( + self[this.name], + member.name, + this.name + ' does not have property ' + format_value(member.name)); + + var desc = Object.getOwnPropertyDescriptor(self[this.name], member.name); + assert_equals(desc.set, undefined, "setter must be undefined for namespace members"); + a_test.done(); + }.bind(this)); +}; + +IdlNamespace.prototype.test_self = function () +{ + /** + * TODO(lukebjerring): Assert: + * - "Note that unlike interfaces or dictionaries, namespaces do not create types." + */ + + subsetTestByKey(this.name, test, () => { + assert_true(this.extAttrs.every(o => o.name === "Exposed" || o.name === "SecureContext"), + "Only the [Exposed] and [SecureContext] extended attributes are applicable to namespaces"); + assert_true(this.has_extended_attribute("Exposed"), + "Namespaces must be annotated with the [Exposed] extended attribute"); + }, `${this.name} namespace: extended attributes`); + + const namespaceObject = self[this.name]; + + subsetTestByKey(this.name, test, () => { + const desc = Object.getOwnPropertyDescriptor(self, this.name); + assert_equals(desc.value, namespaceObject, `wrong value for ${this.name} namespace object`); + assert_true(desc.writable, "namespace object should be writable"); + assert_false(desc.enumerable, "namespace object should not be enumerable"); + assert_true(desc.configurable, "namespace object should be configurable"); + assert_false("get" in desc, "namespace object should not have a getter"); + assert_false("set" in desc, "namespace object should not have a setter"); + }, `${this.name} namespace: property descriptor`); + + subsetTestByKey(this.name, test, () => { + assert_true(Object.isExtensible(namespaceObject)); + }, `${this.name} namespace: [[Extensible]] is true`); + + subsetTestByKey(this.name, test, () => { + assert_true(namespaceObject instanceof Object); + + if (this.name === "console") { + // https://console.spec.whatwg.org/#console-namespace + const namespacePrototype = Object.getPrototypeOf(namespaceObject); + assert_equals(Reflect.ownKeys(namespacePrototype).length, 0); + assert_equals(Object.getPrototypeOf(namespacePrototype), Object.prototype); + } else { + assert_equals(Object.getPrototypeOf(namespaceObject), Object.prototype); + } + }, `${this.name} namespace: [[Prototype]] is Object.prototype`); + + subsetTestByKey(this.name, test, () => { + assert_equals(typeof namespaceObject, "object"); + }, `${this.name} namespace: typeof is "object"`); + + subsetTestByKey(this.name, test, () => { + assert_equals( + Object.getOwnPropertyDescriptor(namespaceObject, "length"), + undefined, + "length property must be undefined" + ); + }, `${this.name} namespace: has no length property`); + + subsetTestByKey(this.name, test, () => { + assert_equals( + Object.getOwnPropertyDescriptor(namespaceObject, "name"), + undefined, + "name property must be undefined" + ); + }, `${this.name} namespace: has no name property`); +}; + +IdlNamespace.prototype.test = function () +{ + if (!this.untested) { + this.test_self(); + } + + for (const v of Object.values(this.members)) { + switch (v.type) { + + case 'operation': + this.test_member_operation(v); + break; + + case 'attribute': + this.test_member_attribute(v); + break; + + default: + throw 'Invalid namespace member ' + v.name + ': ' + v.type + ' not supported'; + } + }; +}; + +}()); + +/** + * idl_test is a promise_test wrapper that handles the fetching of the IDL, + * avoiding repetitive boilerplate. + * + * @param {String[]} srcs Spec name(s) for source idl files (fetched from + * /interfaces/{name}.idl). + * @param {String[]} deps Spec name(s) for dependency idl files (fetched + * from /interfaces/{name}.idl). Order is important - dependencies from + * each source will only be included if they're already know to be a + * dependency (i.e. have already been seen). + * @param {Function} setup_func Function for extra setup of the idl_array, such + * as adding objects. Do not call idl_array.test() in the setup; it is + * called by this function (idl_test). + */ +function idl_test(srcs, deps, idl_setup_func) { + return promise_test(function (t) { + var idl_array = new IdlArray(); + var setup_error = null; + const validationIgnored = [ + "constructor-member", + "dict-arg-default", + "require-exposed" + ]; + return Promise.all( + srcs.concat(deps).map(fetch_spec)) + .then(function(results) { + const astArray = results.map(result => + WebIDL2.parse(result.idl, { sourceName: result.spec }) + ); + test(() => { + const validations = WebIDL2.validate(astArray) + .filter(v => !validationIgnored.includes(v.ruleName)); + if (validations.length) { + const message = validations.map(v => v.message).join("\n\n"); + throw new Error(message); + } + }, "idl_test validation"); + for (var i = 0; i < srcs.length; i++) { + idl_array.internal_add_idls(astArray[i]); + } + for (var i = srcs.length; i < srcs.length + deps.length; i++) { + idl_array.internal_add_dependency_idls(astArray[i]); + } + }) + .then(function() { + if (idl_setup_func) { + return idl_setup_func(idl_array, t); + } + }) + .catch(function(e) { setup_error = e || 'IDL setup failed.'; }) + .then(function () { + var error = setup_error; + try { + idl_array.test(); // Test what we can. + } catch (e) { + // If testing fails hard here, the original setup error + // is more likely to be the real cause. + error = error || e; + } + if (error) { + throw error; + } + }); + }, 'idl_test setup'); +} + +/** + * fetch_spec is a shorthand for a Promise that fetches the spec's content. + */ +function fetch_spec(spec) { + var url = '/interfaces/' + spec + '.idl'; + return fetch(url).then(function (r) { + if (!r.ok) { + throw new IdlHarnessError("Error fetching " + url + "."); + } + return r.text(); + }).then(idl => ({ spec, idl })); +} +// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker: diff --git a/testing/web-platform/tests/resources/idlharness.js.headers b/testing/web-platform/tests/resources/idlharness.js.headers new file mode 100644 index 0000000000..5e8f640c66 --- /dev/null +++ b/testing/web-platform/tests/resources/idlharness.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript; charset=utf-8 +Cache-Control: max-age=3600 diff --git a/testing/web-platform/tests/resources/readme.md b/testing/web-platform/tests/resources/readme.md new file mode 100644 index 0000000000..09a62fbd7b --- /dev/null +++ b/testing/web-platform/tests/resources/readme.md @@ -0,0 +1,14 @@ +# Resources + +This directory contains utilities intended for use by tests and maintained as project infrastructure. +It does not contain tests. + +## `testharness.js` + +`testharness.js` is a framework for writing low-level tests of +browser functionality in javascript. It provides a convenient API for +making assertions and is intended to work for both simple synchronous +tests, and tests of asynchronous behaviour. + +Complete documentation is available in the `docs/` directory of this repository +and on the web at https://web-platform-tests.org/writing-tests/. diff --git a/testing/web-platform/tests/resources/sriharness.js b/testing/web-platform/tests/resources/sriharness.js new file mode 100644 index 0000000000..943d677224 --- /dev/null +++ b/testing/web-platform/tests/resources/sriharness.js @@ -0,0 +1,226 @@ +// `integrityValue` indicates the 'integrity' attribute value at the time of +// #prepare-a-script. +// +// `integrityValueAfterPrepare` indicates how the 'integrity' attribute value +// is modified after #prepare-a-script: +// - `undefined` => not modified. +// - `null` => 'integrity' attribute is removed. +// - others => 'integrity' attribute value is set to that value. +// +// TODO: Make the arguments a dictionary for readability in the test files. +var SRIScriptTest = function(pass, name, src, integrityValue, crossoriginValue, nonce, integrityValueAfterPrepare) { + this.pass = pass; + this.name = "Script: " + name; + this.src = src; + this.integrityValue = integrityValue; + this.crossoriginValue = crossoriginValue; + this.nonce = nonce; + this.integrityValueAfterPrepare = integrityValueAfterPrepare; +} + +SRIScriptTest.prototype.execute = function() { + var test = async_test(this.name); + var e = document.createElement("script"); + e.src = this.src; + if (this.integrityValue) { + e.setAttribute("integrity", this.integrityValue); + } + if(this.crossoriginValue) { + e.setAttribute("crossorigin", this.crossoriginValue); + } + if(this.nonce) { + e.setAttribute("nonce", this.nonce); + } + if(this.pass) { + e.addEventListener("load", function() {test.done()}); + e.addEventListener("error", function() { + test.step(function(){ assert_unreached("Good load fired error handler.") }) + }); + } else { + e.addEventListener("load", function() { + test.step(function() { assert_unreached("Bad load succeeded.") }) + }); + e.addEventListener("error", function() {test.done()}); + } + document.body.appendChild(e); + + if (this.integrityValueAfterPrepare === null) { + e.removeAttribute("integrity"); + } else if (this.integrityValueAfterPrepare !== undefined) { + e.setAttribute("integrity", this.integrityValueAfterPrepare); + } +}; + +function set_extra_attributes(element, attrs) { + // Apply the rest of the attributes, if any. + for (const [attr_name, attr_val] of Object.entries(attrs)) { + element[attr_name] = attr_val; + } +} + +function buildElementFromDestination(resource_url, destination, attrs) { + // Assert: |destination| is a valid destination. + let element; + + // The below switch is responsible for: + // 1. Creating the correct subresource element + // 2. Setting said element's href, src, or fetch-instigating property + // appropriately. + switch (destination) { + case "script": + element = document.createElement(destination); + set_extra_attributes(element, attrs); + element.src = resource_url; + break; + case "style": + element = document.createElement('link'); + set_extra_attributes(element, attrs); + element.rel = 'stylesheet'; + element.href = resource_url; + break; + case "image": + element = document.createElement('img'); + set_extra_attributes(element, attrs); + element.src = resource_url; + break; + default: + assert_unreached("INVALID DESTINATION"); + } + + return element; +} + +// When using SRIPreloadTest, also include /preload/resources/preload_helper.js +// |number_of_requests| is used to ensure that preload requests are actually +// reused as expected. +const SRIPreloadTest = (preload_sri_success, subresource_sri_success, name, + number_of_requests, destination, resource_url, + link_attrs, subresource_attrs) => { + const test = async_test(name); + const link = document.createElement('link'); + + // Early-fail in UAs that do not support `preload` links. + test.step_func(() => { + assert_true(link.relList.supports('preload'), + "This test is automatically failing because the browser does not" + + "support `preload` links."); + })(); + + // Build up the link. + link.rel = 'preload'; + link.as = destination; + link.href = resource_url; + for (const [attr_name, attr_val] of Object.entries(link_attrs)) { + link[attr_name] = attr_val; // This may override `rel` to modulepreload. + } + + // Preload + subresource success and failure loading functions. + const valid_preload_failed = test.step_func(() => + { assert_unreached("Valid preload fired error handler.") }); + const invalid_preload_succeeded = test.step_func(() => + { assert_unreached("Invalid preload load succeeded.") }); + const valid_subresource_failed = test.step_func(() => + { assert_unreached("Valid subresource fired error handler.") }); + const invalid_subresource_succeeded = test.step_func(() => + { assert_unreached("Invalid subresource load succeeded.") }); + const subresource_pass = test.step_func(() => { + verifyNumberOfResourceTimingEntries(resource_url, number_of_requests); + test.done(); + }); + const preload_pass = test.step_func(() => { + const subresource_element = buildElementFromDestination( + resource_url, + destination, + subresource_attrs + ); + + if (subresource_sri_success) { + subresource_element.onload = subresource_pass; + subresource_element.onerror = valid_subresource_failed; + } else { + subresource_element.onload = invalid_subresource_succeeded; + subresource_element.onerror = subresource_pass; + } + + document.body.append(subresource_element); + }); + + if (preload_sri_success) { + link.onload = preload_pass; + link.onerror = valid_preload_failed; + } else { + link.onload = invalid_preload_succeeded; + link.onerror = preload_pass; + } + + document.head.append(link); +} + +// <link> tests +// Style tests must be done synchronously because they rely on the presence +// and absence of global style, which can affect later tests. Thus, instead +// of executing them one at a time, the style tests are implemented as a +// queue that builds up a list of tests, and then executes them one at a +// time. +var SRIStyleTest = function(queue, pass, name, attrs, customCallback, altPassValue) { + this.pass = pass; + this.name = "Style: " + name; + this.customCallback = customCallback || function () {}; + this.attrs = attrs || {}; + this.passValue = altPassValue || "rgb(255, 255, 0)"; + + this.test = async_test(this.name); + + this.queue = queue; + this.queue.push(this); +} + +SRIStyleTest.prototype.execute = function() { + var that = this; + var container = document.getElementById("container"); + while (container.hasChildNodes()) { + container.removeChild(container.firstChild); + } + + var test = this.test; + + var div = document.createElement("div"); + div.className = "testdiv"; + var e = document.createElement("link"); + + // The link relation is guaranteed to not be "preload" or "modulepreload". + this.attrs.rel = this.attrs.rel || "stylesheet"; + for (var key in this.attrs) { + if (this.attrs.hasOwnProperty(key)) { + e.setAttribute(key, this.attrs[key]); + } + } + + if(this.pass) { + e.addEventListener("load", function() { + test.step(function() { + var background = window.getComputedStyle(div, null).getPropertyValue("background-color"); + assert_equals(background, that.passValue); + test.done(); + }); + }); + e.addEventListener("error", function() { + test.step(function(){ assert_unreached("Good load fired error handler.") }) + }); + } else { + e.addEventListener("load", function() { + test.step(function() { assert_unreached("Bad load succeeded.") }) + }); + e.addEventListener("error", function() { + test.step(function() { + var background = window.getComputedStyle(div, null).getPropertyValue("background-color"); + assert_not_equals(background, that.passValue); + test.done(); + }); + }); + } + container.appendChild(div); + container.appendChild(e); + this.customCallback(e, container); +}; + diff --git a/testing/web-platform/tests/resources/test-only-api.js b/testing/web-platform/tests/resources/test-only-api.js new file mode 100644 index 0000000000..a66eb44ede --- /dev/null +++ b/testing/web-platform/tests/resources/test-only-api.js @@ -0,0 +1,31 @@ +'use strict'; + +/* Whether the browser is Chromium-based with MojoJS enabled */ +const isChromiumBased = 'MojoInterfaceInterceptor' in self; +/* Whether the browser is WebKit-based with internal test-only API enabled */ +const isWebKitBased = !isChromiumBased && 'internals' in self; + +/** + * Loads a script in a window or worker. + * + * @param {string} path - A script path + * @returns {Promise} + */ +function loadScript(path) { + if (typeof document === 'undefined') { + // Workers (importScripts is synchronous and may throw.) + importScripts(path); + return Promise.resolve(); + } else { + // Window + const script = document.createElement('script'); + script.src = path; + script.async = false; + const p = new Promise((resolve, reject) => { + script.onload = () => { resolve(); }; + script.onerror = e => { reject(`Error loading ${path}`); }; + }) + document.head.appendChild(script); + return p; + } +} diff --git a/testing/web-platform/tests/resources/test-only-api.js.headers b/testing/web-platform/tests/resources/test-only-api.js.headers new file mode 100644 index 0000000000..5e8f640c66 --- /dev/null +++ b/testing/web-platform/tests/resources/test-only-api.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript; charset=utf-8 +Cache-Control: max-age=3600 diff --git a/testing/web-platform/tests/resources/test-only-api.m.js b/testing/web-platform/tests/resources/test-only-api.m.js new file mode 100644 index 0000000000..984f635aba --- /dev/null +++ b/testing/web-platform/tests/resources/test-only-api.m.js @@ -0,0 +1,5 @@ +/* Whether the browser is Chromium-based with MojoJS enabled */ +export const isChromiumBased = 'MojoInterfaceInterceptor' in self; + +/* Whether the browser is WebKit-based with internal test-only API enabled */ +export const isWebKitBased = !isChromiumBased && 'internals' in self; diff --git a/testing/web-platform/tests/resources/test-only-api.m.js.headers b/testing/web-platform/tests/resources/test-only-api.m.js.headers new file mode 100644 index 0000000000..5e8f640c66 --- /dev/null +++ b/testing/web-platform/tests/resources/test-only-api.m.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript; charset=utf-8 +Cache-Control: max-age=3600 diff --git a/testing/web-platform/tests/resources/test/README.md b/testing/web-platform/tests/resources/test/README.md new file mode 100644 index 0000000000..edc03ef214 --- /dev/null +++ b/testing/web-platform/tests/resources/test/README.md @@ -0,0 +1,83 @@ +# `testharness.js` test suite + +The test suite for the `testharness.js` testing framework. + +## Executing Tests + +Install the following dependencies: + +- [Python 2.7.9+](https://www.python.org/) +- [the tox Python package](https://tox.readthedocs.io/en/latest/) +- [the Mozilla Firefox web browser](https://mozilla.org/firefox) +- [the GeckoDriver server](https://github.com/mozilla/geckodriver) + +Make sure `geckodriver` can be found in your `PATH`. + +Currently, the tests should be run with the latest *Firefox Nightly*. In order to +specify the path to Firefox Nightly, use the following command-line option: + + tox -- --binary=/path/to/FirefoxNightly + +### Automated Script + +Alternatively, you may run `tools/ci/ci_resources_unittest.sh`, which only depends on +Python 2. The script will install other dependencies automatically and start `tox` with +the correct arguments. + +## Authoring Tests + +Test cases are expressed as `.html` files located within the `tests/unit/` and +`tests/functional/` sub-directories. Each test should include the +`testharness.js` library with the following markup: + + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + +This should be followed by one or more `<script>` tags that interface with the +`testharness.js` API in some way. For example: + + <script> + test(function() { + 1 = 1; + }, 'This test is expected to fail.'); + </script> + +### Unit tests + +The "unit test" type allows for concisely testing the expected behavior of +assertion methods. These tests may define any number of sub-tests; the +acceptance criteria is simply that all tests executed pass. + +### Functional tests + +Thoroughly testing the behavior of the harness itself requires ensuring a +number of considerations which cannot be verified with the "unit testing" +strategy. These include: + +- Ensuring that some tests are not run +- Ensuring conditions that cause test failures +- Ensuring conditions that cause harness errors + +Functional tests allow for these details to be verified. Every functional test +must include a summary of the expected results as a JSON string within a +`<script>` tag with an `id` of `"expected"`, e.g.: + + <script type="text/json" id="expected"> + { + "summarized_status": { + "message": null, + "stack": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "message": "ReferenceError: invalid assignment left-hand side", + "name": "Sample HTML5 API Tests", + "properties": {}, + "stack": "(implementation-defined)", + "status_string": "FAIL" + } + ], + "type": "complete" + } + </script> diff --git a/testing/web-platform/tests/resources/test/conftest.py b/testing/web-platform/tests/resources/test/conftest.py new file mode 100644 index 0000000000..7253cac9ac --- /dev/null +++ b/testing/web-platform/tests/resources/test/conftest.py @@ -0,0 +1,269 @@ +import copy +import json +import os +import ssl +import sys +import subprocess +import urllib + +import html5lib +import py +import pytest + +from wptserver import WPTServer + +HERE = os.path.dirname(os.path.abspath(__file__)) +WPT_ROOT = os.path.normpath(os.path.join(HERE, '..', '..')) +HARNESS = os.path.join(HERE, 'harness.html') +TEST_TYPES = ('functional', 'unit') + +sys.path.insert(0, os.path.normpath(os.path.join(WPT_ROOT, "tools"))) +import localpaths + +sys.path.insert(0, os.path.normpath(os.path.join(WPT_ROOT, "tools", "webdriver"))) +import webdriver + + +def pytest_addoption(parser): + parser.addoption("--binary", action="store", default=None, help="path to browser binary") + parser.addoption("--headless", action="store_true", default=False, help="run browser in headless mode") + + +def pytest_collect_file(file_path, path, parent): + if file_path.suffix.lower() != '.html': + return + + # Tests are organized in directories by type + test_type = os.path.relpath(str(file_path), HERE) + if os.path.sep not in test_type or ".." in test_type: + # HTML files in this directory are not tests + return + test_type = test_type.split(os.path.sep)[1] + + # Handle the deprecation of Node construction in pytest6 + # https://docs.pytest.org/en/stable/deprecations.html#node-construction-changed-to-node-from-parent + if hasattr(HTMLItem, "from_parent"): + return HTMLItem.from_parent(parent, filename=str(file_path), test_type=test_type) + return HTMLItem(parent, str(file_path), test_type) + + +def pytest_configure(config): + config.proc = subprocess.Popen(["geckodriver"]) + config.add_cleanup(config.proc.kill) + + capabilities = {"alwaysMatch": {"acceptInsecureCerts": True, "moz:firefoxOptions": {}}} + if config.getoption("--binary"): + capabilities["alwaysMatch"]["moz:firefoxOptions"]["binary"] = config.getoption("--binary") + if config.getoption("--headless"): + capabilities["alwaysMatch"]["moz:firefoxOptions"]["args"] = ["--headless"] + + config.driver = webdriver.Session("localhost", 4444, + capabilities=capabilities) + config.add_cleanup(config.driver.end) + + # Although the name of the `_create_unverified_context` method suggests + # that it is not intended for external consumption, the standard library's + # documentation explicitly endorses its use: + # + # > To revert to the previous, unverified, behavior + # > ssl._create_unverified_context() can be passed to the context + # > parameter. + # + # https://docs.python.org/2/library/httplib.html#httplib.HTTPSConnection + config.ssl_context = ssl._create_unverified_context() + + config.server = WPTServer(WPT_ROOT) + config.server.start(config.ssl_context) + config.add_cleanup(config.server.stop) + + +def resolve_uri(context, uri): + if uri.startswith('/'): + base = WPT_ROOT + path = uri[1:] + else: + base = os.path.dirname(context) + path = uri + + return os.path.exists(os.path.join(base, path)) + + +def _summarize(actual): + def _scrub_stack(test_obj): + copy = dict(test_obj) + del copy['stack'] + return copy + + def _expand_status(status_obj): + for key, value in [item for item in status_obj.items()]: + # In "status" and "test" objects, the "status" value enum + # definitions are interspersed with properties for unrelated + # metadata. The following condition is a best-effort attempt to + # ignore non-enum properties. + if key != key.upper() or not isinstance(value, int): + continue + + del status_obj[key] + + if status_obj['status'] == value: + status_obj[u'status_string'] = key + + del status_obj['status'] + + return status_obj + + def _summarize_test(test_obj): + del test_obj['index'] + + assert 'phase' in test_obj + assert 'phases' in test_obj + assert 'COMPLETE' in test_obj['phases'] + assert test_obj['phase'] == test_obj['phases']['COMPLETE'] + del test_obj['phases'] + del test_obj['phase'] + + return _expand_status(_scrub_stack(test_obj)) + + def _summarize_status(status_obj): + return _expand_status(_scrub_stack(status_obj)) + + + summarized = {} + + summarized[u'summarized_status'] = _summarize_status(actual['status']) + summarized[u'summarized_tests'] = [ + _summarize_test(test) for test in actual['tests']] + summarized[u'summarized_tests'].sort(key=lambda test_obj: test_obj.get('name')) + summarized[u'summarized_asserts'] = [ + {"assert_name": assert_item["assert_name"], + "test": assert_item["test"]["name"] if assert_item["test"] else None, + "args": assert_item["args"], + "status": assert_item["status"]} for assert_item in actual["asserts"]] + summarized[u'type'] = actual['type'] + + return summarized + + +class HTMLItem(pytest.Item, pytest.Collector): + def __init__(self, parent, filename, test_type): + self.url = parent.session.config.server.url(filename) + self.type = test_type + # Some tests are reliant on the WPT servers substitution functionality, + # so tests must be retrieved from the server rather than read from the + # file system directly. + handle = urllib.request.urlopen(self.url, + context=parent.session.config.ssl_context) + try: + markup = handle.read() + finally: + handle.close() + + if test_type not in TEST_TYPES: + raise ValueError('Unrecognized test type: "%s"' % test_type) + + parsed = html5lib.parse(markup, namespaceHTMLElements=False) + name = None + self.expected = None + + for element in parsed.iter(): + if not name and element.tag == 'title': + name = element.text + continue + if element.tag == 'script': + if element.attrib.get('id') == 'expected': + try: + self.expected = json.loads(element.text) + except ValueError: + print("Failed parsing JSON in %s" % filename) + raise + + if not name: + raise ValueError('No name found in %s add a <title> element' % filename) + elif self.type == 'functional': + if not self.expected: + raise ValueError('Functional tests must specify expected report data') + elif self.type == 'unit' and self.expected: + raise ValueError('Unit tests must not specify expected report data') + + # Ensure that distinct items have distinct fspath attributes. + # This is necessary because pytest has an internal cache keyed on it, + # and only the first test with any given fspath will be run. + # + # This cannot use super(HTMLItem, self).__init__(..) because only the + # Collector constructor takes the fspath argument. + pytest.Item.__init__(self, name, parent) + pytest.Collector.__init__(self, name, parent, fspath=py.path.local(filename)) + + + def reportinfo(self): + return self.fspath, None, self.url + + def repr_failure(self, excinfo): + return pytest.Collector.repr_failure(self, excinfo) + + def runtest(self): + if self.type == 'unit': + self._run_unit_test() + elif self.type == 'functional': + self._run_functional_test() + else: + raise NotImplementedError + + def _run_unit_test(self): + driver = self.session.config.driver + server = self.session.config.server + + driver.url = server.url(HARNESS) + + actual = driver.execute_async_script( + 'runTest("%s", "foo", arguments[0])' % self.url + ) + + summarized = _summarize(copy.deepcopy(actual)) + + print(json.dumps(summarized, indent=2)) + + assert summarized[u'summarized_status'][u'status_string'] == u'OK', summarized[u'summarized_status'][u'message'] + for test in summarized[u'summarized_tests']: + msg = "%s\n%s" % (test[u'name'], test[u'message']) + assert test[u'status_string'] == u'PASS', msg + + def _run_functional_test(self): + driver = self.session.config.driver + server = self.session.config.server + + driver.url = server.url(HARNESS) + + test_url = self.url + actual = driver.execute_async_script('runTest("%s", "foo", arguments[0])' % test_url) + + print(json.dumps(actual, indent=2)) + + summarized = _summarize(copy.deepcopy(actual)) + + print(json.dumps(summarized, indent=2)) + + # Test object ordering is not guaranteed. This weak assertion verifies + # that the indices are unique and sequential + indices = [test_obj.get('index') for test_obj in actual['tests']] + self._assert_sequence(indices) + + self.expected[u'summarized_tests'].sort(key=lambda test_obj: test_obj.get('name')) + + # Make asserts opt-in for now + if "summarized_asserts" not in self.expected: + del summarized["summarized_asserts"] + else: + # We can't be sure of the order of asserts even within the same test + # although we could also check for the failing assert being the final + # one + for obj in [summarized, self.expected]: + obj["summarized_asserts"].sort( + key=lambda x: (x["test"] or "", x["status"], x["assert_name"], tuple(x["args"]))) + + assert summarized == self.expected + + @staticmethod + def _assert_sequence(nums): + if nums and len(nums) > 0: + assert nums == list(range(1, nums[-1] + 1)) diff --git a/testing/web-platform/tests/resources/test/harness.html b/testing/web-platform/tests/resources/test/harness.html new file mode 100644 index 0000000000..5ee0f285e8 --- /dev/null +++ b/testing/web-platform/tests/resources/test/harness.html @@ -0,0 +1,26 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + </head> + <body> + <script> +function runTest(url, id, done) { + var child; + + function onMessage(event) { + if (!event.data || event.data.type !== 'complete') { + return; + } + + window.removeEventListener('message', onMessage); + child.close(); + done(event.data); + } + window.addEventListener('message', onMessage); + + window.child = child = window.open(url, id); +} + </script> + </body> +</html> diff --git a/testing/web-platform/tests/resources/test/idl-helper.js b/testing/web-platform/tests/resources/test/idl-helper.js new file mode 100644 index 0000000000..2b73527ff2 --- /dev/null +++ b/testing/web-platform/tests/resources/test/idl-helper.js @@ -0,0 +1,24 @@ +"use strict"; + +var typedefFrom = interfaceFrom; +var dictionaryFrom = interfaceFrom; +function interfaceFrom(i) { + var idl = new IdlArray(); + idl.add_idls(i); + for (var prop in idl.members) { + return idl.members[prop]; + } +} + +function memberFrom(m) { + var idl = new IdlArray(); + idl.add_idls('interface A { ' + m + '; };'); + return idl.members["A"].members[0]; +} + +function typeFrom(type) { + var ast = WebIDL2.parse('interface Foo { ' + type + ' a(); };'); + ast = ast[0]; // get the first fragment + ast = ast.members[0]; // get the first member + return ast.idlType; // get the type of the first field +} diff --git a/testing/web-platform/tests/resources/test/nested-testharness.js b/testing/web-platform/tests/resources/test/nested-testharness.js new file mode 100644 index 0000000000..d97c1568c7 --- /dev/null +++ b/testing/web-platform/tests/resources/test/nested-testharness.js @@ -0,0 +1,80 @@ +'use strict'; + +/** + * Execute testharness.js and one or more scripts in an iframe. Report the + * results of the execution. + * + * @param {...function|...string} bodies - a function body. If specified as a + * function object, it will be + * serialized to a string using the + * built-in + * `Function.prototype.toString` prior + * to inclusion in the generated + * iframe. + * + * @returns {Promise} eventual value describing the result of the test + * execution; the summary object has two properties: + * `harness` (a string describing the harness status) and + * `tests` (an object whose "own" property names are the + * titles of the defined sub-tests and whose associated + * values are the subtest statuses). + */ +function makeTest(...bodies) { + const closeScript = '<' + '/script>'; + let src = ` +<!DOCTYPE HTML> +<html> +<head> +<title>Document title</title> +<script src="/resources/testharness.js?${Math.random()}">${closeScript} +</head> + +<body> +<div id="log"></div>`; + bodies.forEach((body) => { + src += '<script>(' + body + ')();' + closeScript; + }); + + const iframe = document.createElement('iframe'); + + document.body.appendChild(iframe); + iframe.contentDocument.write(src); + + return new Promise((resolve) => { + window.addEventListener('message', function onMessage(e) { + if (e.source !== iframe.contentWindow) { + return; + } + if (!e.data || e.data.type !=='complete') { + return; + } + window.removeEventListener('message', onMessage); + resolve(e.data); + }); + + iframe.contentDocument.close(); + }).then(({ tests, status }) => { + const summary = { + harness: getEnumProp(status, status.status), + tests: {} + }; + + tests.forEach((test) => { + summary.tests[test.name] = getEnumProp(test, test.status); + }); + + return summary; + }); +} + +function getEnumProp(object, value) { + for (let property in object) { + if (!/^[A-Z]+$/.test(property)) { + continue; + } + + if (object[property] === value) { + return property; + } + } +} diff --git a/testing/web-platform/tests/resources/test/requirements.txt b/testing/web-platform/tests/resources/test/requirements.txt new file mode 100644 index 0000000000..95d87c3875 --- /dev/null +++ b/testing/web-platform/tests/resources/test/requirements.txt @@ -0,0 +1 @@ +html5lib==1.1 diff --git a/testing/web-platform/tests/resources/test/tests/functional/abortsignal.html b/testing/web-platform/tests/resources/test/tests/functional/abortsignal.html new file mode 100644 index 0000000000..e6080e96ec --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/abortsignal.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<title>Test#get_signal</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + "use strict"; + + setup(() => { + assert_implements_optional(window.AbortController, "No AbortController"); + }); + + let signal; + let observed = false; + + test(t => { + signal = t.get_signal(); + assert_true(signal instanceof AbortSignal, "Returns an abort signal"); + assert_false(signal.aborted, "Signal should not be aborted before test end"); + signal.onabort = () => observed = true; + }, "t.signal existence"); + + test(t => { + assert_true(signal.aborted, "Signal should be aborted after test end"); + assert_true(observed, "onabort should have been called"); + }, "t.signal.aborted"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "message": null, + "name": "t.signal existence", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "t.signal.aborted", + "properties": {}, + "status_string": "PASS" + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html new file mode 100644 index 0000000000..468319fdbe --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup.html @@ -0,0 +1,91 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +var log_sync; +test(function(t) { + log_sync = ""; + t.add_cleanup(function() { log_sync += "1"; }); + t.add_cleanup(function() { log_sync += "2"; }); + t.add_cleanup(function() { log_sync += "3"; }); + t.add_cleanup(function() { log_sync += "4"; }); + t.add_cleanup(function() { log_sync += "5"; }); + log_sync += "0"; +}, "probe synchronous"); + +test(function() { + if (log_sync !== "012345") { + throw new Error("Expected: '012345'. Actual: '" + log_sync + "'."); + } +}, "Cleanup methods are invoked exactly once and in the expected sequence."); + +var complete, log_async; +async_test(function(t) { + complete = t.step_func(function() { + if (log_async !== "012") { + throw new Error("Expected: '012'. Actual: '" + log_async + "'."); + } + + t.done(); + }); +}, "Cleanup methods are invoked following the completion of asynchronous tests"); + +async_test(function(t) { + log_async = ""; + t.add_cleanup(function() { log_async += "1"; }); + + setTimeout(t.step_func(function() { + t.add_cleanup(function() { + log_async += "2"; + complete(); + }); + log_async += "0"; + t.done(); + }), 0); +}, "probe asynchronous"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Cleanup methods are invoked exactly once and in the expected sequence.", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Cleanup methods are invoked following the completion of asynchronous tests", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "probe asynchronous", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "probe synchronous", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html new file mode 100644 index 0000000000..07ade4b93b --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup with Promise-returning functions</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +"use strict"; +var completeCount = 0; +var counts = { + afterTick: null, + afterFirst: null +}; + +add_result_callback(function(result_t) { + completeCount += 1; +}); + +promise_test(function(t) { + t.add_cleanup(function() { + return new Promise(function(resolve) { + setTimeout(function() { + counts.afterTick = completeCount; + resolve(); + }, 0); + }); + }); + t.add_cleanup(function() { + return new Promise(function(resolve) { + + setTimeout(function() { + counts.afterFirst = completeCount; + resolve(); + }, 0); + }); + }); + + return Promise.resolve(); +}, 'promise_test with asynchronous cleanup'); + +promise_test(function() { + assert_equals( + counts.afterTick, + 0, + "test is not asynchronously considered 'complete'" + ); + assert_equals( + counts.afterFirst, + 0, + "test is not considered 'complete' following fulfillment of first promise" + ); + assert_equals(completeCount, 1); + + return Promise.resolve(); +}, "synchronously-defined promise_test"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "promise_test with asynchronous cleanup", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "synchronously-defined promise_test", + "message": null, + "properties": {} + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html new file mode 100644 index 0000000000..867bde2c39 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_bad_return.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup with non-thenable-returning function</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +"use strict"; + +promise_test(function(t) { + t.add_cleanup(function() {}); + t.add_cleanup(function() { + return { then: 9 }; + }); + t.add_cleanup(function() { return Promise.resolve(); }); + + return Promise.resolve(); +}, "promise_test that returns a non-thenable object in one \"cleanup\" callback"); + +promise_test(function() {}, "The test runner is in an unpredictable state ('NOT RUN')"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Test named 'promise_test that returns a non-thenable object in one \"cleanup\" callback' specified 3 'cleanup' functions, and 1 returned a non-thenable value." + }, + "summarized_tests": [ + { + "status_string": "NOTRUN", + "name": "The test runner is in an unpredictable state ('NOT RUN')", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "promise_test that returns a non-thenable object in one \"cleanup\" callback", + "message": null, + "properties": {} + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html new file mode 100644 index 0000000000..e51465e7eb --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup with Promise-returning functions (rejection handling)</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +"use strict"; +var resolve, reject; +var completeCount = 0; +add_result_callback(function(result_t) { + completeCount += 1; +}); +promise_test(function(t) { + t.add_cleanup(function() { + return new Promise(function(_, _reject) { reject = _reject; }); + }); + t.add_cleanup(function() { + return new Promise(function(_resolve) { resolve = _resolve; }); + }); + + // The following cleanup function defines empty tests so that the reported + // data demonstrates the intended run-time behavior without relying on the + // test harness's handling of errors during test cleanup (which is tested + // elsewhere). + t.add_cleanup(function() { + if (completeCount === 0) { + promise_test( + function() {}, + "test is not asynchronously considered 'complete' ('NOT RUN')" + ); + } + + reject(); + + setTimeout(function() { + if (completeCount === 0) { + promise_test( + function() {}, + "test is not considered 'complete' following rejection of first " + + "promise ('NOT RUN')" + ); + } + + resolve(); + }, 0); + }); + + return Promise.resolve(); +}, "promise_test with asynchronous cleanup including rejection"); + +promise_test(function() {}, "synchronously-defined test ('NOT RUN')"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Test named 'promise_test with asynchronous cleanup including rejection' specified 3 'cleanup' functions, and 1 failed." + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "promise_test with asynchronous cleanup including rejection", + "message": null, + "properties": {} + }, + { + "status_string": "NOTRUN", + "name": "synchronously-defined test ('NOT RUN')", + "message": null, + "properties": {} + }, + { + "status_string": "NOTRUN", + "name": "test is not asynchronously considered 'complete' ('NOT RUN')", + "message": null, + "properties": {} + }, + { + "status_string": "NOTRUN", + "name": "test is not considered 'complete' following rejection of first promise ('NOT RUN')", + "message": null, + "properties": {} + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html new file mode 100644 index 0000000000..f9b2846100 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_rejection_after_load.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup with Promise-returning functions (rejection handling following "load" event)</title> +</head> +<body> +<h1>Promise Tests</h1> +<p>This test demonstrates the use of <tt>promise_test</tt>. Assumes ECMAScript 6 +Promise support. Some failures are expected.</p> +<div id="log"></div> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +<script> +promise_test(function(t) { + t.add_cleanup(function() { + return Promise.reject(new Error("foo")); + }); + + return new Promise((resolve) => { + document.addEventListener("DOMContentLoaded", function() { + setTimeout(resolve, 0) + }); + }); +}, "Test with failing cleanup that completes after DOMContentLoaded event"); + +promise_test(function(t) { + return Promise.resolve(); +}, "Test that should not be run due to invalid harness state ('NOT RUN')"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Test named 'Test with failing cleanup that completes after DOMContentLoaded event' specified 1 'cleanup' function, and 1 failed." + }, + "summarized_tests": [ + { + "status_string": "NOTRUN", + "name": "Test that should not be run due to invalid harness state ('NOT RUN')", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test with failing cleanup that completes after DOMContentLoaded event", + "message": null, + "properties": {} + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html new file mode 100644 index 0000000000..429536ce6e --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_async_timeout.html @@ -0,0 +1,57 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup with Promise-returning functions (timeout handling)</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +promise_test(function(t) { + t.add_cleanup(function() { + return Promise.resolve(); + }); + + t.add_cleanup(function() { + return new Promise(function() {}); + }); + + t.add_cleanup(function() {}); + + t.add_cleanup(function() { + return new Promise(function() {}); + }); + + return Promise.resolve(); +}, "promise_test with asynchronous cleanup"); + +promise_test(function() {}, "promise_test following timed out cleanup ('NOT RUN')"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Timeout while running cleanup for test named \"promise_test with asynchronous cleanup\"." + }, + "summarized_tests": [ + { + "status_string": "NOTRUN", + "name": "promise_test following timed out cleanup ('NOT RUN')", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "promise_test with asynchronous cleanup", + "message": null, + "properties": {} + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html new file mode 100644 index 0000000000..3cfb28adf2 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_bad_return.html @@ -0,0 +1,61 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup with value-returning function</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +test(function(t) { + t.add_cleanup(function() {}); + t.add_cleanup(function() { return null; }); + t.add_cleanup(function() { + test( + function() {}, + "The test runner is in an unpredictable state #1 ('NOT RUN')" + ); + + throw new Error(); + }); + t.add_cleanup(function() { return 4; }); + t.add_cleanup(function() { return { then: function() {} }; }); + t.add_cleanup(function() {}); +}, "Test that returns a value in three \"cleanup\" functions"); + +test(function() {}, "The test runner is in an unpredictable state #2 ('NOT RUN')"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Test named 'Test that returns a value in three \"cleanup\" functions' specified 6 'cleanup' functions, and 1 failed, and 3 returned a non-undefined value." + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Test that returns a value in three \"cleanup\" functions", + "properties": {}, + "message": null + }, + { + "status_string": "NOTRUN", + "name": "The test runner is in an unpredictable state #1 ('NOT RUN')", + "message": null, + "properties": {} + }, + { + "status_string": "NOTRUN", + "name": "The test runner is in an unpredictable state #2 ('NOT RUN')", + "message": null, + "properties": {} + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html new file mode 100644 index 0000000000..2c9b51c6d0 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_count.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup reported count</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +promise_test(function(t) { + t.add_cleanup(function() {}); + t.add_cleanup(function() {}); + t.add_cleanup(function() { throw new Error(); }); + new EventWatcher(t, document.body, []); + + return Promise.resolve(); +}, 'test with 3 user-defined cleanup functions'); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Test named 'test with 3 user-defined cleanup functions' specified 3 'cleanup' functions, and 1 failed." + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "test with 3 user-defined cleanup functions", + "message": null, + "properties": {} + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html new file mode 100644 index 0000000000..60357c66ee --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup: error</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> +<script> +"use strict"; + +test(function(t) { + t.add_cleanup(function() { + throw new Error('exception in cleanup function'); + }); +}, "Exception in cleanup function causes harness failure."); + +test(function() {}, "This test should not be run."); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Test named 'Exception in cleanup function causes harness failure.' specified 1 'cleanup' function, and 1 failed." + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Exception in cleanup function causes harness failure.", + "properties": {}, + "message": null + }, + { + "status_string": "NOTRUN", + "name": "This test should not be run.", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html new file mode 100644 index 0000000000..80ba1b4959 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_err_multi.html @@ -0,0 +1,52 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup: multiple functions with one in error</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +"use strict"; + +test(function(t) { + t.add_cleanup(function() { + throw new Error("exception in cleanup function"); + }); + + // The following cleanup function defines a test so that the reported + // data demonstrates the intended run-time behavior, i.e. that + // `testharness.js` invokes all cleanup functions even when one or more + // throw errors. + t.add_cleanup(function() { + test(function() {}, "Verification test"); + }); + }, "Test with multiple cleanup functions"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Test named 'Test with multiple cleanup functions' specified 2 'cleanup' functions, and 1 failed." + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Test with multiple cleanup functions", + "properties": {}, + "message": null + }, + { + "status_string": "NOTRUN", + "name": "Verification test", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html new file mode 100644 index 0000000000..0a61503cc8 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/add_cleanup_sync_queue.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#add_cleanup: queuing tests</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +"use strict"; +var firstCleaned = false; + +promise_test(function(t) { + promise_test(function() { + assert_true( + firstCleaned, "should not execute until first test is complete" + ); + + return Promise.resolve(); + }, "test defined when no tests are queued, but one test is executing"); + + t.add_cleanup(function() { + firstCleaned = true; + }); + + return Promise.resolve(); +}, "Test with a 'cleanup' function"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "message": null, + "name": "Test with a 'cleanup' function", + "status_string": "PASS", + "properties": {} + }, + { + "message": null, + "name": "test defined when no tests are queued, but one test is executing", + "status_string": "PASS", + "properties": {} + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html b/testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html new file mode 100644 index 0000000000..9de875b0f1 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/api-tests-1.html @@ -0,0 +1,991 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Sample HTML5 API Tests</title> +<meta name="timeout" content="6000"> +</head> +<body onload="load_test_attr.done()"> +<h1>Sample HTML5 API Tests</h1> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + setup_run = false; + setup(function() { + setup_run = true; + }); + test(function() {assert_true(setup_run)}, "Setup function ran"); + + // Two examples for testing events from handler and attributes + var load_test_event = async_test("window onload event fires when set from the handler"); + + function windowLoad() + { + load_test_event.done(); + } + on_event(window, "load", windowLoad); + + test(function() { + var sequence = []; + var outer = document.createElement("div"); + var inner = document.createElement("div"); + outer.appendChild(inner); + document.body.appendChild(outer); + inner.addEventListener("click", function() { + sequence.push("inner"); + }, false); + + on_event(outer, "click", function() { + sequence.push("outer"); + }); + inner.click(); + + assert_array_equals(sequence, ["inner", "outer"]); + }, "on_event does not use event capture"); + + // see the body onload below + var load_test_attr = async_test("body element fires the onload event set from the attribute"); +</script> +<script> + function bodyElement() + { + assert_equals(document.body, document.getElementsByTagName("body")[0]); + } + test(bodyElement, "document.body should be the first body element in the document"); + + test(function() { + assert_equals(1,1); + assert_equals(NaN, NaN, "NaN case"); + assert_equals(0, 0, "Zero case"); + }, "assert_equals tests") + + test(function() { + assert_equals(-0, 0, "Zero case"); + }, "assert_equals tests expected to fail") + + test(function() { + assert_not_equals({}, {}, "object case"); + assert_not_equals(-0, 0, "Zero case"); + }, "assert_not_equals tests") + + function testAssertPass() + { + assert_true(true); + } + test(testAssertPass, "assert_true expected to pass"); + + function testAssertFalse() + { + assert_true(false, "false should not be true"); + } + test(testAssertFalse, "assert_true expected to fail"); + + function basicAssertArrayEquals() + { + assert_array_equals([1, NaN], [1, NaN], "[1, NaN] is equal to [1, NaN]"); + } + test(basicAssertArrayEquals, "basic assert_array_equals test"); + + function assertArrayEqualsUndefined() + { + assert_array_equals(undefined, [1], "undefined equals [1]?"); + } + test(assertArrayEqualsUndefined, "assert_array_equals with first param undefined"); + + function assertArrayEqualsTrue() + { + assert_array_equals(true, [1], "true equals [1]?"); + } + test(assertArrayEqualsTrue, "assert_array_equals with first param true"); + + function assertArrayEqualsFalse() + { + assert_array_equals(false, [1], "false equals [1]?"); + } + test(assertArrayEqualsFalse, "assert_array_equals with first param false"); + + function assertArrayEqualsNull() + { + assert_array_equals(null, [1], "null equals [1]?"); + } + test(assertArrayEqualsNull, "assert_array_equals with first param null"); + + function assertArrayEqualsNumeric() + { + assert_array_equals(1, [1], "1 equals [1]?"); + } + test(assertArrayEqualsNumeric, "assert_array_equals with first param 1"); + + function basicAssertArrayApproxEquals() + { + assert_array_approx_equals([10, 11], [11, 10], 1, "[10, 11] is approximately (+/- 1) [11, 10]") + } + test(basicAssertArrayApproxEquals, "basic assert_array_approx_equals test"); + + function basicAssertApproxEquals() + { + assert_approx_equals(10, 11, 1, "10 is approximately (+/- 1) 11") + } + test(basicAssertApproxEquals, "basic assert_approx_equals test"); + + function basicAssertLessThan() + { + assert_less_than(10, 11, "10 is less than 11") + } + test(basicAssertApproxEquals, "basic assert_less_than test"); + + function basicAssertGreaterThan() + { + assert_greater_than(10, 11, "10 is not greater than 11"); + } + test(basicAssertGreaterThan, "assert_greater_than expected to fail"); + + function basicAssertGreaterThanEqual() + { + assert_greater_than_equal(10, 10, "10 is greater than or equal to 10") + } + test(basicAssertGreaterThanEqual, "basic assert_greater_than_equal test"); + + function basicAssertLessThanEqual() + { + assert_greater_than_equal('10', 10, "'10' is not a number") + } + test(basicAssertLessThanEqual, "assert_less_than_equal expected to fail"); + + function testAssertInherits() { + var A = function(){this.a = "a"} + A.prototype = {b:"b"} + var a = new A(); + assert_own_property(a, "a"); + assert_not_own_property(a, "b", "unexpected property found: \"b\""); + assert_inherits(a, "b"); + } + test(testAssertInherits, "test for assert[_not]_own_property and insert_inherits") + + test(function() + { + var a = document.createElement("a") + var b = document.createElement("b") + assert_throws_dom("NOT_FOUND_ERR", function () {a.removeChild(b)}); + }, "Test throw DOM exception") + + test(function() + { + var a = document.createElement("a") + var b = document.createElement("b") + assert_throws_js(DOMException, function () {a.removeChild(b)}); + }, "Test throw DOMException as JS exception expected to fail") + + test(function() + { + assert_throws_js(SyntaxError, function () {document.querySelector("")}); + }, "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail") + + test(function() + { + assert_throws_js(SyntaxError, function () {JSON.parse("{")}); + }, "Test throw JS SyntaxError") + + test(function() + { + assert_throws_dom("SyntaxError", function () {document.querySelector("")}); + }, "Test throw DOM SyntaxError") + + test(function() + { + var ifr = document.createElement("iframe"); + document.body.appendChild(ifr); + this.add_cleanup(() => ifr.remove()); + assert_throws_dom("SyntaxError", ifr.contentWindow.DOMException, + function () {ifr.contentDocument.querySelector("")}); + }, "Test throw DOM SyntaxError from subframe"); + + test(function() + { + var ifr = document.createElement("iframe"); + document.body.appendChild(ifr); + this.add_cleanup(() => ifr.remove()); + assert_throws_dom("SyntaxError", + function () {ifr.contentDocument.querySelector("")}); + }, "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail"); + + test(function() + { + var ifr = document.createElement("iframe"); + document.body.appendChild(ifr); + this.add_cleanup(() => ifr.remove()); + assert_throws_dom("SyntaxError", ifr.contentWindow.DOMException, + function () {document.querySelector("")}); + }, "Test throw DOM SyntaxError with incorrect expectation; expected to fail"); + + test(function() + { + assert_throws_dom("SyntaxError", function () {JSON.parse("{")}); + }, "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail") + + test(function() + { + var a = document.createTextNode("a") + var b = document.createElement("b") + assert_throws_dom("NOT_FOUND_ERR", function () {a.appendChild(b)}); + }, "Test throw DOM exception expected to fail") + + test(function() + { + var e = new DOMException("I am not known", "TEST_ERROR_NO_SUCH_THING"); + assert_throws_dom(0, function() {throw e}); + }, "Test assert_throws_dom with ambiguous DOM-exception expected to Fail"); + + test(function() + { + var e = {code:0, name:"TEST_ERR", TEST_ERR:0}; + e.constructor = DOMException; + assert_throws_dom("TEST_ERR", function() {throw e}); + }, "Test assert_throws_dom with non-DOM-exception expected to Fail"); + + test(function() + { + var e = {code: DOMException.SYNTAX_ERR, name:"SyntaxError"}; + e.constructor = DOMException; + assert_throws_dom(DOMException.SYNTAX_ERR, function() {throw e}); + }, "Test assert_throws_dom with number code value expected to Pass"); + + test(function() + { + var e = new DOMException("Some message", "SyntaxError"); + assert_throws_dom(DOMException.SYNTAX_ERR, function() {throw e}); + }, "Test assert_throws_dom with number code value and real DOMException expected to Pass"); + + var t = async_test("Test step_func") + setTimeout( + t.step_func( + function () { + assert_true(true); t.done(); + }), 0); + + async_test(function(t) { + setTimeout(t.step_func(function (){assert_true(true); t.done();}), 0); + }, "Test async test with callback"); + + async_test(function() { + setTimeout(this.step_func(function (){assert_true(true); this.done();}), 0); + }, "Test async test with callback and `this` obj."); + + async_test("test should timeout (fail) with the default of 2 seconds").step(function(){}); + + async_test("async test that is never started, should have status Not Run"); + + + test(function(t) { + window.global = 1; + t.add_cleanup(function() {delete window.global}); + assert_equals(window.global, 1); + }, + "Test that defines a global and cleans it up"); + + test(function() {assert_equals(window.global, undefined)}, + "Test that cleanup handlers from previous test ran"); + +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "TIMEOUT", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Setup function ran", + "message": null, + "properties": {} + }, + { + "status_string": "FAIL", + "name": "Test assert_throws_dom with ambiguous DOM-exception expected to Fail", + "message": "Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "Test assert_throws_dom with non-DOM-exception expected to Fail", + "message": "Test bug: unrecognized DOMException code name or name \"TEST_ERR\" passed to assert_throws_dom()", + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test assert_throws_dom with number code value expected to Pass", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test assert_throws_dom with number code value and real DOMException expected to Pass", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test async test with callback", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test async test with callback and `this` obj.", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test step_func", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test that cleanup handlers from previous test ran", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test that defines a global and cleans it up", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test throw DOM exception", + "message": null, + "properties": {} + }, + { + "status_string": "FAIL", + "name": "Test throw DOMException as JS exception expected to fail", + "message": "assert_throws_js: function \"function DOMException() {\n [native code]\n}\" is not an Error subtype", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail", + "message": "assert_throws_js: function \"function () {document.querySelector(\"\")}\" threw object \"SyntaxError: Document.querySelector: '' is not a valid selector\" (\"SyntaxError\") expected instance of function \"function SyntaxError() {\n [native code]\n}\" (\"SyntaxError\")", + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test throw JS SyntaxError", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test throw DOM SyntaxError", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Test throw DOM SyntaxError from subframe", + "message": null, + "properties": {} + }, + { + "status_string": "FAIL", + "name": "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail", + "message": "assert_throws_dom: function \"function () {ifr.contentDocument.querySelector(\"\")}\" threw an exception from the wrong global", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "Test throw DOM SyntaxError with incorrect expectation; expected to fail", + "message": "assert_throws_dom: function \"function () {document.querySelector(\"\")}\" threw an exception from the wrong global", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail", + "message": "assert_throws_dom: function \"function () {JSON.parse(\"{\")}\" threw object \"SyntaxError: JSON.parse: end of data while reading object contents at line 1 column 2 of the JSON data\" that is not a DOMException SyntaxError: property \"code\" is equal to undefined, expected 12", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "Test throw DOM exception expected to fail", + "message": "assert_throws_dom: function \"function () {a.appendChild(b)}\" threw object \"HierarchyRequestError: Node.appendChild: Cannot add children to a Text\" that is not a DOMException NOT_FOUND_ERR: property \"code\" is equal to 3, expected 8", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "assert_array_equals with first param 1", + "message": "assert_array_equals: 1 equals [1]? value is 1, expected array", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "assert_array_equals with first param false", + "message": "assert_array_equals: false equals [1]? value is false, expected array", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "assert_array_equals with first param null", + "message": "assert_array_equals: null equals [1]? value is null, expected array", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "assert_array_equals with first param true", + "message": "assert_array_equals: true equals [1]? value is true, expected array", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "assert_array_equals with first param undefined", + "message": "assert_array_equals: undefined equals [1]? value is undefined, expected array", + "properties": {} + }, + { + "status_string": "PASS", + "name": "assert_equals tests", + "message": null, + "properties": {} + }, + { + "status_string": "FAIL", + "name": "assert_equals tests expected to fail", + "message": "assert_equals: Zero case expected 0 but got -0", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "assert_greater_than expected to fail", + "message": "assert_greater_than: 10 is not greater than 11 expected a number greater than 11 but got 10", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "assert_less_than_equal expected to fail", + "message": "assert_greater_than_equal: '10' is not a number expected a number but got a \"string\"", + "properties": {} + }, + { + "status_string": "PASS", + "name": "assert_not_equals tests", + "message": null, + "properties": {} + }, + { + "status_string": "FAIL", + "name": "assert_true expected to fail", + "message": "assert_true: false should not be true expected true got false", + "properties": {} + }, + { + "status_string": "PASS", + "name": "assert_true expected to pass", + "message": null, + "properties": {} + }, + { + "status_string": "NOTRUN", + "name": "async test that is never started, should have status Not Run", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "basic assert_approx_equals test", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "basic assert_array_approx_equals test", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "basic assert_array_equals test", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "basic assert_greater_than_equal test", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "basic assert_less_than test", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "body element fires the onload event set from the attribute", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "document.body should be the first body element in the document", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "test for assert[_not]_own_property and insert_inherits", + "message": null, + "properties": {} + }, + { + "status_string": "TIMEOUT", + "name": "test should timeout (fail) with the default of 2 seconds", + "message": "Test timed out", + "properties": {} + }, + { + "status_string": "PASS", + "name": "window onload event fires when set from the handler", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "on_event does not use event capture", + "message": null, + "properties": {} + } + ], + "summarized_asserts": [ + { + "assert_name": "assert_true", + "test": "Setup function ran", + "args": [ + "true" + ], + "status": 0 + }, + { + "assert_name": "assert_array_equals", + "test": "on_event does not use event capture", + "args": [ + "[\"inner\", \"outer\"]", + "[\"inner\", \"outer\"]" + ], + "status": 0 + }, + { + "assert_name": "assert_equals", + "test": "document.body should be the first body element in the document", + "args": [ + "Element node <body onload=\"load_test_attr.done()\"> <h1>Sample HTML5 AP...", + "Element node <body onload=\"load_test_attr.done()\"> <h1>Sample HTML5 AP..." + ], + "status": 0 + }, + { + "assert_name": "assert_equals", + "test": "assert_equals tests", + "args": [ + "1", + "1" + ], + "status": 0 + }, + { + "assert_name": "assert_equals", + "test": "assert_equals tests", + "args": [ + "NaN", + "NaN", + "\"NaN case\"" + ], + "status": 0 + }, + { + "assert_name": "assert_equals", + "test": "assert_equals tests", + "args": [ + "0", + "0", + "\"Zero case\"" + ], + "status": 0 + }, + { + "assert_name": "assert_equals", + "test": "assert_equals tests expected to fail", + "args": [ + "-0", + "0", + "\"Zero case\"" + ], + "status": 1 + }, + { + "assert_name": "assert_not_equals", + "test": "assert_not_equals tests", + "args": [ + "object \"[object Object]\"", + "object \"[object Object]\"", + "\"object case\"" + ], + "status": 0 + }, + { + "assert_name": "assert_not_equals", + "test": "assert_not_equals tests", + "args": [ + "-0", + "0", + "\"Zero case\"" + ], + "status": 0 + }, + { + "assert_name": "assert_true", + "test": "assert_true expected to pass", + "args": [ + "true" + ], + "status": 0 + }, + { + "assert_name": "assert_true", + "test": "assert_true expected to fail", + "args": [ + "false", + "\"false should not be true\"" + ], + "status": 1 + }, + { + "assert_name": "assert_array_equals", + "test": "basic assert_array_equals test", + "args": [ + "[1, NaN]", + "[1, NaN]", + "\"[1, NaN] is equal to [1, NaN]\"" + ], + "status": 0 + }, + { + "assert_name": "assert_array_equals", + "test": "assert_array_equals with first param undefined", + "args": [ + "undefined", + "[1]", + "\"undefined equals [1]?\"" + ], + "status": 1 + }, + { + "assert_name": "assert_array_equals", + "test": "assert_array_equals with first param true", + "args": [ + "true", + "[1]", + "\"true equals [1]?\"" + ], + "status": 1 + }, + { + "assert_name": "assert_array_equals", + "test": "assert_array_equals with first param false", + "args": [ + "false", + "[1]", + "\"false equals [1]?\"" + ], + "status": 1 + }, + { + "assert_name": "assert_array_equals", + "test": "assert_array_equals with first param null", + "args": [ + "null", + "[1]", + "\"null equals [1]?\"" + ], + "status": 1 + }, + { + "assert_name": "assert_array_equals", + "test": "assert_array_equals with first param 1", + "args": [ + "1", + "[1]", + "\"1 equals [1]?\"" + ], + "status": 1 + }, + { + "assert_name": "assert_array_approx_equals", + "test": "basic assert_array_approx_equals test", + "args": [ + "[10, 11]", + "[11, 10]", + "1", + "\"[10, 11] is approximately (+/- 1) [11, 10]\"" + ], + "status": 0 + }, + { + "assert_name": "assert_approx_equals", + "test": "basic assert_approx_equals test", + "args": [ + "10", + "11", + "1", + "\"10 is approximately (+/- 1) 11\"" + ], + "status": 0 + }, + { + "assert_name": "assert_approx_equals", + "test": "basic assert_less_than test", + "args": [ + "10", + "11", + "1", + "\"10 is approximately (+/- 1) 11\"" + ], + "status": 0 + }, + { + "assert_name": "assert_greater_than", + "test": "assert_greater_than expected to fail", + "args": [ + "10", + "11", + "\"10 is not greater than 11\"" + ], + "status": 1 + }, + { + "assert_name": "assert_greater_than_equal", + "test": "basic assert_greater_than_equal test", + "args": [ + "10", + "10", + "\"10 is greater than or equal to 10\"" + ], + "status": 0 + }, + { + "assert_name": "assert_greater_than_equal", + "test": "assert_less_than_equal expected to fail", + "args": [ + "\"10\"", + "10", + "\"'10' is not a number\"" + ], + "status": 1 + }, + { + "assert_name": "assert_own_property", + "test": "test for assert[_not]_own_property and insert_inherits", + "args": [ + "object \"[object Object]\"", + "\"a\"" + ], + "status": 0 + }, + { + "assert_name": "assert_not_own_property", + "test": "test for assert[_not]_own_property and insert_inherits", + "args": [ + "object \"[object Object]\"", + "\"b\"", + "\"unexpected property found: \\\"b\\\"\"" + ], + "status": 0 + }, + { + "assert_name": "assert_inherits", + "test": "test for assert[_not]_own_property and insert_inherits", + "args": [ + "object \"[object Object]\"", + "\"b\"" + ], + "status": 0 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test throw DOM exception", + "args": [ + "\"NOT_FOUND_ERR\"", + "function \"function () {a.removeChild(b)}\"" + ], + "status": 0 + }, + { + "assert_name": "assert_throws_js", + "test": "Test throw DOMException as JS exception expected to fail", + "args": [ + "function \"function DOMException() { [native code] }\"", + "function \"function () {a.removeChild(b)}\"" + ], + "status": 1 + }, + { + "assert_name": "assert_throws_js", + "test": "Test throw SyntaxError DOMException where JS SyntaxError expected; expected to fail", + "args": [ + "function \"function SyntaxError() { [native code] }\"", + "function \"function () {document.querySelector(\"\")}\"" + ], + "status": 1 + }, + { + "assert_name": "assert_throws_js", + "test": "Test throw JS SyntaxError", + "args": [ + "function \"function SyntaxError() { [native code] }\"", + "function \"function () {JSON.parse(\"{\")}\"" + ], + "status": 0 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test throw DOM SyntaxError", + "args": [ + "\"SyntaxError\"", + "function \"function () {document.querySelector(\"\")}\"" + ], + "status": 0 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test throw DOM SyntaxError from subframe", + "args": [ + "\"SyntaxError\"", + "function \"function DOMException() { [native code] }\"", + "function \"function () {ifr.contentDocument.querySelector(\"\")}\"" + ], + "status": 0 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test throw DOM SyntaxError from subframe with incorrect global expectation; expected to fail", + "args": [ + "\"SyntaxError\"", + "function \"function () {ifr.contentDocument.querySelector(\"\")}\"" + ], + "status": 1 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test throw DOM SyntaxError with incorrect expectation; expected to fail", + "args": [ + "\"SyntaxError\"", + "function \"function DOMException() { [native code] }\"", + "function \"function () {document.querySelector(\"\")}\"" + ], + "status": 1 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test throw JS SyntaxError where SyntaxError DOMException expected; expected to fail", + "args": [ + "\"SyntaxError\"", + "function \"function () {JSON.parse(\"{\")}\"" + ], + "status": 1 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test throw DOM exception expected to fail", + "args": [ + "\"NOT_FOUND_ERR\"", + "function \"function () {a.appendChild(b)}\"" + ], + "status": 1 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test assert_throws_dom with ambiguous DOM-exception expected to Fail", + "args": [ + "0", + "function \"function() {throw e}\"" + ], + "status": 1 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test assert_throws_dom with non-DOM-exception expected to Fail", + "args": [ + "\"TEST_ERR\"", + "function \"function() {throw e}\"" + ], + "status": 1 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test assert_throws_dom with number code value expected to Pass", + "args": [ + "12", + "function \"function() {throw e}\"" + ], + "status": 0 + }, + { + "assert_name": "assert_throws_dom", + "test": "Test assert_throws_dom with number code value and real DOMException expected to Pass", + "args": [ + "12", + "function \"function() {throw e}\"" + ], + "status": 0 + }, + { + "assert_name": "assert_equals", + "test": "Test that defines a global and cleans it up", + "args": [ + "1", + "1" + ], + "status": 0 + }, + { + "assert_name": "assert_equals", + "test": "Test that cleanup handlers from previous test ran", + "args": [ + "undefined", + "undefined" + ], + "status": 0 + }, + { + "assert_name": "assert_true", + "test": "Test step_func", + "args": [ + "true" + ], + "status": 0 + }, + { + "assert_name": "assert_true", + "test": "Test async test with callback", + "args": [ + "true" + ], + "status": 0 + }, + { + "assert_name": "assert_true", + "test": "Test async test with callback and `this` obj.", + "args": [ + "true" + ], + "status": 0 + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html b/testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html new file mode 100644 index 0000000000..9af94f61ac --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/api-tests-2.html @@ -0,0 +1,62 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Sample HTML5 API Tests</title> +</head> +<body> +<h1>Sample HTML5 API Tests</h1> +<p>There should be two results</p> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +setup({explicit_done:true}) +test(function() {assert_true(true)}, "Test defined before onload"); + +onload = function() {test(function (){assert_true(true)}, "Test defined after onload"); +done(); +} +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Test defined after onload", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Test defined before onload", + "properties": {}, + "message": null + } + ], + "summarized_asserts": [ + { + "assert_name": "assert_true", + "test": "Test defined before onload", + "args": [ + "true" + ], + "status": 0 + }, + { + "assert_name": "assert_true", + "test": "Test defined after onload", + "args": [ + "true" + ], + "status": 0 + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html b/testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html new file mode 100644 index 0000000000..991fc6da67 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/api-tests-3.html @@ -0,0 +1,34 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Sample HTML5 API Tests</title> +</head> +<script src="/resources/testharness.js"></script> + +<body> +<h1>Sample HTML5 API Tests</h1> +<div id="log"></div> +<script> +setup({explicit_timeout:true}); +var t = async_test("This test should give a status of 'Not Run' without a delay"); +timeout(); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "TIMEOUT", + "message": null + }, + "summarized_tests": [ + { + "status_string": "NOTRUN", + "name": "This test should give a status of 'Not Run' without a delay", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html b/testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html new file mode 100644 index 0000000000..b6460a4868 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/assert-array-equals.html @@ -0,0 +1,162 @@ +<!DOCTYPE HTML> +<title>assert_array_equals</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +test(() => { + assert_array_equals([], []); +}, "empty and equal"); +test(() => { + assert_array_equals([1], [1]); +}, "non-empty and equal"); +test(() => { + assert_array_equals([], [1]); +}, "length differs"); +test(() => { + assert_array_equals([1], [,]); +}, "property is present"); +test(() => { + assert_array_equals([,], [1]); +}, "property is missing"); +test(() => { + assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], ["x",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]); +}, "property 0 differs"); +test(() => { + assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20], [0,1,2,3,4,"x",6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]); +}, "property 5 differs"); +test(() => { + assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]); +}, "lengths differ and input array beyond display limit"); +test(() => { + assert_array_equals([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]); +}, "lengths differ and expected array beyond display limit"); +test(() => { + assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]); +}, "property 0 is present and arrays are beyond display limit"); +test(() => { + assert_array_equals([,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]); +}, "property 0 is missing and arrays are beyond display limit"); +test(() => { + assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,,19,20,21]); +}, "property 18 is present and arrays are beyond display limit"); +test(() => { + assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,,19,20,21], [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]); +}, "property 18 is missing and arrays are beyond display limit"); +test(() => { + assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], ["x",1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]); +}, "property 0 differs and arrays are beyond display limit"); +test(() => { + assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21], [0,1,2,3,4,"x",6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21]); +}, "property 5 differs and arrays are beyond display limit"); +test(() => { + assert_array_equals([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26], [0,1,2,3,4,5,6,7,8,9,10,11,"x",13,14,15,16,17,18,19,20,21,22,23,24,25,26]); +}, "property 5 differs and arrays are beyond display limit on both sides"); +</script> +<script type="text/json" id="expected"> +{ + "type": "complete", + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "name": "empty and equal", + "message": null, + "properties": {}, + "status_string": "PASS" + }, + { + "name": "length differs", + "message": "assert_array_equals: lengths differ, expected array [1] length 1, got [] length 0", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "lengths differ and expected array beyond display limit", + "message": "assert_array_equals: lengths differ, expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 22, got [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 21", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "lengths differ and input array beyond display limit", + "message": "assert_array_equals: lengths differ, expected array [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 21, got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] length 22", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "non-empty and equal", + "message": null, + "properties": {}, + "status_string": "PASS" + }, + { + "name": "property 0 differs", + "message": "assert_array_equals: expected property 0 to be \"x\" but got 0 (expected array [\"x\", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property 0 differs and arrays are beyond display limit", + "message": "assert_array_equals: expected property 0 to be \"x\" but got 0 (expected array [\"x\", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property 0 is missing and arrays are beyond display limit", + "message": "assert_array_equals: expected property 0 to be \"present\" but was \"missing\" (expected array [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property 0 is present and arrays are beyond display limit", + "message": "assert_array_equals: expected property 0 to be \"missing\" but was \"present\" (expected array [, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property 18 is missing and arrays are beyond display limit", + "message": "assert_array_equals: expected property 18 to be \"present\" but was \"missing\" (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, , 19, 20, 21])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property 18 is present and arrays are beyond display limit", + "message": "assert_array_equals: expected property 18 to be \"missing\" but was \"present\" (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, , 19, 20, 21] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property 5 differs", + "message": "assert_array_equals: expected property 5 to be \"x\" but got 5 (expected array [0, 1, 2, 3, 4, \"x\", 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property 5 differs and arrays are beyond display limit", + "message": "assert_array_equals: expected property 5 to be \"x\" but got 5 (expected array [0, 1, 2, 3, 4, \"x\", 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026] got [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, \u2026])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property 5 differs and arrays are beyond display limit on both sides", + "message": "assert_array_equals: expected property 12 to be \"x\" but got 12 (expected array [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, \"x\", 13, 14, 15, 16, 17, 18, 19, 20, 21, \u2026] got [\u2026, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, \u2026])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property is missing", + "message": "assert_array_equals: expected property 0 to be \"present\" but was \"missing\" (expected array [1] got [])", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "property is present", + "message": "assert_array_equals: expected property 0 to be \"missing\" but was \"present\" (expected array [] got [1])", + "properties": {}, + "status_string": "FAIL" + } + ] +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html b/testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html new file mode 100644 index 0000000000..4dd66b2372 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/assert-throws-dom.html @@ -0,0 +1,55 @@ +<!DOCTYPE HTML> +<title>assert_throws_dom</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<div id="log"></div> +<script> +test(() => { + function f() { + assert_true(false, "Trivial assertion."); + + // Would lead to throwing a SyntaxError. + document.createElement("div").contentEditable = "invalid"; + } + + assert_throws_dom("SyntaxError", () => { f(); }); +}, "Violated assertion nested in `assert_throws_dom`."); +</script> +<script type="text/json" id="expected"> +{ + "type": "complete", + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "message": "assert_true: Trivial assertion. expected true got false", + "name": "Violated assertion nested in `assert_throws_dom`.", + "properties": {}, + "status_string": "FAIL" + } + ], + "summarized_asserts": [ + { + "assert_name": "assert_throws_dom", + "test": "Violated assertion nested in `assert_throws_dom`.", + "args": [ + "\"SyntaxError\"", + "function \"() => { f(); }\"" + ], + "status": 1 + }, + { + "assert_name": "assert_true", + "test": "Violated assertion nested in `assert_throws_dom`.", + "args": [ + "false", + "\"Trivial assertion.\"" + ], + "status": 1 + } + ] +} +</script> + diff --git a/testing/web-platform/tests/resources/test/tests/functional/force_timeout.html b/testing/web-platform/tests/resources/test/tests/functional/force_timeout.html new file mode 100644 index 0000000000..2058fdb862 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/force_timeout.html @@ -0,0 +1,60 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test#force_timeout</title> +</head> +<body> +<h1>Test#force_timeout</h1> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +setup({ explicit_timeout: true }); + +test(function(t) { + t.force_timeout(); + }, 'test (synchronous)'); + +async_test(function(t) { + t.step_timeout(function() { + t.force_timeout(); + }, 0); + }, 'async_test'); + +promise_test(function(t) { + t.force_timeout(); + + return new Promise(function() {}); + }, 'promise_test'); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "TIMEOUT", + "name": "async_test", + "message": "Test timed out", + "properties": {} + }, + { + "status_string": "TIMEOUT", + "name": "promise_test", + "message": "Test timed out", + "properties": {} + }, + { + "status_string": "TIMEOUT", + "name": "test (synchronous)", + "message": "Test timed out", + "properties": {} + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/generate-callback.html b/testing/web-platform/tests/resources/test/tests/functional/generate-callback.html new file mode 100644 index 0000000000..11d41743b3 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/generate-callback.html @@ -0,0 +1,153 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Sample for using generate_tests to create a series of tests that share the same callback.</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<script> +// generate_tests takes an array of arrays that define tests +// but lets pass it an empty array and verify it does nothing. +function null_callback() { + throw "null_callback should not be called."; +} +generate_tests(null_callback, []); + +// Generate 3 tests specifying the name and one parameter +function validate_arguments(arg1) { + assert_equals(arg1, 1, "Ensure that we get our expected argument"); +} +generate_tests(validate_arguments, [ + ["first test", 1], + ["second test", 1], + ["third test", 1], +]); + +// Generate a test passing in a properties object that is shared across tests. +function validate_properties() { + assert_true(this.properties.sentinel, "Ensure that we got the right properties object."); +} +generate_tests(validate_properties, [["sentinel check 1"], ["sentinel check 2"]], {sentinel: true}); + +// Generate a test passing in a properties object that is shared across tests. +function validate_separate_properties() { + if (this.name === "sentinel check 1 unique properties") { + assert_true(this.properties.sentinel, "Ensure that we got the right properties object. Expect sentinel: true."); + } + else { + assert_false(this.properties.sentinel, "Ensure that we got the right properties object. Expect sentinel: false."); + } +} +generate_tests(validate_separate_properties, [["sentinel check 1 unique properties"], ["sentinel check 2 unique properties"]], [{sentinel: true}, {sentinel: false}]); + +// Finally generate a complicated set of tests from another data source +var letters = ["a", "b", "c", "d", "e", "f"]; +var numbers = [0, 1, 2, 3, 4, 5]; +function validate_related_arguments(arg1, arg2) { + assert_equals(arg1.charCodeAt(0) - "a".charCodeAt(0), arg2, "Ensure that we can map letters to numbers."); +} +function format_as_test(letter, index, letters) { + return ["Test to map " + letter + " to " + numbers[index], letter, numbers[index]]; +} +generate_tests(validate_related_arguments, letters.map(format_as_test)); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Test to map a to 0", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Test to map b to 1", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Test to map c to 2", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Test to map d to 3", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Test to map e to 4", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Test to map f to 5", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "first test", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "second test", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "sentinel check 1", + "properties": { + "sentinel": true + }, + "message": null + }, + { + "status_string": "PASS", + "name": "sentinel check 1 unique properties", + "properties": { + "sentinel": true + }, + "message": null + }, + { + "status_string": "PASS", + "name": "sentinel check 2", + "properties": { + "sentinel": true + }, + "message": null + }, + { + "status_string": "PASS", + "name": "sentinel check 2 unique properties", + "properties": { + "sentinel": false + }, + "message": null + }, + { + "status_string": "PASS", + "name": "third test", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html new file mode 100644 index 0000000000..f635768c69 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlDictionary/test_partial_interface_of.html @@ -0,0 +1,89 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>idlharness: Partial dictionary</title> + <script src="/resources/test/variants.js"></script> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> +</head> + +<body> + <p>Verify the series of sub-tests that are executed for "partial" dictionary objects.</p> + <script> + "use strict"; + + // No original existence + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls('partial dictionary A {};'); + idlArray.test(); + })(); + + // Multiple partials existence + (() => { + const idlArray = new IdlArray(); + idlArray.add_untested_idls('partial dictionary B {};'); + idlArray.add_idls('partial dictionary B {};'); + idlArray.add_idls('partial dictionary B {};'); + idlArray.add_idls('partial dictionary B {};'); + idlArray.add_idls('dictionary B {};'); + idlArray.test(); + })(); + + // Original is a namespace, not a dictionary. + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls(` + partial dictionary C {}; + namespace C {};`); + idlArray.merge_partials(); + })(); + </script> + <script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "name": "Partial dictionary A: original dictionary defined", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: Original dictionary should be defined expected true got false" + }, + { + "name": "Partial dictionary B[2]: original dictionary defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial dictionary B[3]: original dictionary defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial dictionary B[4]: original dictionary defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial dictionary C: original dictionary defined", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: Original C definition should have type dictionary expected true got false" + } + ], + "type": "complete" +} +</script> +</body> + +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html new file mode 100644 index 0000000000..addc0eb4fc --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_exposed_wildcard.html @@ -0,0 +1,233 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>idlharness: Exposed=*</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> +</head> +<body> +<script> +"use strict"; + +Object.defineProperty(window, "A", { + enumerable: false, + writable: true, + configurable: true, + value: function A() {} + }); +Object.defineProperty(window.A, "prototype", { + writable: false, + value: window.A.prototype + }); +A.prototype[Symbol.toStringTag] = "A"; + +Object.defineProperty(window, "C", { + enumerable: false, + writable: true, + configurable: true, + value: function C() {} + }); +Object.defineProperty(window.C, "prototype", { + writable: false, + value: window.C.prototype + }); +C.prototype[Symbol.toStringTag] = "C"; + +Object.defineProperty(window, "D", { + enumerable: false, + writable: true, + configurable: true, + value: function D() {} + }); +Object.defineProperty(window.D, "prototype", { + writable: false, + value: window.D.prototype + }); +C.prototype[Symbol.toStringTag] = "D"; +Object.defineProperty(window, "B", { + enumerable: false, + writable: true, + configurable: true, + value: window.A + }); + +var idlArray = new IdlArray(); +idlArray.add_idls(` +[Exposed=*, LegacyWindowAlias=B] interface A {}; +[Exposed=*] partial interface A {}; +[Exposed=Window] interface C {}; +[Exposed=*] partial interface C {}; +[Exposed=*] interface D {}; +[Exposed=Window] partial interface D {}; +`); +idlArray.add_objects({ + Window: ["window"] +}); +idlArray.test(); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "name": "A interface object length", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "A interface object name", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "A interface: existence and properties of interface object", + "properties": {}, + "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw", + "status_string": "FAIL" + }, + { + "name": "A interface: existence and properties of interface prototype object", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "A interface: existence and properties of interface prototype object's \"constructor\" property", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "A interface: existence and properties of interface prototype object's @@unscopables property", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "A interface: legacy window alias", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "C interface object length", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "C interface object name", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "C interface: existence and properties of interface object", + "properties": {}, + "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw", + "status_string": "FAIL" + }, + { + "name": "C interface: existence and properties of interface prototype object", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "C interface: existence and properties of interface prototype object's \"constructor\" property", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "C interface: existence and properties of interface prototype object's @@unscopables property", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "D interface object length", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "D interface object name", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "D interface: existence and properties of interface object", + "properties": {}, + "message": "assert_throws_js: interface object didn't throw TypeError when called as a function function \"function() {\n interface_object();\n }\" did not throw", + "status_string": "FAIL" + }, + { + "name": "D interface: existence and properties of interface prototype object", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "D interface: existence and properties of interface prototype object's \"constructor\" property", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "D interface: existence and properties of interface prototype object's @@unscopables property", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "Partial interface A: original interface defined", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "Partial interface A: valid exposure set", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "Partial interface C: original interface defined", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "Partial interface C: valid exposure set", + "properties": {}, + "message": "Partial C interface is exposed everywhere, the original interface is not.", + "status_string": "FAIL" + }, + { + "name": "Partial interface D: original interface defined", + "properties": {}, + "message": null, + "status_string": "PASS" + }, + { + "name": "Partial interface D: valid exposure set", + "properties": {}, + "message": null, + "status_string": "PASS" + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html new file mode 100644 index 0000000000..5fe05915b0 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_immutable_prototype.html @@ -0,0 +1,298 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>idlharness: Immutable prototypes</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> +</head> +<body> +<script> +"use strict"; + +Object.defineProperty(window, "Foo", { + enumerable: false, + writable: true, + configurable: true, + value: function Foo() { + if (!new.target) { + throw new TypeError('Foo() must be called with new'); + } + } + }); +Object.defineProperty(window.Foo, "prototype", { + writable: false, + value: window.Foo.prototype + }); +Foo.prototype[Symbol.toStringTag] = "Foo"; + +var idlArray = new IdlArray(); +idlArray.add_untested_idls("interface EventTarget {};"); +idlArray.add_idls( + "[Global=Window, Exposed=Window]\n" + + "interface Window : EventTarget {};\n" + + + "[Global=Window, Exposed=Window]\n" + + "interface Foo { constructor(); };" + ); +idlArray.add_objects({ + Foo: ["new Foo()"], + Window: ["window"] +}); +idlArray.test(); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "name": "Foo interface object length", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface object name", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: existence and properties of interface object", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: existence and properties of interface prototype object", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError", + "status_string": "FAIL", + "properties": {}, + "message": "assert_throws_js: function \"function() {\n Object.setPrototypeOf(obj, newValue);\n }\" did not throw" + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false", + "status_string": "FAIL", + "properties": {}, + "message": "assert_false: expected false got true" + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError", + "status_string": "FAIL", + "properties": {}, + "message": "assert_throws_js: function \"function() {\n obj.__proto__ = newValue;\n }\" did not throw" + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError", + "status_string": "FAIL", + "properties": {}, + "message": "assert_throws_js: function \"function() {\n Object.setPrototypeOf(obj, newValue);\n }\" did not throw" + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false", + "status_string": "FAIL", + "properties": {}, + "message": "assert_false: expected false got true" + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError", + "status_string": "FAIL", + "properties": {}, + "message": "assert_throws_js: function \"function() {\n obj.__proto__ = newValue;\n }\" did not throw" + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo must be primary interface of new Foo()", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Stringification of new Foo()", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Stringification of window", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface object length", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface object name", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: existence and properties of interface object", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: existence and properties of interface prototype object", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: existence and properties of interface prototype object's \"constructor\" property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: existence and properties of interface prototype object's @@unscopables property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Object.setPrototypeOf should throw a TypeError", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via Reflect.setPrototypeOf should return false", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to a new value via __proto__ should throw a TypeError", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Object.setPrototypeOf should not throw", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via Reflect.setPrototypeOf should return true", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of global platform object - setting to its original value via __proto__ should not throw", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Object.setPrototypeOf should throw a TypeError", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via Reflect.setPrototypeOf should return false", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to a new value via __proto__ should throw a TypeError", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Object.setPrototypeOf should not throw", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via Reflect.setPrototypeOf should return true", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window interface: internal [[SetPrototypeOf]] method of interface prototype object - setting to its original value via __proto__ should not throw", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Window must be primary interface of window", + "status_string": "PASS", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html new file mode 100644 index 0000000000..be2844e698 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_interface_mixin.html @@ -0,0 +1,131 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>idlharness: interface mixins</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> +</head> + +<body> + <p>Verify the series of sub-tests that are executed for "interface mixin" objects.</p> + <script> + "use strict"; + + // Simple includes statement (valid) + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls(` + [Exposed=Window] interface I1 {}; + interface mixin M1 { attribute any a1; }; + I1 includes M1;`); + idlArray.merge_partials(); + idlArray.merge_mixins(); + })(); + + // Partial interface mixin (valid) + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls(` + [Exposed=Window] interface I2 {}; + interface mixin M2 {}; + partial interface mixin M2 { attribute any a2; }; + I2 includes M2;`); + idlArray.merge_partials(); + idlArray.merge_mixins(); + })(); + + // Partial interface mixin without original mixin + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls('partial interface mixin M3 {};'); + idlArray.merge_partials(); + idlArray.merge_mixins(); + })(); + + // Name clash between mixin and partial mixin + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls(` + interface mixin M4 { attribute any a4; }; + partial interface mixin M4 { attribute any a4; };`); + idlArray.merge_partials(); + idlArray.merge_mixins(); + })(); + + // Name clash between interface and mixin + (() => { + const idlArray = new IdlArray(); + idlArray.add_untested_idls(` + interface mixin M5 { attribute any a5; }; + interface I5 { attribute any a5; }; + I5 includes M5;`); + idlArray.merge_partials(); + idlArray.merge_mixins(); + })(); + </script> + <script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "name": "I1 includes M1: member names are unique", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "I2 includes M2: member names are unique", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "I5 includes M5: member names are unique", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: member a5 is unique expected true got false" + }, + { + "name": "Partial interface mixin M2: member names are unique", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial interface mixin M2: original interface mixin defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial interface mixin M3: original interface mixin defined", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: Original interface mixin should be defined expected true got false" + }, + { + "name": "Partial interface mixin M4: member names are unique", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: member a4 is unique expected true got false" + }, + { + "name": "Partial interface mixin M4: original interface mixin defined", + "status_string": "PASS", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> + +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html new file mode 100644 index 0000000000..7dd9e676af --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_partial_interface_of.html @@ -0,0 +1,187 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>idlharness: Partial interface</title> + <script src="/resources/test/variants.js"></script> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> +</head> + +<body> + <p>Verify the series of sub-tests that are executed for "partial" interface objects.</p> + <script> + "use strict"; + + // No original existence + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls('partial interface A {};'); + idlArray.test(); + })(); + + // Valid exposure (Note: Worker -> {Shared,Dedicated,Service}Worker) + (() => { + const idlArray = new IdlArray(); + idlArray.add_untested_idls(` + [Exposed=(Worker)] + interface B {}; + + [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)] + interface C {};`); + idlArray.add_idls(` + [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)] + partial interface B {}; + + [Exposed=(Worker)] + partial interface C {};`); + idlArray.merge_partials(); + })(); + + // Invalid exposure + (() => { + const idlArray = new IdlArray(); + idlArray.add_untested_idls(` + [Exposed=(Window, ServiceWorker)] + interface D {};`); + idlArray.add_idls(` + [Exposed=(DedicatedWorker)] + partial interface D {};`); + idlArray.merge_partials(); + })(); + + // Original is a namespace, not an interface. + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls(` + partial interface E {}; + namespace E {};`); + idlArray.merge_partials(); + })(); + + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls(` + partial interface F {}; + partial interface mixin G {}; + `); + idlArray.add_dependency_idls(` + interface F {}; + interface mixin G {}; + interface mixin H {}; + F includes H; + I includes H; + J includes G; + interface K : J {}; + interface L : F {}; + `); + test(() => { + // Convert idlArray.includes into a Map from name of target interface to + // name of included mixin. (This assumes each interface includes at most + // one mixin, otherwise later includes would clobber earlier ones.) + const includes = new Map(idlArray.includes.map(i => [i.target, i.includes])); + // F is tested, so H is a dep. + assert_equals(includes.get('F'), 'H', 'F should be picked up'); + // H is not tested, so I is not a dep. + assert_false(includes.has('I'), 'I should be ignored'); + // G is a dep, so J is a dep. + assert_equals(includes.get('J'), 'G', 'J should be picked up'); + // K isn't a dep because J isn't defined. + assert_false('K' in idlArray.members, 'K should be ignored'); + // L isn't a dep because F is untested. + assert_false('L' in idlArray.members, 'L should be ignored'); + }, 'partial mixin dep implications'); + })(); + + // Name clash (partials) + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls(` + interface M { attribute any A; }; + partial interface M { attribute any A; };`); + idlArray.merge_partials(); + })(); + </script> + <script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "name": "Partial interface A: original interface defined", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: Original interface should be defined expected true got false" + }, + { + "name": "Partial interface B: original interface defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial interface B: valid exposure set", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial interface C: original interface defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial interface C: valid exposure set", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial interface D: original interface defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial interface D: valid exposure set", + "status_string": "FAIL", + "properties": {}, + "message": "Partial D interface is exposed to 'DedicatedWorker', the original interface is not." + }, + { + "name": "Partial interface E: original interface defined", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: Original E definition should have type interface expected true got false" + }, + { + "name": "partial mixin dep implications", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial interface M: original interface defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial interface M: member names are unique", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: member A is unique expected true got false" + } + ], + "type": "complete" +} +</script> +</body> + +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html new file mode 100644 index 0000000000..309de60bb7 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_primary_interface_of.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>idlharness: Primary interface</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> +</head> +<body> +<p>Verify the series of sub-tests that are executed for "tested" interface +objects but skipped for "untested" interface objects.</p> +<script> +"use strict"; + +function FooParent() { + if (!new.target) { + throw new TypeError('FooParent() must be called with new'); + } +} +Object.defineProperty(window, "Foo", { + enumerable: false, + writable: true, + configurable: true, + value: function Foo() { + if (!new.target) { + throw new TypeError('Foo() must be called with new'); + } + } + }); +Object.defineProperty(window.Foo, "prototype", { + writable: false, + value: new FooParent() + }); +Object.defineProperty(window.Foo.prototype, "constructor", { + enumerable: false, + writable: true, + configurable: true, + value: window.Foo + }); +Object.setPrototypeOf(Foo, FooParent); +Foo.prototype[Symbol.toStringTag] = "Foo"; + +var idlArray = new IdlArray(); +idlArray.add_untested_idls("interface FooParent {};"); +idlArray.add_idls( + "interface Foo : FooParent { constructor(); };" + ); +idlArray.add_objects({ + Foo: ["new Foo()"], + FooParent: ["new FooParent()"] +}); +idlArray.test(); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "name": "Foo interface object length", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface object name", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: existence and properties of interface object", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: existence and properties of interface prototype object", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: existence and properties of interface prototype object's \"constructor\" property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo interface: existence and properties of interface prototype object's @@unscopables property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Foo must be primary interface of new Foo()", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Stringification of new Foo()", + "status_string": "PASS", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html new file mode 100644 index 0000000000..bbc502a313 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlInterface/test_to_json_operation.html @@ -0,0 +1,177 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>IdlInterface.prototype.test_to_json_operation()</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> + <script src="../../../../idl-helper.js"></script> +</head> +<body> +<script> + "use strict"; + function wrap(member, obj) { + function F(obj) { + this._obj = obj; + } + + F.prototype.toJSON = function() { + return this._obj; + } + Object.defineProperty(F, 'name', { value: member.name }); + return new F(obj); + } + + var i, obj; + i = interfaceFrom("interface A { [Default] object toJSON(); attribute long foo; };"); + i.test_to_json_operation("object", wrap(i, { foo: 123 }), i.members[0]); + + // should fail (wrong type) + i = interfaceFrom("interface B { [Default] object toJSON(); attribute long foo; };"); + i.test_to_json_operation("object", wrap(i, { foo: "a value" }), i.members[0]); + + // should handle extraneous attributes (e.g. from an extension specification) + i = interfaceFrom("interface C { [Default] object toJSON(); attribute long foo; };"); + i.test_to_json_operation("object", wrap(i, { foo: 123, bar: 456 }), i.members[0]); + + // should fail (missing property) + i = interfaceFrom("interface D { [Default] object toJSON(); attribute long foo; };"); + i.test_to_json_operation("object", wrap(i, { }), i.members[0]); + + // should fail (should be writable) + obj = Object.defineProperties({}, { foo: { + writable: false, + enumerable: true, + configurable: true, + value: 123 + }}); + i = interfaceFrom("interface F { [Default] object toJSON(); attribute long foo; };"); + i.test_to_json_operation("object", wrap(i, obj), i.members[0]); + + // should fail (should be enumerable) + obj = Object.defineProperties({}, { foo: { + writable: true, + enumerable: false, + configurable: true, + value: 123 + }}); + i = interfaceFrom("interface G { [Default] object toJSON(); attribute long foo; };"); + i.test_to_json_operation("object", wrap(i, obj), i.members[0]); + + // should fail (should be configurable) + obj = Object.defineProperties({}, { foo: { + writable: true, + enumerable: true, + configurable: false, + value: 123 + }}); + i = interfaceFrom("interface H { [Default] object toJSON(); attribute long foo; };"); + i.test_to_json_operation("object", wrap(i, obj), i.members[0]); + + var idl = new IdlArray(); + idl.add_idls("interface I : J { [Default] object toJSON(); attribute long foo; };"); + idl.add_idls("interface J { [Default] object toJSON(); attribute DOMString foo;};"); + var i = idl.members.I; + i.test_to_json_operation("object", wrap(i, { foo: 123 }), i.members[0]); + + i = interfaceFrom("interface K { [Default] object toJSON(); };"); + i.test_to_json_operation("object", wrap(i, {}), i.members[0]); + + i = interfaceFrom("interface L { DOMString toJSON(); };"); + i.test_to_json_operation("object", wrap(i, "a string"), i.members[0]); + + // should fail (wrong output type) + i = interfaceFrom("interface M { DOMString toJSON(); };"); + i.test_to_json_operation("object", wrap(i, {}), i.members[0]); + + // should fail (not an IDL type) + i = interfaceFrom("interface N { DOMException toJSON(); };"); + i.test_to_json_operation("object", wrap(i, {}), i.members[0]); +</script> +<script type="text/json" id="expected"> + { + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "message": null, + "name": "A interface: default toJSON operation on object", + "properties": {}, + "status_string": "PASS" + }, + { + "message": "assert_equals: expected \"number\" but got \"string\"", + "name": "B interface: default toJSON operation on object", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": null, + "name": "C interface: default toJSON operation on object", + "properties": {}, + "status_string": "PASS" + }, + { + "message": "assert_true: property \"foo\" should be present in the output of D.prototype.toJSON() expected true got false", + "name": "D interface: default toJSON operation on object", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": "assert_true: property foo should be writable expected true got false", + "name": "F interface: default toJSON operation on object", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": "assert_true: property foo should be enumerable expected true got false", + "name": "G interface: default toJSON operation on object", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": "assert_true: property foo should be configurable expected true got false", + "name": "H interface: default toJSON operation on object", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": null, + "name": "I interface: default toJSON operation on object", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "K interface: default toJSON operation on object", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "L interface: toJSON operation on object", + "properties": {}, + "status_string": "PASS" + }, + { + "message": "assert_equals: expected \"string\" but got \"object\"", + "name": "M interface: toJSON operation on object", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": "assert_true: {\"type\":\"return-type\",\"extAttrs\":[],\"generic\":\"\",\"nullable\":false,\"union\":false,\"idlType\":\"DOMException\"} is not an appropriate return value for the toJSON operation of N expected true got false", + "name": "N interface: toJSON operation on object", + "properties": {}, + "status_string": "FAIL" + } + ], + "type": "complete" + } +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html new file mode 100644 index 0000000000..2c94061fc1 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_attribute.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>idlharness: namespace attribute</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> +</head> +<body> +<p>Verify the series of sub-tests that are executed for namespace attributes.</p> +<script> +"use strict"; + +Object.defineProperty(self, "foo", { + value: { + truth: true, + }, + writable: true, + enumerable: false, + configurable: true, +}); + +var idlArray = new IdlArray(); +idlArray.add_idls(` +[Exposed=Window] +namespace foo { + readonly attribute bool truth; + readonly attribute bool lies; +};`); +idlArray.test(); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "name": "foo namespace: extended attributes", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: property descriptor", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: [[Extensible]] is true", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: [[Prototype]] is Object.prototype", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: typeof is \"object\"", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: has no length property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: has no name property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: attribute truth", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: attribute lies", + "status_string": "FAIL", + "properties": {}, + "message": "assert_own_property: foo does not have property \"lies\" expected property \"lies\" missing" + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html new file mode 100644 index 0000000000..da70c8fa31 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_operation.html @@ -0,0 +1,242 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>idlharness: namespace operation</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> +</head> +<body> +<p>Verify the series of sub-tests that are executed for namespace operations.</p> +<script> +"use strict"; + +Object.defineProperty(self, "foo", { + value: Object.defineProperty({}, "Truth", { + value: function() {}, + writable: true, + enumerable: true, + configurable: true, + }), + writable: true, + enumerable: false, + configurable: true, +}); + +Object.defineProperty(self, "bar", { + value: Object.defineProperty({}, "Truth", { + value: function() {}, + writable: false, + enumerable: true, + configurable: false, + }), + writable: true, + enumerable: false, + configurable: true, +}); + +Object.defineProperty(self, "baz", { + value: { + LongStory: function(hero, ...details) { + return `${hero} went and ${details.join(', then')}` + }, + ShortStory: function(...details) { + return `${details.join('. ')}`; + }, + }, + writable: true, + enumerable: false, + configurable: true, +}); + +var idlArray = new IdlArray(); +idlArray.add_idls(` +[Exposed=Window] +namespace foo { + undefined Truth(); + undefined Lies(); +}; +[Exposed=Window] +namespace bar { + [LegacyUnforgeable] + undefined Truth(); +}; +[Exposed=Window] +namespace baz { + DOMString LongStory(any hero, DOMString... details); + DOMString ShortStory(DOMString... details); +};`); +idlArray.test(); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "name": "foo namespace: extended attributes", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: property descriptor", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: [[Extensible]] is true", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: [[Prototype]] is Object.prototype", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: typeof is \"object\"", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: has no length property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: has no name property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: operation Truth()", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "foo namespace: operation Lies()", + "status_string": "FAIL", + "properties": {}, + "message": "assert_own_property: namespace object missing operation \"Lies\" expected property \"Lies\" missing" + }, + { + "name": "bar namespace: extended attributes", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "bar namespace: property descriptor", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "bar namespace: [[Extensible]] is true", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "bar namespace: [[Prototype]] is Object.prototype", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "bar namespace: typeof is \"object\"", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "bar namespace: has no length property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "bar namespace: has no name property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "bar namespace: operation Truth()", + "status_string": "PASS", + "properties": {}, + "message": null + }, + + { + "name": "baz namespace: extended attributes", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "baz namespace: property descriptor", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "baz namespace: [[Extensible]] is true", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "baz namespace: [[Prototype]] is Object.prototype", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "baz namespace: typeof is \"object\"", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "baz namespace: has no length property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "baz namespace: has no name property", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "baz namespace: operation LongStory(any, DOMString...)", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "baz namespace: operation ShortStory(DOMString...)", + "status_string": "PASS", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html new file mode 100644 index 0000000000..eabdcd10a9 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/idlharness/IdlNamespace/test_partial_namespace.html @@ -0,0 +1,125 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>idlharness: Partial namespace</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> +</head> + +<body> + <p>Verify the series of sub-tests that are executed for "partial" namespace objects.</p> + <script> + "use strict"; + + // No original existence + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls('partial namespace A {};'); + idlArray.test(); + })(); + + // Valid exposure (Note: Worker -> {Shared,Dedicated,Service}Worker) + (() => { + const idlArray = new IdlArray(); + idlArray.add_untested_idls(` + [Exposed=(Worker)] + namespace B {}; + + [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)] + namespace C {};`); + idlArray.add_idls(` + [Exposed=(DedicatedWorker, ServiceWorker, SharedWorker)] + partial namespace B {}; + + [Exposed=(Worker)] + partial namespace C {};`); + idlArray.merge_partials(); + })(); + + // Invalid exposure + (() => { + const idlArray = new IdlArray(); + idlArray.add_untested_idls(` + [Exposed=(Window, ServiceWorker)] + namespace D {};`); + idlArray.add_idls(` + [Exposed=(DedicatedWorker)] + partial namespace D {};`); + idlArray.merge_partials(); + })(); + + // Original is an interface, not a namespace. + (() => { + const idlArray = new IdlArray(); + idlArray.add_idls(` + partial namespace E {}; + interface E {};`); + idlArray.merge_partials(); + })(); + </script> + <script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "name": "Partial namespace A: original namespace defined", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: Original namespace should be defined expected true got false" + }, + { + "name": "Partial namespace B: original namespace defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial namespace B: valid exposure set", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial namespace C: original namespace defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial namespace C: valid exposure set", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial namespace D: original namespace defined", + "status_string": "PASS", + "properties": {}, + "message": null + }, + { + "name": "Partial namespace D: valid exposure set", + "status_string": "FAIL", + "properties": {}, + "message": "Partial D namespace is exposed to 'DedicatedWorker', the original namespace is not." + }, + { + "name": "Partial namespace E: original namespace defined", + "status_string": "FAIL", + "properties": {}, + "message": "assert_true: Original E definition should have type namespace expected true got false" + } + ], + "type": "complete" +} +</script> +</body> + +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html new file mode 100644 index 0000000000..f49d0aa6b8 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-callback.html @@ -0,0 +1,116 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Example with iframe that notifies containing document via callbacks</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body onload="start_test_in_iframe()"> +<h1>Callbacks From Tests Running In An IFRAME</h1> +<p>A test is run inside an <tt>iframe</tt> with a same origin document. The +containing document should receive callbacks as the tests progress inside the +<tt>iframe</tt>. A single passing test is expected in the summary below. +<div id="log"></div> + +<script> +var callbacks = []; +var START = 1 +var TEST_STATE = 2 +var RESULT = 3 +var COMPLETION = 4 +var test_complete = false; + +setup({explicit_done: true}); + +// The following callbacks are called for tests in this document as well as the +// tests in the IFRAME. Currently, callbacks invoked from this document and any +// child document are indistinguishable from each other. + +function start_callback(properties) { + callbacks.push(START); +} + +function test_state_callback(test) { + callbacks.push(TEST_STATE); +} + +function result_callback(test) { + callbacks.push(RESULT); +} + +function completion_callback(tests, status) { + if (test_complete) { + return; + } + test_complete = true; + callbacks.push(COMPLETION); + verify_received_callbacks(); + done(); +} + +function verify_received_callbacks() { + var copy_of_callbacks = callbacks.slice(0); + + // Note that you can't run test assertions directly in a callback even if + // this is a file test. When the callback is invoked from a same-origin child + // page, the callstack reaches into the calling child document. Any + // exception thrown in a callback will be handled by the child rather than + // this document. + test( + function() { + // callbacks list should look like: + // START 1*(TEST_STATE) RESULT COMPLETION + assert_equals(copy_of_callbacks.shift(), START, + "The first received callback should be 'start_callback'."); + assert_equals(copy_of_callbacks.shift(), TEST_STATE, + "'test_state_callback' should be received before any " + + "result or completion callbacks."); + while(copy_of_callbacks.length > 0) { + var callback = copy_of_callbacks.shift(); + if (callback != TEST_STATE) { + copy_of_callbacks.unshift(callback); + break; + } + } + assert_equals(copy_of_callbacks.shift(), RESULT, + "'test_state_callback' should be followed by 'result_callback'."); + assert_equals(copy_of_callbacks.shift(), COMPLETION, + "Final 'result_callback' should be followed by 'completion_callback'."); + assert_equals(copy_of_callbacks.length, 0, + "'completion_callback' should be the last callback."); + }); +} + +function start_test_in_iframe() { + // This document is going to clear any received callbacks and maintain + // radio silence until the test in the iframe runs to completion. The + // completion_callback() will then complete the testing on this document. + callbacks.length = 0; + var iframe = document.createElement("iframe"); + // single-page-test-pass.html has a single test. + iframe.src = "single-page-test-pass.html"; + iframe.style.setProperty("display", "none"); + document.getElementById("target").appendChild(iframe); +} +</script> + +<div id="target"> +</div> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Example with iframe that notifies containing document via callbacks", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html new file mode 100644 index 0000000000..ef9b8702ec --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-errors.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Example with iframe that consolidates errors via fetch_tests_from_window</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +var parent_test = async_test("Test executing in parent context"); +</script> +</head> +<body onload="parent_test.done()"> +<h1>Fetching Tests From a Child Context</h1> +<p>This test demonstrates the use of <tt>fetch_tests_from_window</tt> to pull +tests from an <tt>iframe</tt> into the primary document.</p> +<p>The test suite is expected to fail due to an unhandled exception in the +child context.</p> +<div id="log"></div> + +<iframe id="childContext" src="uncaught-exception-handle.html" style="display:none"></iframe> +<!-- apisample4.html is a failing suite due to an unhandled Error. --> + +<script> + var childContext = document.getElementById("childContext"); + fetch_tests_from_window(childContext.contentWindow); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Error in remote: Error: Example Error" + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Test executing in parent context", + "properties": {}, + "message": null + }, + { + "status_string": "NOTRUN", + "name": "This should show a harness status of 'Error' and a test status of 'Not Run'", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html new file mode 100644 index 0000000000..246dddee11 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-consolidate-tests.html @@ -0,0 +1,85 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Example with iframe that consolidates tests via fetch_tests_from_window</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +var parent_test = async_test("Test executing in parent context"); +</script> +</head> +<body onload="parent_test.done()"> +<h1>Fetching Tests From a Child Context</h1> +<p>This test demonstrates the use of <tt>fetch_tests_from_window</tt> to pull +tests from an <tt>iframe</tt> into the primary document.</p> +<p>The test suite will not complete until tests in the child context have finished +executing</p> +<div id="log"></div> + +<iframe id="childContext" src="promise-async.html" style="display:none"></iframe> + +<script> + var childContext = document.getElementById("childContext"); + fetch_tests_from_window(childContext.contentWindow); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Promise rejection", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Promise resolution", + "properties": {}, + "message": null + }, + { + "status_string": "FAIL", + "name": "Promises and test assertion failures (should fail)", + "properties": {}, + "message": "assert_true: This failure is expected expected true got false" + }, + { + "status_string": "PASS", + "name": "Promises are supported in your browser", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Promises resolution chaining", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Test executing in parent context", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Use of step_func with Promises", + "properties": {}, + "message": null + }, + { + "status_string": "FAIL", + "name": "Use of unreached_func with Promises (should fail)", + "properties": {}, + "message": "assert_unreached: This failure is expected Reached unreachable code" + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html b/testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html new file mode 100644 index 0000000000..283a5d98cc --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/iframe-msg.html @@ -0,0 +1,84 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Example with iframe that notifies containing document via cross document messaging</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<h1>Notifications From Tests Running In An IFRAME</h1> +<p>A test is run inside an <tt>iframe</tt> with a same origin document. The +containing document should receive messages via <tt>postMessage</tt>/ +<tt>onmessage</tt> as the tests progress inside the <tt>iframe</tt>. A single +passing test is expected in the summary below. +</p> +<div id="log"></div> + +<script> +var t = async_test("Containing document receives messages"); +var start_received = false; +var result_received = false; +var completion_received = false; + +// These are the messages that are expected to be seen while running the tests +// in the IFRAME. +var expected_messages = [ + t.step_func( + function(message) { + assert_equals(message.data.type, "start"); + assert_own_property(message.data, "properties"); + }), + + t.step_func( + function(message) { + assert_equals(message.data.type, "test_state"); + assert_equals(message.data.test.status, message.data.test.NOTRUN); + }), + + t.step_func( + function(message) { + assert_equals(message.data.type, "result"); + assert_equals(message.data.test.status, message.data.test.PASS); + }), + + t.step_func( + function(message) { + assert_equals(message.data.type, "complete"); + assert_equals(message.data.tests.length, 1); + assert_equals(message.data.tests[0].status, + message.data.tests[0].PASS); + assert_equals(message.data.status.status, message.data.status.OK); + t.done(); + }), + + t.unreached_func("Too many messages received") +]; + +on_event(window, + "message", + function(message) { + var handler = expected_messages.shift(); + handler(message); + }); +</script> +<iframe src="single-page-test-pass.html" style="display:none"> + <!-- single-page-test-pass.html implements a file_is_test test. --> +</iframe> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Containing document receives messages", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> diff --git a/testing/web-platform/tests/resources/test/tests/functional/log-insertion.html b/testing/web-platform/tests/resources/test/tests/functional/log-insertion.html new file mode 100644 index 0000000000..9a63c3dbde --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/log-insertion.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML> +<title>Log insertion</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +test(function(t) { + assert_equals(document.body, null); +}, "Log insertion before load"); +test(function(t) { + assert_equals(document.body, null); +}, "Log insertion before load (again)"); +async_test(function(t) { + window.onload = t.step_func_done(function() { + var body = document.body; + assert_not_equals(body, null); + + var log = document.getElementById("log"); + assert_equals(log.parentNode, body); + }); +}, "Log insertion after load"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [{ + "status_string": "PASS", + "name": "Log insertion before load", + "message": null, + "properties": {} + }, { + "status_string": "PASS", + "name": "Log insertion before load (again)", + "message": null, + "properties": {} + }, { + "status_string": "PASS", + "name": "Log insertion after load", + "message": null, + "properties": {} + }], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/no-title.html b/testing/web-platform/tests/resources/test/tests/functional/no-title.html new file mode 100644 index 0000000000..a337e4e5f5 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/no-title.html @@ -0,0 +1,146 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Tests with no title</title> +</head> +<script src="/resources/testharness.js"></script> + +<body> +<h1>Tests with no title</h1> +<div id="log"></div> +<script> + test(function(){assert_true(true, '1')}); + test(()=>assert_true(true, '2')); + test(() => assert_true(true, '3')); + test(() => assert_true(true, '3')); // test duplicate behaviour + test(() => assert_true(true, '3')); // test duplicate behaviour + test(() => { + assert_true(true, '4'); + }); + test(() => { assert_true(true, '5') }); + test(() => { assert_true(true, '6') } ); + test(() => { assert_true(true, '7'); }); + test(() => {}); + test(() => { }); + test(() => {;;;;}); + test(() => { ; ; ; ; }); + test(()=>{}); + test(()=>{ }); + test(()=>{;;;;}); + test(()=>{ ; ; ; ; }); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Tests with no title", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "assert_true(true, '2')", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "assert_true(true, '3')", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "assert_true(true, '3') 1", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "assert_true(true, '3') 2", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Tests with no title 1", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "assert_true(true, '5')", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "assert_true(true, '6')", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "assert_true(true, '7')", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Tests with no title 2", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Tests with no title 3", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Tests with no title 4", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Tests with no title 5", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Tests with no title 6", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Tests with no title 7", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Tests with no title 8", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Tests with no title 9", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/order.html b/testing/web-platform/tests/resources/test/tests/functional/order.html new file mode 100644 index 0000000000..686383861a --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/order.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Ordering</title> +<meta name="timeout" content="6000"> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +test(function() {}, 'second'); +test(function() {}, 'first'); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [{ + "status_string": "PASS", + "name": "first", + "message": null, + "properties": {} + }, { + "status_string": "PASS", + "name": "second", + "message": null, + "properties": {} + }], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/promise-async.html b/testing/web-platform/tests/resources/test/tests/functional/promise-async.html new file mode 100644 index 0000000000..fa82665cf0 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/promise-async.html @@ -0,0 +1,172 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Async Tests and Promises</title> +</head> +<body> +<h1>Async Tests and Promises</h1> +<p>This test assumes ECMAScript 6 Promise support. Some failures are expected.</p> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + +test(function() { + var p = new Promise(function(resolve, reject) {}); + assert_true('then' in p); + assert_equals(typeof Promise.resolve, 'function'); + assert_equals(typeof Promise.reject, 'function'); +}, "Promises are supported in your browser"); + +(function() { + var t = async_test("Promise resolution"); + t.step(function() { + Promise.resolve('x').then( + t.step_func(function(value) { + assert_equals(value, 'x'); + t.done(); + }), + t.unreached_func('Promise should not reject') + ); + }); +}()); + +(function() { + var t = async_test("Promise rejection"); + t.step(function() { + Promise.reject(Error('fail')).then( + t.unreached_func('Promise should reject'), + t.step_func(function(reason) { + assert_true(reason instanceof Error); + assert_equals(reason.message, 'fail'); + t.done(); + }) + ); + }); +}()); + +(function() { + var t = async_test("Promises resolution chaining"); + t.step(function() { + var resolutions = []; + Promise.resolve('a').then( + t.step_func(function(value) { + resolutions.push(value); + return 'b'; + }) + ).then( + t.step_func(function(value) { + resolutions.push(value); + return 'c'; + }) + ).then( + t.step_func(function(value) { + resolutions.push(value); + + assert_array_equals(resolutions, ['a', 'b', 'c']); + t.done(); + }) + ).catch( + t.unreached_func('promise should not have rejected') + ); + }); +}()); + +(function() { + var t = async_test("Use of step_func with Promises"); + t.step(function() { + var resolutions = []; + Promise.resolve('x').then( + t.step_func_done(), + t.unreached_func('Promise should not have rejected') + ); + }); +}()); + +(function() { + var t = async_test("Promises and test assertion failures (should fail)"); + t.step(function() { + var resolutions = []; + Promise.resolve('x').then( + t.step_func(function(value) { + assert_true(false, 'This failure is expected'); + }) + ).then( + t.unreached_func('Promise should not have resolved') + ).catch( + t.unreached_func('Promise should not have rejected') + ); + }); +}()); + +(function() { + var t = async_test("Use of unreached_func with Promises (should fail)"); + t.step(function() { + var resolutions = []; + var r; + var p = new Promise(function(resolve, reject) { + // Reject instead of resolve, to demonstrate failure. + reject(123); + }); + p.then( + function(value) { + assert_equals(value, 123, 'This should not actually happen'); + }, + t.unreached_func('This failure is expected') + ); + }); +}()); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Promise rejection", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Promise resolution", + "properties": {}, + "message": null + }, + { + "status_string": "FAIL", + "name": "Promises and test assertion failures (should fail)", + "properties": {}, + "message": "assert_true: This failure is expected expected true got false" + }, + { + "status_string": "PASS", + "name": "Promises are supported in your browser", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Promises resolution chaining", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Use of step_func with Promises", + "properties": {}, + "message": null + }, + { + "status_string": "FAIL", + "name": "Use of unreached_func with Promises (should fail)", + "properties": {}, + "message": "assert_unreached: This failure is expected Reached unreachable code" + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html b/testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html new file mode 100644 index 0000000000..e8e680a9c7 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/promise-with-sync.html @@ -0,0 +1,79 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Promise Tests and Synchronous Tests</title> +</head> +<body> +<h1>Promise Tests</h1> +<p>This test demonstrates the use of <tt>promise_test</tt> alongside synchronous tests.</p> +<div id="log"></div> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +<script> +"use strict"; +var sequence = []; + +test(function(t) { + assert_array_equals(sequence, []); + sequence.push(1); +}, "first synchronous test"); + +promise_test(function() { + assert_array_equals(sequence, [1, 2]); + + return Promise.resolve() + .then(function() { + assert_array_equals(sequence, [1, 2]); + sequence.push(3); + }); +}, "first promise_test");; + +test(function(t) { + assert_array_equals(sequence, [1]); + sequence.push(2); +}, "second synchronous test"); + +promise_test(function() { + assert_array_equals(sequence, [1, 2, 3]); + + return Promise.resolve() + .then(function() { + assert_array_equals(sequence, [1, 2, 3]); + }); +}, "second promise_test");; +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "message": null, + "properties": {}, + "name": "first promise_test", + "status_string": "PASS" + }, + { + "message": null, + "properties": {}, + "name": "first synchronous test", + "status_string": "PASS" + }, + { + "message": null, + "properties": {}, + "name": "second promise_test", + "status_string": "PASS" + }, + { + "message": null, + "properties": {}, + "name": "second synchronous test", + "status_string": "PASS" + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/promise.html b/testing/web-platform/tests/resources/test/tests/functional/promise.html new file mode 100644 index 0000000000..f35feb0e21 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/promise.html @@ -0,0 +1,219 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Promise Tests</title> +</head> +<body> +<h1>Promise Tests</h1> +<p>This test demonstrates the use of <tt>promise_test</tt>. Assumes ECMAScript 6 +Promise support. Some failures are expected.</p> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +test( + function() { + var p = new Promise(function(resolve, reject){}); + assert_true("then" in p); + assert_equals(typeof Promise.resolve, "function"); + assert_equals(typeof Promise.reject, "function"); + }, + "Promises are supported in your browser"); + +promise_test( + function() { + return Promise.resolve("x") + .then( + function(value) { + assert_equals(value, + "x", + "Fulfilled promise should pass result to " + + "fulfill reaction."); + }); + }, + "Promise fulfillment with result"); + +promise_test( + function(t) { + return Promise.reject(new Error("fail")) + .then(t.unreached_func("Promise should reject"), + function(reason) { + assert_true( + reason instanceof Error, + "Rejected promise should pass reason to fulfill reaction."); + assert_equals( + reason.message, + "fail", + "Rejected promise should pass reason to reject reaction."); + }); + }, + "Promise rejection with result"); + +promise_test( + function() { + var resolutions = []; + return Promise.resolve("a") + .then( + function(value) { + resolutions.push(value); + return "b"; + }) + .then( + function(value) { + resolutions.push(value); + return "c"; + }) + .then( + function(value) { + resolutions.push(value); + assert_array_equals(resolutions, ["a", "b", "c"]); + }); + }, + "Chain of promise resolutions"); + +promise_test( + function(t) { + var resolutions = []; + return Promise.resolve("x") + .then( + function(value) { + assert_true(false, "Expected failure."); + }) + .then(t.unreached_func("UNEXPECTED FAILURE: Promise should not have resolved.")); + }, + "Assertion failure in a fulfill reaction (should FAIL with an expected failure)"); + +promise_test( + function(t) { + return new Promise( + function(resolve, reject) { + reject(123); + }) + .then(t.unreached_func("UNEXPECTED FAILURE: Fulfill reaction reached after rejection."), + t.unreached_func("Expected failure.")); + }, + "unreached_func as reactor (should FAIL with an expected failure)"); + +promise_test( + function() { + return true; + }, + "promise_test with function that doesn't return a Promise (should FAIL)"); + +promise_test(function(){}, + "promise_test with function that doesn't return anything"); + +promise_test( + function() { return { then: 23 }; }, + "promise_test that returns a non-thenable (should FAIL)"); + +promise_test( + function() { + return Promise.reject("Expected rejection"); + }, + "promise_test with unhandled rejection (should FAIL)"); + +promise_test( + function() { + return Promise.resolve(10) + .then( + function(value) { + throw Error("Expected exception."); + }); + }, + "promise_test with unhandled exception in fulfill reaction (should FAIL)"); + +promise_test( + function(t) { + return Promise.reject(10) + .then( + t.unreached_func("UNEXPECTED FAILURE: Fulfill reaction reached after rejection."), + function(value) { + throw Error("Expected exception."); + }); + }, + "promise_test with unhandled exception in reject reaction (should FAIL)"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "FAIL", + "name": "Assertion failure in a fulfill reaction (should FAIL with an expected failure)", + "message": "assert_true: Expected failure. expected true got false", + "properties": {} + }, + { + "status_string": "PASS", + "name": "Chain of promise resolutions", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Promise fulfillment with result", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Promise rejection with result", + "message": null, + "properties": {} + }, + { + "status_string": "PASS", + "name": "Promises are supported in your browser", + "message": null, + "properties": {} + }, + { + "status_string": "FAIL", + "name": "promise_test with function that doesn't return a Promise (should FAIL)", + "message": "promise_test: test body must return a 'thenable' object (received an object with no `then` method)", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "promise_test with function that doesn't return anything", + "message": "promise_test: test body must return a 'thenable' object (received undefined)", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "promise_test that returns a non-thenable (should FAIL)", + "message": "promise_test: test body must return a 'thenable' object (received an object with no `then` method)", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "promise_test with unhandled exception in fulfill reaction (should FAIL)", + "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "promise_test with unhandled exception in reject reaction (should FAIL)", + "message": "promise_test: Unhandled rejection with value: object \"Error: Expected exception.\"", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "promise_test with unhandled rejection (should FAIL)", + "message": "promise_test: Unhandled rejection with value: \"Expected rejection\"", + "properties": {} + }, + { + "status_string": "FAIL", + "name": "unreached_func as reactor (should FAIL with an expected failure)", + "message": "assert_unreached: Expected failure. Reached unreachable code", + "properties": {} + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/queue.html b/testing/web-platform/tests/resources/test/tests/functional/queue.html new file mode 100644 index 0000000000..0c721286ec --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/queue.html @@ -0,0 +1,130 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test queuing synchronous tests</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<div id="log"></div> + +<script> +"use strict"; +var inInitialTurn = true; + +test(function(t) { + assert_true( + inInitialTurn, "should execute in the initial turn of the event loop" + ); +}, "First synchronous test"); + +test(function(t) { + assert_true( + inInitialTurn, "should execute in the initial turn of the event loop" + ); +}, "Second synchronous test"); + +async_test(function(t) { + assert_true( + inInitialTurn, "should execute in the initial turn of the event loop" + ); + t.done(); +}, "First async_test (run in parallel)"); + +async_test(function(t) { + assert_true( + inInitialTurn, "should execute in the initial turn of the event loop" + ); + t.done(); +}, "Second async_test (run in parallel)"); + +test(function(t) { + assert_true( + inInitialTurn, "should execute in the initial turn of the event loop" + ); +}, "Third synchronous test"); + +promise_test(function(t) { + assert_false( + inInitialTurn, "should not execute in the initial turn of the event loop" + ); + + return Promise.resolve(); +}, "promise_test"); + +async_test(function(t) { + assert_true( + inInitialTurn, "should execute in the initial turn of the event loop" + ); + t.done(); +}, "Third async_test (run in parallel)"); + +test(function(t) { + assert_true( + inInitialTurn, "should execute in the initial turn of the event loop" + ); +}, "Fourth synchronous test"); + +inInitialTurn = false; +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "properties": {}, + "name": "First async_test (run in parallel)", + "status_string": "PASS", + "message": null + }, + { + "properties": {}, + "name": "First synchronous test", + "status_string": "PASS", + "message": null + }, + { + "properties": {}, + "name": "Fourth synchronous test", + "status_string": "PASS", + "message": null + }, + { + "properties": {}, + "name": "Second async_test (run in parallel)", + "status_string": "PASS", + "message": null + }, + { + "properties": {}, + "name": "Second synchronous test", + "status_string": "PASS", + "message": null + }, + { + "properties": {}, + "name": "Third async_test (run in parallel)", + "status_string": "PASS", + "message": null + }, + { + "properties": {}, + "name": "Third synchronous test", + "status_string": "PASS", + "message": null + }, + { + "properties": {}, + "name": "promise_test", + "status_string": "PASS", + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js b/testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js new file mode 100644 index 0000000000..82c1456aa6 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/setup-function-worker.js @@ -0,0 +1,14 @@ +importScripts("/resources/testharness.js"); + +// Regression test for https://github.com/web-platform-tests/wpt/issues/27299, +// where we broke the ability for a setup function in a worker to contain an +// assertion (even a passing one). +setup(function() { + assert_true(true, "True is true"); +}); + +// We must define at least one test for the harness, though it is not what we +// are testing here. +test(function() { + assert_false(false, "False is false"); +}, 'Worker test'); diff --git a/testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html b/testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html new file mode 100644 index 0000000000..9f24adac2d --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/setup-worker-service.html @@ -0,0 +1,86 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Setup function in a service worker</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<h1>Setup function in a service worker</h1> +<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/service-workers/">ServiceWorkers</a>. +<div id="log"></div> + +<script> +test(function(t) { + assert_true("serviceWorker" in navigator, + "navigator.serviceWorker exists"); +}, "Browser supports ServiceWorker"); + +promise_test(function() { + // Since the service worker registration could be in an indeterminate + // state (due to, for example, a previous test run failing), we start by + // unregstering our service worker and then registering it again. + var scope = "service-worker-scope"; + var worker_url = "setup-function-worker.js"; + + return navigator.serviceWorker.register(worker_url, {scope: scope}) + .then(function(registration) { + return registration.unregister(); + }).then(function() { + return navigator.serviceWorker.register(worker_url, {scope: scope}); + }).then(function(registration) { + add_completion_callback(function() { + registration.unregister(); + }); + + return new Promise(function(resolve) { + registration.addEventListener("updatefound", function() { + resolve(registration.installing); + }); + }); + }).then(function(worker) { + fetch_tests_from_worker(worker); + }); +}, "Register ServiceWorker"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Browser supports ServiceWorker", + "properties": {}, + "message": null + }, + { + "message": null, + "name": "Register ServiceWorker", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "Worker test", + "properties": {}, + "status_string": "PASS" + } + ], + "summarized_asserts": [ + { + "assert_name": "assert_true", + "test": "Browser supports ServiceWorker", + "args": [ + "true", + "\"navigator.serviceWorker exists\"" + ], + "status": 0 + } + ], + "type": "complete" +} +</script> +</body> diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html new file mode 100644 index 0000000000..8bbd530c48 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-fail.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<title>Example with file_is_test (should fail)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +setup({ single_test: true }); +onload = function() { + assert_true(false); + done(); +} +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "FAIL", + "name": "Example with file_is_test (should fail)", + "properties": {}, + "message": "uncaught exception: Error: assert_true: expected true got false" + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html new file mode 100644 index 0000000000..9b39d2a02c --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-assertions.html @@ -0,0 +1,25 @@ +<!DOCTYPE HTML> +<title>Example single page test with no asserts</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +setup({ single_test: true }); +done(); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Example single page test with no asserts", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html new file mode 100644 index 0000000000..cb018f4dae --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-no-body.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<title>Example single page test with no body</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +setup({ single_test: true }); +assert_true(true); +done(); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Example single page test with no body", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html new file mode 100644 index 0000000000..e143e22f3c --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/single-page-test-pass.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<title>Example with file_is_test</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +setup({ single_test: true }); +onload = function() { + assert_true(true); + done(); +} +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Example with file_is_test", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/step_wait.html b/testing/web-platform/tests/resources/test/tests/functional/step_wait.html new file mode 100644 index 0000000000..8235d9d48a --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/step_wait.html @@ -0,0 +1,79 @@ +<!doctype html> +<title>Tests for step_wait</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +promise_test(async t => { + let x = 1; + Promise.resolve().then(() => ++x); + await t.step_wait(() => x === 1); + assert_equals(x, 2); +}, "Basic step_wait() test"); + +promise_test(async t => { + let cond = false; + let x = 0; + setTimeout(() => cond = true, 100); + await t.step_wait(() => { + ++x; + return cond; + }); + assert_equals(x, 2); +}, "step_wait() isn't invoked too often"); + +promise_test(async t => { + await t.step_wait(); // Throws +}, "step_wait() takes an argument"); + +promise_test(async t => { + function wait(ms) { + return new Promise(resolve => step_timeout(resolve, ms)); + } + let x = 1; + step_timeout(() => { + ++x; + }, 100); + + await t.step_wait(async () => { + await wait(1); + return x === 2; + }); + assert_equals(x, 2); +}, "async step_wait()"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_tests": [ + { + "name": "Basic step_wait() test", + "message": null, + "properties": {}, + "status_string": "PASS" + }, + { + "name": "step_wait() isn't invoked too often", + "message": null, + "properties": {}, + "status_string": "PASS" + }, + { + "name": "step_wait() takes an argument", + "message": "cond is not a function", + "properties": {}, + "status_string": "FAIL" + }, + { + "name": "async step_wait()", + "message": null, + "properties": {}, + "status_string": "PASS" + } + ], + "type": "complete", + "summarized_status": { + "status_string": "OK", + "message": null + } +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html b/testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html new file mode 100644 index 0000000000..9fed18a3e2 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/step_wait_func.html @@ -0,0 +1,49 @@ +<!doctype html> +<title>Tests for step_wait_func and step_wait_func_done</title> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<div id=log></div> +<script> +async_test(t => { + let x = 0; + let step_x = 0; + setTimeout(() => ++x, 100); + t.step_wait_func(() => { + ++step_x; + return x === 1; + }, () => { + assert_equals(step_x, 2); + t.done(); + }); +}, "Basic step_wait_func() test"); + +async_test(t => { + let x = 0; + setTimeout(() => ++x, 100); + t.step_wait_func_done(() => true, () => assert_equals(x, 0)); +}, "Basic step_wait_func_done() test"); + +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "properties": {}, + "message": null, + "name": "Basic step_wait_func() test", + "status_string": "PASS" + }, + { + "properties": {}, + "message": null, + "name": "Basic step_wait_func_done() test", + "status_string": "PASS" + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html new file mode 100644 index 0000000000..9d8e5c11cc --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-promise-test.html @@ -0,0 +1,241 @@ +<!doctype html> +<title>testharness.js - task scheduling</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +<script> +var sameTask = null; +var sameMicrotask = null; +var expectedError = new Error('This error is expected'); + +promise_test(function() { + return Promise.resolve() + .then(function() { + sameMirotask = true; + Promise.resolve().then(() => sameMicrotask = false); + }); +}, 'promise test without cleanup #1'); + +promise_test(function() { + assert_false(sameMicrotask); + + return Promise.resolve(); +}, 'sub-test with 0 cleanup functions executes in distinct microtask from a passing sub-test'); + +promise_test(function() { + return Promise.resolve() + .then(function() { + sameMirotask = true; + Promise.resolve().then(() => sameMicrotask = false); + throw expectedError; + }); +}, 'failing promise test without cleanup #1'); + +promise_test(function() { + assert_false(sameMicrotask); + + return Promise.resolve(); +}, 'sub-test with 0 cleanup functions executes in distinct microtask from a failing sub-test'); + +promise_test(function(t) { + t.add_cleanup(function() {}); + + return Promise.resolve() + .then(function() { + sameMirotask = true; + Promise.resolve().then(() => sameMicrotask = false); + }); +}, 'promise test with cleanup #1'); + +promise_test(function() { + assert_false(sameMicrotask); + + return Promise.resolve(); +}, 'sub-test with some cleanup functions executes in distinct microtask from a passing sub-test'); + +promise_test(function(t) { + t.add_cleanup(function() {}); + + return Promise.resolve() + .then(function() { + sameMirotask = true; + Promise.resolve().then(() => sameMicrotask = false); + throw expectedError; + }); +}, 'failing promise test with cleanup #1'); + +promise_test(function() { + assert_false(sameMicrotask); + + return Promise.resolve(); +}, 'sub-test with some cleanup functions executes in distinct microtask from a failing sub-test'); + +promise_test(function(t) { + return Promise.resolve() + .then(function() { + sameTask = true; + t.step_timeout(() => sameTask = false, 0); + }); +}, 'promise test without cleanup #2'); + +promise_test(function() { + assert_true(sameTask); + + return Promise.resolve(); +}, 'sub-test with 0 cleanup functions executes in the same task as a passing sub-test'); + +promise_test(function(t) { + return Promise.resolve() + .then(function() { + sameTask = true; + t.step_timeout(() => sameTask = false, 0); + throw expectedError; + }); +}, 'failing promise test without cleanup #2'); + +promise_test(function() { + assert_true(sameTask); + + return Promise.resolve(); +}, 'sub-test with 0 cleanup functions executes in the same task as a failing sub-test'); + +promise_test(function(t) { + t.add_cleanup(function() {}); + + return Promise.resolve() + .then(function() { + sameTask = true; + t.step_timeout(() => sameTask = false, 0); + }); +}, 'promise test with cleanup #2'); + +promise_test(function() { + assert_true(sameTask); + + return Promise.resolve(); +}, 'sub-test with some cleanup functions executes in the same task as a passing sub-test'); + +promise_test(function(t) { + t.add_cleanup(function() {}); + + return Promise.resolve() + .then(function() { + sameTask = true; + t.step_timeout(() => sameTask = false, 0); + throw expectedError; + }); +}, 'failing promise test with cleanup #2'); + +promise_test(function() { + assert_true(sameTask); + + return Promise.resolve(); +}, 'sub-test with some cleanup functions executes in the same task as a failing sub-test'); +</script> + +<script type="text/json" id="expected"> +{ + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"", + "name": "failing promise test with cleanup #1", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"", + "name": "failing promise test with cleanup #2", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"", + "name": "failing promise test without cleanup #1", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": "promise_test: Unhandled rejection with value: object \"Error: This error is expected\"", + "name": "failing promise test without cleanup #2", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": null, + "name": "promise test with cleanup #1", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "promise test with cleanup #2", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "promise test without cleanup #1", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "promise test without cleanup #2", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with 0 cleanup functions executes in distinct microtask from a failing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with 0 cleanup functions executes in distinct microtask from a passing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with 0 cleanup functions executes in the same task as a failing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with 0 cleanup functions executes in the same task as a passing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with some cleanup functions executes in distinct microtask from a failing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with some cleanup functions executes in distinct microtask from a passing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with some cleanup functions executes in the same task as a failing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with some cleanup functions executes in the same task as a passing sub-test", + "properties": {}, + "status_string": "PASS" + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html new file mode 100644 index 0000000000..035844448d --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/task-scheduling-test.html @@ -0,0 +1,141 @@ +<!doctype html> +<title>testharness.js - task scheduling</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +<script> +var sameMicrotask = null; +var expectedError = new Error('This error is expected'); + +// Derived from `immediate` +// https://github.com/calvinmetcalf/immediate/blob/c353bd2106648cee1d525bfda22cfc4456e69c0e/lib/mutation.js +function microTask(callback) { + var observer = new MutationObserver(callback); + var element = document.createTextNode(''); + observer.observe(element, { + characterData: true + }); + + element.data = true; +}; + +async_test(function(t) { + var microtask_ran = false; + + t.step_timeout(t.step_func(function() { + assert_true(microtask_ran, 'function registered as a microtask was executed before task'); + t.done(); + }), 0); + + microTask(function() { + microtask_ran = true; + }); +}, 'precondition: microtask creation logic functions as expected'); + +test(function() { + sameMicrotask = true; + microTask(function() { sameMicrotask = false; }); +}, 'synchronous test without cleanup'); + +test(function() { + assert_true(sameMicrotask); +}, 'sub-test with 0 cleanup functions executes in the same microtask as a passing sub-test'); + +test(function() { + sameMicrotask = true; + microTask(function() { sameMicrotask = false; }); + throw expectedError; +}, 'failing synchronous test without cleanup'); + +test(function() { + assert_true(sameMicrotask); +}, 'sub-test with 0 cleanup functions executes in the same microtask as a failing sub-test'); + +test(function(t) { + t.add_cleanup(function() {}); + + sameMicrotask = true; + microTask(function() { sameMicrotask = false; }); +}, 'synchronous test with cleanup'); + +test(function() { + assert_true(sameMicrotask); +}, 'sub-test with some cleanup functions executes in the same microtask as a passing sub-test'); + +test(function(t) { + t.add_cleanup(function() {}); + + sameMicrotask = true; + microTask(function() { sameMicrotask = false; }); + throw expectedError; +}, 'failing synchronous test with cleanup'); + +test(function() { + assert_true(sameMicrotask); +}, 'sub-test with some cleanup functions executes in the same microtask as a failing sub-test'); +</script> + +<script type="text/json" id="expected"> +{ + "summarized_status": { + "message": null, + "status_string": "OK" + }, + "summarized_tests": [ + { + "message": "This error is expected", + "name": "failing synchronous test with cleanup", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": "This error is expected", + "name": "failing synchronous test without cleanup", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": null, + "name": "precondition: microtask creation logic functions as expected", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with 0 cleanup functions executes in the same microtask as a failing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with 0 cleanup functions executes in the same microtask as a passing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with some cleanup functions executes in the same microtask as a failing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "sub-test with some cleanup functions executes in the same microtask as a passing sub-test", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "synchronous test with cleanup", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "synchronous test without cleanup", + "properties": {}, + "status_string": "PASS" + } + ], + "type": "complete" +} +</script> diff --git a/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html new file mode 100644 index 0000000000..764b0c4055 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-handle.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Harness Handling Uncaught Exception</title> +</head> +<script src="/resources/testharness.js"></script> + +<body> +<h1>Harness Handling Uncaught Exception</h1> +<div id="log"></div> +<script> +var t = async_test("This should show a harness status of 'Error' and a test status of 'Not Run'"); +throw new Error("Example Error"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Error: Example Error" + }, + "summarized_tests": [ + { + "status_string": "NOTRUN", + "name": "This should show a harness status of 'Error' and a test status of 'Not Run'", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html new file mode 100644 index 0000000000..6bd0ddbb0d --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/uncaught-exception-ignore.html @@ -0,0 +1,35 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Harness Ignoring Uncaught Exception</title> +</head> +<script src="/resources/testharness.js"></script> + +<body> +<h1>Harness Ignoring Uncaught Exception</h1> +<div id="log"></div> +<script> +setup({allow_uncaught_exception:true}); +var t = async_test("setup({allow_uncaught_exception:true}) should allow tests to pass even if there is an exception"); +onerror = t.step_func(function() {t.done()}); +throw new Error("Example Error"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "setup({allow_uncaught_exception:true}) should allow tests to pass even if there is an exception", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html new file mode 100644 index 0000000000..ba28d4914f --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-allow.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Dedicated Worker Tests - Allowed Uncaught Exception</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<h1>Dedicated Web Worker Tests - Allowed Uncaught Exception</h1> +<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker. +<p>The test harness is expected to pass despite an uncaught exception in a worker because that worker is configured to allow uncaught exceptions.</p> +<div id="log"></div> + +<script> +test(function(t) { + assert_true("Worker" in self, "Browser should support Workers"); + }, + "Browser supports Workers"); + +fetch_tests_from_worker(new Worker("worker-uncaught-allow.js")); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Browser supports Workers", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "onerror event is triggered", + "properties": {}, + "message": null + } + ], + "type": "complete" +} +</script> +</body> diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html new file mode 100644 index 0000000000..486e067114 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated-uncaught-single.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Dedicated Worker Tests - Uncaught Exception in Single-Page Test</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<h1>Dedicated Web Worker Tests - Uncaught Exception in Single-Page Test</h1> +<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker. +<p>The test harness is expected to pass despite an uncaught exception in a worker because that worker is a single-page test.</p> +<div id="log"></div> + +<script> +test(function(t) { + assert_true("Worker" in self, "Browser should support Workers"); + }, + "Browser supports Workers"); + +fetch_tests_from_worker(new Worker("worker-uncaught-single.js")); + +test(function(t) { + assert_false(false, "False is false"); + }, + "Test running on main document."); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "OK", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Browser supports Workers", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Test running on main document.", + "properties": {}, + "message": null + }, + { + "status_string": "FAIL", + "name": "worker-uncaught-single", + "properties": {}, + "message": "Error: This failure is expected." + } + ], + "type": "complete" +} +</script> +</body> diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html new file mode 100644 index 0000000000..efd703c760 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/worker-dedicated.sub.html @@ -0,0 +1,88 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Dedicated Worker Tests</title> +<script src="../../../testharness.js"></script> +<script src="../../../testharnessreport.js"></script> +</head> +<body> +<h1>Dedicated Web Worker Tests</h1> +<p>Demonstrates running <tt>testharness</tt> based tests inside a dedicated web worker. +<p>The test harness is expected to fail due to an uncaught exception in one worker.</p> +<div id="log"></div> + +<script> +test(function(t) { + assert_true("Worker" in self, "Browser should support Workers"); + }, + "Browser supports Workers"); + +fetch_tests_from_worker(new Worker("worker.js")); + +fetch_tests_from_worker(new Worker("worker-error.js")); + +test(function(t) { + assert_false(false, "False is false"); + }, + "Test running on main document."); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "ERROR", + "message": "Error: This failure is expected." + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Browser supports Workers", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Test running on main document.", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Worker async_test that completes successfully", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Worker test that completes successfully", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "worker test that completes successfully before exception", + "properties": {}, + "message": null + }, + { + "status_string": "NOTRUN", + "name": "Worker test that doesn't run ('NOT RUN')", + "properties": {}, + "message": null + }, + { + "status_string": "FAIL", + "name": "Worker test that fails ('FAIL')", + "properties": {}, + "message": "assert_true: Failing test expected true got false" + }, + { + "status_string": "TIMEOUT", + "name": "Worker test that times out ('TIMEOUT')", + "properties": {}, + "message": "Test timed out" + } + ], + "type": "complete" +} +</script> +</body> diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-error.js b/testing/web-platform/tests/resources/test/tests/functional/worker-error.js new file mode 100644 index 0000000000..7b89602f04 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/worker-error.js @@ -0,0 +1,8 @@ +importScripts("/resources/testharness.js"); + +// The following sub-test ensures that the worker is not interpreted as a +// single-page test. The subsequent uncaught exception should therefore be +// interpreted as a harness error rather than a single-page test failure. +test(function() {}, "worker test that completes successfully before exception"); + +throw new Error("This failure is expected."); diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-service.html b/testing/web-platform/tests/resources/test/tests/functional/worker-service.html new file mode 100644 index 0000000000..2e07746e62 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/worker-service.html @@ -0,0 +1,115 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Example with a service worker</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<h1>Service Worker Tests</h1> +<p>Demonstrates running <tt>testharness</tt> based tests inside a service worker. +<p>The test harness should time out due to one of the tests inside the worker timing out. +<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/service-workers/">ServiceWorkers</a>. +<div id="log"></div> + +<script> +test( + function(t) { + assert_true("serviceWorker" in navigator, + "navigator.serviceWorker exists"); + }, + "Browser supports ServiceWorker"); + +promise_test( + function() { + // Since the service worker registration could be in an indeterminate + // state (due to, for example, a previous test run failing), we start by + // unregstering our service worker and then registering it again. + var scope = "service-worker-scope"; + var worker_url = "worker.js"; + + return navigator.serviceWorker.register(worker_url, {scope: scope}) + .then( + function(registration) { + return registration.unregister(); + }) + .then( + function() { + return navigator.serviceWorker.register(worker_url, {scope: scope}); + }) + .then( + function(registration) { + add_completion_callback( + function() { + registration.unregister(); + }); + + return new Promise( + function(resolve) { + registration.addEventListener("updatefound", + function() { + resolve(registration.installing); + }); + }); + }) + .then( + function(worker) { + fetch_tests_from_worker(worker); + }); + }, + "Register ServiceWorker"); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "TIMEOUT", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Browser supports ServiceWorker", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Register ServiceWorker", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Worker async_test that completes successfully", + "properties": {}, + "message": null + }, + { + "status_string": "PASS", + "name": "Worker test that completes successfully", + "properties": {}, + "message": null + }, + { + "status_string": "NOTRUN", + "name": "Worker test that doesn't run ('NOT RUN')", + "properties": {}, + "message": null + }, + { + "status_string": "FAIL", + "name": "Worker test that fails ('FAIL')", + "properties": {}, + "message": "assert_true: Failing test expected true got false" + }, + { + "status_string": "TIMEOUT", + "name": "Worker test that times out ('TIMEOUT')", + "properties": {}, + "message": "Test timed out" + } + ], + "type": "complete" +} +</script> +</body> diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-shared.html b/testing/web-platform/tests/resources/test/tests/functional/worker-shared.html new file mode 100644 index 0000000000..e26f17dec2 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/worker-shared.html @@ -0,0 +1,73 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Example with a shared worker</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +</head> +<body> +<h1>Shared Web Worker Tests</h1> +<p>Demonstrates running <tt>testharness</tt> based tests inside a shared worker. +<p>The test harness should time out due to one of the tests in the worker timing out. +<p>This test assumes that the browser supports <a href="http://www.w3.org/TR/workers/#shared-workers-and-the-sharedworker-interface">shared web workers</a>. +<div id="log"></div> + +<script> +test( + function(t) { + assert_true("SharedWorker" in self, + "Browser should support SharedWorkers"); + }, + "Browser supports SharedWorkers"); + +fetch_tests_from_worker(new SharedWorker("worker.js", + "My shared worker")); +</script> +<script type="text/json" id="expected"> +{ + "summarized_status": { + "status_string": "TIMEOUT", + "message": null + }, + "summarized_tests": [ + { + "status_string": "PASS", + "name": "Browser supports SharedWorkers", + "properties": {}, + "message": null + }, + { + "message": null, + "name": "Worker async_test that completes successfully", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "Worker test that completes successfully", + "properties": {}, + "status_string": "PASS" + }, + { + "message": null, + "name": "Worker test that doesn't run ('NOT RUN')", + "properties": {}, + "status_string": "NOTRUN" + }, + { + "message": "assert_true: Failing test expected true got false", + "name": "Worker test that fails ('FAIL')", + "properties": {}, + "status_string": "FAIL" + }, + { + "message": "Test timed out", + "name": "Worker test that times out ('TIMEOUT')", + "properties": {}, + "status_string": "TIMEOUT" + } + ], + "type": "complete" +} +</script> +</body> diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js new file mode 100644 index 0000000000..6925d59349 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-allow.js @@ -0,0 +1,19 @@ +importScripts("/resources/testharness.js"); + +setup({allow_uncaught_exception:true}); + +async_test(function(t) { + onerror = function() { + // Further delay the test's completion to ensure that the worker's + // `onerror` handler does not influence results in the parent context. + setTimeout(function() { + t.done(); + }, 0); + }; + + setTimeout(function() { + throw new Error("This error is expected."); + }, 0); +}, 'onerror event is triggered'); + +done(); diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js new file mode 100644 index 0000000000..c04542b2f5 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/worker-uncaught-single.js @@ -0,0 +1,8 @@ +importScripts("/resources/testharness.js"); + +setup({ single_test: true }); + +// Because this script enables the `single_test` configuration option, it +// should be interpreted as a single-page test, and the uncaught exception +// should be reported as a test failure (harness status: OK). +throw new Error("This failure is expected."); diff --git a/testing/web-platform/tests/resources/test/tests/functional/worker.js b/testing/web-platform/tests/resources/test/tests/functional/worker.js new file mode 100644 index 0000000000..a923bc2d89 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/functional/worker.js @@ -0,0 +1,34 @@ +importScripts("/resources/testharness.js"); + +test( + function(test) { + assert_true(true, "True is true"); + }, + "Worker test that completes successfully"); + +test( + function(test) { + assert_true(false, "Failing test"); + }, + "Worker test that fails ('FAIL')"); + +async_test( + function(test) { + assert_true(true, "True is true"); + }, + "Worker test that times out ('TIMEOUT')"); + +async_test("Worker test that doesn't run ('NOT RUN')"); + +async_test( + function(test) { + self.setTimeout( + function() { + test.done(); + }, + 0); + }, + "Worker async_test that completes successfully"); + +// An explicit done() is required for dedicated and shared web workers. +done(); diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html b/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html new file mode 100644 index 0000000000..18e83a8e89 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlArray/is_json_type.html @@ -0,0 +1,192 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>IdlArray.prototype.is_json_type()</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> + "use strict"; + + test(function() { + var idl = new IdlArray(); + assert_true(idl.is_json_type(typeFrom("DOMString"))); + assert_true(idl.is_json_type(typeFrom("ByteString"))); + assert_true(idl.is_json_type(typeFrom("USVString"))); + idl.add_untested_idls('enum BarEnum { "a", "b", "c" };'); + assert_true(idl.is_json_type(typeFrom("BarEnum"))); + }, 'should return true for all string types'); + + test(function() { + var idl = new IdlArray(); + assert_false(idl.is_json_type(typeFrom("Error"))); + assert_false(idl.is_json_type(typeFrom("DOMException"))); + }, 'should return false for all exception types'); + + test(function() { + var idl = new IdlArray(); + assert_false(idl.is_json_type(typeFrom("Int8Array"))); + assert_false(idl.is_json_type(typeFrom("Int16Array"))); + assert_false(idl.is_json_type(typeFrom("Int32Array"))); + assert_false(idl.is_json_type(typeFrom("Uint8Array"))); + assert_false(idl.is_json_type(typeFrom("Uint16Array"))); + assert_false(idl.is_json_type(typeFrom("Uint32Array"))); + assert_false(idl.is_json_type(typeFrom("Uint8ClampedArray"))); + assert_false(idl.is_json_type(typeFrom("BigInt64Array"))); + assert_false(idl.is_json_type(typeFrom("BigUint64Array"))); + assert_false(idl.is_json_type(typeFrom("Float32Array"))); + assert_false(idl.is_json_type(typeFrom("Float64Array"))); + assert_false(idl.is_json_type(typeFrom("ArrayBuffer"))); + assert_false(idl.is_json_type(typeFrom("DataView"))); + }, 'should return false for all buffer source types'); + + test(function() { + var idl = new IdlArray(); + assert_true(idl.is_json_type(typeFrom("boolean"))); + }, 'should return true for boolean'); + + test(function() { + var idl = new IdlArray(); + assert_true(idl.is_json_type(typeFrom("byte"))); + assert_true(idl.is_json_type(typeFrom("octet"))); + assert_true(idl.is_json_type(typeFrom("short"))); + assert_true(idl.is_json_type(typeFrom("unsigned short"))); + assert_true(idl.is_json_type(typeFrom("long"))); + assert_true(idl.is_json_type(typeFrom("unsigned long"))); + assert_true(idl.is_json_type(typeFrom("long long"))); + assert_true(idl.is_json_type(typeFrom("unsigned long long"))); + assert_true(idl.is_json_type(typeFrom("float"))); + assert_true(idl.is_json_type(typeFrom("unrestricted float"))); + assert_true(idl.is_json_type(typeFrom("double"))); + assert_true(idl.is_json_type(typeFrom("unrestricted double"))); + }, 'should return true for all numeric types'); + + test(function() { + var idl = new IdlArray(); + assert_false(idl.is_json_type(typeFrom("Promise<DOMString>"))); + }, 'should return false for promises'); + + test(function() { + var idl = new IdlArray(); + assert_false(idl.is_json_type(typeFrom("sequence<DOMException>"))); + assert_true(idl.is_json_type(typeFrom("sequence<DOMString>"))); + }, 'should handle sequences according to their inner types'); + + test(function() { + var idl = new IdlArray(); + assert_false(idl.is_json_type(typeFrom("FrozenArray<DOMException>"))); + assert_true(idl.is_json_type(typeFrom("FrozenArray<DOMString>"))); + }, 'should handle frozen arrays according to their inner types'); + + test(function() { + var idl = new IdlArray(); + assert_true(idl.is_json_type(typeFrom("record<DOMString, DOMString>"))); + assert_false(idl.is_json_type(typeFrom("record<DOMString, Error>"))); + }, 'should handle records according to their inner types'); + + test(function() { + var idl = new IdlArray(); + assert_true(idl.is_json_type(typeFrom("object"))); + }, 'should return true for object type'); + + test(function() { + var idl = new IdlArray(); + assert_false(idl.is_json_type(typeFrom("any"))); + }, 'should return false for any type'); + + test(function() { + var idl = new IdlArray(); + idl.add_untested_idls('dictionary Foo { DOMString foo; }; dictionary Bar : Foo { DOMString bar; };'); + assert_true(idl.is_json_type(typeFrom("Foo"))); + assert_true(idl.is_json_type(typeFrom("Bar"))); + }, 'should return true for dictionaries whose members are all JSON types'); + + test(function() { + var idl = new IdlArray(); + idl.add_untested_idls('dictionary Foo { };'); + assert_true(idl.is_json_type(typeFrom("Foo"))); + }, 'should return true for dictionaries which have no members'); + + test(function() { + var idl = new IdlArray(); + idl.add_untested_idls('dictionary FooBar { DOMString a; Error b; }; dictionary Baz : FooBar {};'); + assert_false(idl.is_json_type(typeFrom("FooBar"))); + assert_false(idl.is_json_type(typeFrom("Baz"))); + }, 'should return false for dictionaries whose members are not all JSON types'); + + test(function() { + var idl = new IdlArray(); + idl.add_untested_idls('interface Foo { DOMString toJSON(); };'); + assert_true(idl.is_json_type(typeFrom("Foo"))); + }, 'should return true for interfaces which declare a toJSON operation'); + + test(function() { + var idl = new IdlArray(); + idl.add_untested_idls('interface Foo { DOMString toJSON(); }; interface Bar : Foo { };'); + assert_true(idl.is_json_type(typeFrom("Bar"))); + }, 'should return true for interfaces which inherit from an interface which declares a toJSON operation'); + + test(function() { + var idl = new IdlArray(); + idl.add_untested_idls('interface Foo { }; interface mixin Bar { DOMString toJSON(); }; Foo includes Bar;'); + idl.merge_mixins(); + assert_true(idl.is_json_type(typeFrom("Foo"))); + }, 'should return true for interfaces which mixin an interface which declare a toJSON operation'); + + test(function() { + var idl = new IdlArray(); + idl.add_untested_idls('interface Foo { };'); + assert_false(idl.is_json_type(typeFrom("Foo"))); + }, 'should return false for interfaces which do not declare a toJSON operation'); + + test(function() { + var idl = new IdlArray(); + idl.add_untested_idls('interface Foo { object toJSON(); };'); + assert_true(idl.is_json_type(typeFrom("(Foo or DOMString)"))); + }, 'should return true for union types whose member types are JSON types'); + + test(function() { + var idl = new IdlArray(); + assert_false(idl.is_json_type(typeFrom("(DataView or DOMString)"))); + }, 'should return false for union types whose member types are not all JSON types'); + + test(function() { + var idl = new IdlArray(); + assert_true(idl.is_json_type(typeFrom("DOMString?"))); + assert_false(idl.is_json_type(typeFrom("DataView?"))); + }, 'should consider the inner types of nullable types'); + + test(function() { + var idl = new IdlArray(); + assert_true(idl.is_json_type(typeFrom("[XAttr] long"))); + assert_false(idl.is_json_type(typeFrom("[XAttr] DataView"))); + }, 'should consider the inner types of annotated types.'); + + test(function() { + var idl = new IdlArray(); + assert_throws_js(Error, _ => idl.is_json_type(typeFrom("Foo"))); + }, "should throw if it references a dictionary, enum or interface which wasn't added to the IdlArray"); + + test(function() { + var idl = new IdlArray(); + idl.add_untested_idls('interface Foo : Bar { };'); + assert_throws_js(Error, _ => idl.is_json_type(typeFrom("Foo"))); + }, "should throw for interfaces which inherit from another interface which wasn't added to the IdlArray"); + + test(function() { + var idl = new IdlArray(); + assert_true(idl.is_json_type(typedefFrom("typedef double DOMHighResTimeStamp;").idlType)); + }, 'should return true for typedefs whose source type is a JSON type'); + + test(function() { + var idl = new IdlArray(); + assert_false(idl.is_json_type(typedefFrom("typedef DataView DOMHighResTimeStamp;").idlType)); + }, 'should return false for typedefs whose source type is not a JSON type'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html new file mode 100644 index 0000000000..418bcdec92 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/get_reverse_inheritance_stack.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>IdlDictionary.prototype.get_reverse_inheritance_stack()</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> + "use strict"; + test(function() { + var stack = dictionaryFrom('dictionary A { };').get_reverse_inheritance_stack(); + assert_array_equals(stack.map(d => d.name), ["A"]); + }, 'should return an array that includes itself.'); + + test(function() { + var d = dictionaryFrom('dictionary A : B { };'); + assert_throws_js(Error, _ => d.get_reverse_inheritance_stack()); + }, "should throw for dictionaries which inherit from another dictionary which wasn't added to the IdlArray"); + + test(function() { + var idl = new IdlArray(); + idl.add_idls('dictionary A : B { };'); + idl.add_untested_idls('dictionary B : C { }; dictionary C { };'); + var A = idl.members["A"]; + assert_array_equals(A.get_reverse_inheritance_stack().map(d => d.name), ["C", "B", "A"]); + }, 'should return an array of dictionaries in order of inheritance, starting with the base dictionary'); + + test(function () { + let i = new IdlArray(); + i.add_untested_idls('dictionary A : B {};'); + i.assert_throws(new IdlHarnessError('A inherits B, but B is undefined.'), i => i.test()); + }, 'A : B with B undeclared should throw IdlHarnessError'); + + test(function () { + let i = new IdlArray(); + i.add_untested_idls('dictionary A : B {};'); + i.add_untested_idls('interface B {};'); + i.assert_throws(new IdlHarnessError('A inherits B, but A is not an interface.'), i => i.test()); + }, 'dictionary A : B with B interface should throw IdlHarnessError'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html new file mode 100644 index 0000000000..d6137f6895 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlDictionary/test_partial_dictionary.html @@ -0,0 +1,39 @@ +<!DOCTYPE HTML> +<html> + +<head> + <meta charset="utf-8"> + <title>idlharness: partial dictionaries</title> + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> + <script src="../../../idl-helper.js"></script> +</head> + +<body> +<pre id='idl'> +dictionary A {}; +partial dictionary A { + boolean B; +}; +partial dictionary A { + boolean C; +}; +</pre> + +<script> +'use strict'; + +test(() => { + let idlArray = new IdlArray(); + idlArray.add_idls(document.getElementById('idl').textContent); + idlArray.test(); + + let members = idlArray.members["A"].members.map(m => m.name); + assert_array_equals(members, ["B", "C"], 'A should contain B, C'); +}, 'Partial dictionaries'); +</script> + +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html new file mode 100644 index 0000000000..e9ee3f8680 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/constructors.html @@ -0,0 +1,26 @@ +<!DOCTYPE HTML> +<title>IdlInterface.prototype.constructors()</title> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> +"use strict"; +// [Constructor] extended attribute should not be supported: +test(function() { + var i = interfaceFrom('[Constructor] interface A { };'); + assert_equals(i.constructors().length, 0); +}, 'Interface with Constructor extended attribute.'); + +test(function() { + var i = interfaceFrom('interface A { constructor(); };'); + assert_equals(i.constructors().length, 1); +}, 'Interface with constructor method'); + +test(function() { + var i = interfaceFrom('interface A { constructor(); constructor(any value); };'); + assert_equals(i.constructors().length, 2); +}, 'Interface with constructor overloads'); +</script> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html new file mode 100644 index 0000000000..5ade7d0d28 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/default_to_json_operation.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>IdlDictionary.prototype.default_to_json_operation()</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> + "use strict"; + test(function() { + var map = interfaceFrom('interface A { [Default] object toJSON(); };').default_to_json_operation(); + assert_equals(map.size, 0); + }, 'should return an empty map when there are no attributes'); + + test(function() { + var r = interfaceFrom('interface A { };').default_to_json_operation(); + assert_equals(r, null); + }, 'should return null when there is no toJSON method'); + + test(function() { + var r = interfaceFrom('interface A { DOMString toJSON(); };').default_to_json_operation(); + assert_equals(r, null); + }, 'should return null when there is a toJSON method but it does not have the [Default] extended attribute'); + + test(function() { + var context = new IdlArray(); + context.add_idls("interface A : B { DOMString toJSON(); };"); + context.add_idls("interface B { [Default] object toJSON(); };"); + var r = context.members.A.default_to_json_operation(); + assert_equals(r, null); + }, 'should return null when there is a toJSON method but it does not have the [Default] extended attribute even if this extended attribute exists on inherited interfaces'); + + test(function() { + var map = interfaceFrom('interface A { [Default] object toJSON(); static attribute DOMString foo; };').default_to_json_operation(); + assert_equals(map.size, 0); + }, 'should not include static attributes'); + + test(function() { + var map = interfaceFrom('interface A { [Default] object toJSON(); attribute Promise<DOMString> bar; };').default_to_json_operation(); + assert_equals(map.size, 0); + }, 'should not include attributes which are not JSON types'); + + test(function() { + var map = interfaceFrom('interface A { [Default] object toJSON(); DOMString bar(); };').default_to_json_operation(); + assert_equals(map.size, 0); + }, 'should not include operations'); + + test(function() { + var map = interfaceFrom('interface A { [Default] object toJSON(); attribute DOMString bar; };').default_to_json_operation(); + assert_equals(map.size, 1); + assert_true(map.has("bar")); + assert_equals(map.get("bar").idlType, "DOMString"); + }, 'should return a map whose key/value pair represent the identifier and IDL type of valid attributes'); + + test(function() { + var context = new IdlArray(); + context.add_idls("interface A : B { [Default] object toJSON(); attribute DOMString a; };"); + context.add_idls("interface B { [Default] object toJSON(); attribute long b; };"); + var map = context.members.A.default_to_json_operation(); + assert_array_equals([...map.keys()], ["b", "a"]); + assert_array_equals([...map.values()].map(v => v.idlType), ["long", "DOMString"]); + }, 'should return a properly ordered map that contains IDL types of valid attributes for inherited interfaces'); + + test(function() { + var context = new IdlArray(); + context.add_idls("interface A : B { attribute DOMString a; };"); + context.add_idls("interface B { [Default] object toJSON(); attribute long b; };"); + var map = context.members.A.default_to_json_operation(); + assert_equals(map.size, 1); + assert_true(map.has("b")); + assert_equals(map.get("b").idlType, "long"); + assert_array_equals([...map.keys()], ["b"]); + }, 'should not include attributes of the current interface when the [Default] toJSON method in inherited'); + + test(function() { + var context = new IdlArray(); + context.add_idls("interface A : B { [Default] object toJSON(); };"); + context.add_idls("interface B : C { [Default] object toJSON(); attribute DOMString foo; };"); + context.add_idls("interface C { [Default] object toJSON(); attribute long foo; };"); + var map = context.members.A.default_to_json_operation(); + assert_equals(map.size, 1); + assert_true(map.has("foo")); + assert_equals(map.get("foo").idlType, "DOMString"); + }, 'attributes declared further away in the inheritance hierarchy should be masked by attributes declared closer'); + + test(function() { + var context = new IdlArray(); + context.add_idls("interface A { [Default] object toJSON(); attribute DOMString a; };"); + context.add_idls("interface B : A { attribute any b; };"); + context.add_idls("interface C : B { [Default] object toJSON(); attribute long c; };"); + var map = context.members.C.default_to_json_operation(); + assert_array_equals([...map.keys()], ["a", "c"]); + assert_array_equals([...map.values()].map(v => v.idlType), ["DOMString", "long"]); + }, 'should return an ordered map that ignores attributes of inherited interfaces which do not declare a [Default] toJSON operation.'); + + test(function() { + var context = new IdlArray(); + context.add_idls("interface D { attribute DOMString d; };"); + context.add_idls("interface mixin M { [Default] object toJSON(); attribute long m; };"); + context.add_idls("D includes M;"); + context.merge_mixins(); + var map = context.members.D.default_to_json_operation(); + assert_array_equals([...map.keys()], ["d", "m"]); + assert_array_equals([...map.values()].map(v => v.idlType), ["DOMString", "long"]); + }, 'should return a properly ordered map that accounts for mixed-in interfaces which declare a [Default] toJSON operation.'); +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html new file mode 100644 index 0000000000..90142efe6b --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/do_member_unscopable_asserts.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>IdlDictionary.prototype.do_member_unscopable_asserts()</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> + 'use strict'; + function mock_interface_A(unscopables) { + self.A = function A() {}; + A.prototype[Symbol.unscopables] = unscopables; + } + + test(function() { + const i = interfaceFrom('interface A { [Unscopable] attribute any x; };'); + const member = i.members[0]; + assert_true(member.isUnscopable); + mock_interface_A({ x: true }); + i.do_member_unscopable_asserts(member); + }, 'should not throw for [Unscopable] with property in @@unscopables'); + + test(function() { + const i = interfaceFrom('interface A { [Unscopable] attribute any x; };'); + const member = i.members[0]; + assert_true(member.isUnscopable); + mock_interface_A({}); + // assert_throws_* can't be used because they rethrow AssertionErrors. + try { + i.do_member_unscopable_asserts(member); + } catch(e) { + assert_true(e.message.includes('Symbol.unscopables')); + return; + } + assert_unreached('did not throw'); + }, 'should throw for [Unscopable] with property missing from @@unscopables'); + + // This test checks that for attributes/methods which aren't [Unscopable] + // in the IDL, we don't assert that @@unscopables is missing the property. + // This could miss implementation bugs, but [Unscopable] is so rarely used + // that it's fairly unlikely to ever happen. + test(function() { + const i = interfaceFrom('interface A { attribute any x; };'); + const member = i.members[0]; + assert_false(member.isUnscopable); + mock_interface_A({ x: true }); + i.do_member_unscopable_asserts(member); + }, 'should not throw if [Unscopable] is used but property is in @@unscopables'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html new file mode 100644 index 0000000000..a3d901a752 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object.html @@ -0,0 +1,22 @@ +<!DOCTYPE HTML> +<title>IdlInterface.prototype.get_interface_object()</title> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> +"use strict"; +test(function() { + window.A = {}; + var i = interfaceFrom('interface A { };'); + assert_equals(i.get_interface_object(), window.A); +}, 'Interface does not have LegacyNamespace.'); + +test(function() { + window.Foo = { A: {} }; + var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };'); + assert_equals(i.get_interface_object(), window.Foo.A); +}, 'Interface has LegacyNamespace'); +</script> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html new file mode 100644 index 0000000000..51ab2067bc --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_interface_object_owner.html @@ -0,0 +1,21 @@ +<!DOCTYPE HTML> +<title>IdlInterface.prototype.get_interface_object_owner()</title> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> +"use strict"; +test(function() { + var i = interfaceFrom('interface A { };'); + assert_equals(i.get_interface_object_owner(), window); +}, 'Interface does not have LegacyNamespace.'); + +test(function() { + window.Foo = {}; + var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };'); + assert_equals(i.get_interface_object_owner(), window.Foo); +}, 'Interface has LegacyNamespace'); +</script> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html new file mode 100644 index 0000000000..e2d42bb09e --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_legacy_namespace.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<title>IdlInterface.prototype.get_legacy_namespace()</title> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> +"use strict"; +test(function() { + var i = interfaceFrom('interface A { };'); + assert_equals(i.get_legacy_namespace(), undefined); +}, 'Interface does not have LegacyNamespace.'); + +test(function() { + var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };'); + assert_equals(i.get_legacy_namespace(), "Foo"); +}, 'Interface has LegacyNamespace'); +</script> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html new file mode 100644 index 0000000000..677a31b5e7 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_qualified_name.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML> +<title>IdlInterface.prototype.get_qualified_name()</title> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> +"use strict"; +test(function() { + var i = interfaceFrom('interface A { };'); + assert_equals(i.get_qualified_name(), "A"); +}, 'Interface does not have LegacyNamespace.'); + +test(function() { + var i = interfaceFrom('[LegacyNamespace=Foo] interface A { }; namespace Foo { };'); + assert_equals(i.get_qualified_name(), "Foo.A"); +}, 'Interface has LegacyNamespace'); +</script> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html new file mode 100644 index 0000000000..0c066baabb --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/get_reverse_inheritance_stack.html @@ -0,0 +1,49 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>IdlInterface.prototype.get_reverse_inheritance_stack()</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> + "use strict"; + test(function() { + var stack = interfaceFrom('interface A { };').get_reverse_inheritance_stack(); + assert_array_equals(stack.map(i => i.name), ["A"]); + }, 'should return an array that includes itself.'); + + test(function() { + var i = interfaceFrom('interface A : B { };'); + assert_throws_js(Error, _ => i.get_reverse_inheritance_stack()); + }, "should throw for interfaces which inherit from another interface which wasn't added to the IdlArray"); + + test(function() { + var idl = new IdlArray(); + idl.add_idls('interface A : B { };'); + idl.add_untested_idls('interface B : C { }; interface C { };'); + var A = idl.members["A"]; + assert_array_equals(A.get_reverse_inheritance_stack().map(i => i.name), ["C", "B", "A"]); + }, 'should return an array of interfaces in order of inheritance, starting with the base interface'); + + test(function () { + var idl = new IdlArray(); + idl.add_untested_idls('interface A : B { };'); + idl.add_untested_idls('interface B : A { };'); + idl.assert_throws('A has a circular dependency: A,B,A', i => i.test()); + }, 'should throw when inheritance is circular'); + + test(function () { + var idl = new IdlArray(); + idl.add_untested_idls('interface A : B { };'); + idl.assert_throws( + 'Duplicate identifier A', + i => i.add_untested_idls('interface A : C { };')); + }, 'should throw when multiple inheritances defined'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html new file mode 100644 index 0000000000..b47262b72b --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_default_to_json_regular_operation.html @@ -0,0 +1,47 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>IdlInterface.prototype.has_default_to_json_regular_operation()</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> + "use strict"; + test(function() { + var i = interfaceFrom('interface A { };'); + assert_false(i.has_default_to_json_regular_operation()); + }, 'should return false when the interface declares no toJSON operation.'); + + test(function() { + var i = interfaceFrom('interface A { static object toJSON(); };'); + assert_false(i.has_default_to_json_regular_operation()); + }, 'should return false when the interface declares a static toJSON operation.'); + + test(function() { + var i = interfaceFrom('interface A { object toJSON(); };'); + assert_false(i.has_default_to_json_regular_operation()); + }, 'should return false when the interface declares a regular toJSON operation with no extended attribute.'); + + test(function() { + var i = interfaceFrom('interface A { [x] object toJSON(); };'); + assert_false(i.has_default_to_json_regular_operation()); + }, 'should return false when the interface declares a regular toJSON operation with another extented attribute.'); + + test(function() { + var i = interfaceFrom('interface A { [Default] object toJSON(); };'); + assert_true(i.has_default_to_json_regular_operation()); + }, 'should return true when the interface declares a regular toJSON operation with the [Default] extented attribute.'); + + test(function() { + var i = interfaceFrom('interface A { [Attr, AnotherAttr, Default] object toJSON(); };'); + assert_true(i.has_default_to_json_regular_operation()); + }, 'should return true when the interface declares a regular toJSON operation with multiple extended attributes, including [Default].'); +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html new file mode 100644 index 0000000000..a1a641bd97 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/has_to_json_regular_operation.html @@ -0,0 +1,31 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>IdlInterface.prototype.has_to_json_regular_operation()</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> + "use strict"; + test(function() { + var i = interfaceFrom('interface A { };'); + assert_false(i.has_to_json_regular_operation()); + }, 'should return false when the interface declares no toJSON operation.'); + + test(function() { + var i = interfaceFrom('interface A { static object toJSON(); };'); + assert_false(i.has_to_json_regular_operation()); + }, 'should return false when the interface declares a static toJSON operation.'); + + test(function() { + var i = interfaceFrom('interface A { object toJSON(); };'); + assert_true(i.has_to_json_regular_operation()); + }, 'should return true when the interface declares a regular toJSON operation.'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html new file mode 100644 index 0000000000..3ce945751d --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/should_have_interface_object.html @@ -0,0 +1,30 @@ +<!DOCTYPE HTML> +<title>IdlInterface.prototype.should_have_interface_object()</title> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> +"use strict"; +test(function() { + var i = interfaceFrom("callback interface A { const unsigned short B = 0; };"); + assert_true(i.should_have_interface_object()); +}, "callback interface with a constant"); + +test(function() { + var i = interfaceFrom("callback interface A { undefined b(); sequence<any> c(); };"); + assert_false(i.should_have_interface_object()); +}, "callback interface without a constant"); + +test(function() { + var i = interfaceFrom("[LegacyNoInterfaceObject] interface A { };"); + assert_false(i.should_have_interface_object()); +}, "non-callback interface with [LegacyNoInterfaceObject]"); + +test(function() { + var i = interfaceFrom("interface A { };"); + assert_true(i.should_have_interface_object()); +}, "non-callback interface without [LegacyNoInterfaceObject]"); +</script> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html new file mode 100644 index 0000000000..0031558ad4 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterface/test_primary_interface_of_undefined.html @@ -0,0 +1,29 @@ +<!DOCTYPE HTML> +<html> + +<head> + <title>idlharness test_primary_interface_of_undefined</title> +</head> + +<body> + <script src="/resources/testharness.js"></script> + <script src="/resources/WebIDLParser.js"></script> + <script src="/resources/idlharness.js"></script> + <script> + 'use strict'; + test(function () { + let i = new IdlArray(); + i.add_untested_idls('interface A : B {};'); + i.assert_throws(new IdlHarnessError('A inherits B, but B is undefined.'), i => i.test()); + }, 'A : B with B undeclared should throw IdlHarnessError'); + + test(function () { + let i = new IdlArray(); + i.add_untested_idls('interface A : B {};'); + i.add_untested_idls('dictionary B {};'); + i.assert_throws(new IdlHarnessError('A inherits B, but B is not an interface.'), i => i.test()); + }, 'interface A : B with B dictionary should throw IdlHarnessError'); + </script> +</body> + +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html new file mode 100644 index 0000000000..b3f402dd08 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/is_to_json_regular_operation.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>IdlInterfaceMember.prototype.is_to_json_regular_operation()</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> + "use strict"; + test(function() { + var m = memberFrom("readonly attribute DOMString foo"); + assert_false(m.is_to_json_regular_operation()); + }, 'should return false when member is an attribute.'); + + test(function() { + var m = memberFrom("static undefined foo()"); + assert_false(m.is_to_json_regular_operation()); + }, 'should return false when member is a static operation.'); + + test(function() { + var m = memberFrom("static object toJSON()"); + assert_false(m.is_to_json_regular_operation()); + }, 'should return false when member is a static toJSON operation.'); + + test(function() { + var m = memberFrom("object toJSON()"); + assert_true(m.is_to_json_regular_operation()); + }, 'should return true when member is a regular toJSON operation.'); + + test(function() { + var m = memberFrom("[Foo] object toJSON()"); + assert_true(m.is_to_json_regular_operation()); + }, 'should return true when member is a regular toJSON operation with extensible attributes.'); +</script> +</body> +</html> + diff --git a/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html new file mode 100644 index 0000000000..054dbb1ccb --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/IdlInterfaceMember/toString.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>IdlInterfaceMember.prototype.toString()</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script src="../../../idl-helper.js"></script> +<script> +"use strict"; +const tests = [ + ["long x", "long"], + ["long? x", "long?"], + ["Promise<long> x", "Promise<long>"], + ["Promise<long?> x", "Promise<long?>"], + ["sequence<long> x", "sequence<long>"], + ["(long or DOMString) x", "(long or DOMString)"], + ["long x, boolean y", "long, boolean"], + ["long x, optional boolean y", "long, optional boolean"], + ["long... args", "long..."], + ["sequence<long>... args", "sequence<long>..."], + ["(long or DOMString)... args", "(long or DOMString)..."], +]; +for (const [input, output] of tests) { + test(function() { + var m = memberFrom(`undefined foo(${input})`); + assert_equals(m.toString(), `foo(${output})`); + }, `toString for ${input}`); +} +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/assert_implements.html b/testing/web-platform/tests/resources/test/tests/unit/assert_implements.html new file mode 100644 index 0000000000..6e35f38502 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/assert_implements.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html lang="en"> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/test/tests/unit/helpers.js"></script> +<title>assert_implements unittests</title> +<script> +'use strict'; + +test(() => { + // All values in JS that are not falsy are truthy, so we just check some + // common cases here. + assert_implements(true, 'true is a truthy value'); + assert_implements(5, 'positive integeter is a truthy value'); + assert_implements(-5, 'negative integeter is a truthy value'); + assert_implements('foo', 'non-empty string is a truthy value'); +}, 'truthy values'); + +test_failure(() => { + assert_implements(false); +}, 'false is a falsy value'); + +test_failure(() => { + assert_implements(0); +}, '0 is a falsy value'); + +test_failure(() => { + assert_implements(''); +}, 'empty string is a falsy value'); + +test_failure(() => { + assert_implements(null); +}, 'null is a falsy value'); + +test_failure(() => { + assert_implements(undefined); +}, 'undefined is a falsy value'); + +test_failure(() => { + assert_implements(NaN); +}, 'NaN is a falsy value'); +</script> diff --git a/testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html b/testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html new file mode 100644 index 0000000000..4f23e203c5 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/assert_implements_optional.html @@ -0,0 +1,43 @@ +<!DOCTYPE HTML> +<html lang="en"> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/test/tests/unit/helpers.js"></script> +<title>assert_implements_optional unittests</title> +<script> +'use strict'; + +test(() => { + // All values in JS that are not falsy are truthy, so we just check some + // common cases here. + assert_implements_optional(true, 'true is a truthy value'); + assert_implements_optional(5, 'positive integeter is a truthy value'); + assert_implements_optional(-5, 'negative integeter is a truthy value'); + assert_implements_optional('foo', 'non-empty string is a truthy value'); +}, 'truthy values'); + +test_failure(() => { + assert_implements_optional(false); +}, 'false is a falsy value'); + +test_failure(() => { + assert_implements_optional(0); +}, '0 is a falsy value'); + +test_failure(() => { + assert_implements_optional(''); +}, 'empty string is a falsy value'); + +test_failure(() => { + assert_implements_optional(null); +}, 'null is a falsy value'); + +test_failure(() => { + assert_implements_optional(undefined); +}, 'undefined is a falsy value'); + +test_failure(() => { + assert_implements_optional(NaN); +}, 'NaN is a falsy value'); +</script> diff --git a/testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html b/testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html new file mode 100644 index 0000000000..313d77b977 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/assert_object_equals.html @@ -0,0 +1,152 @@ +<!DOCTYPE HTML> +<html lang="en"> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/test/tests/unit/helpers.js"></script> +<title>Assertion functions</title> +<script> +'use strict'; + +test(function() { + assert_object_equals({}, {}); +}, 'empty objects'); + +test(function() { + var actual = {}; + var expected = {}; + actual.a = actual; + expected.a = actual; + + assert_object_equals(actual, expected); +}, 'tolerates cycles in actual value'); + +test(function() { + var actual = {}; + var expected = {}; + actual.a = expected; + expected.a = expected; + + assert_object_equals(actual, expected); +}, 'tolerates cycles in expected value'); + +test(function() { + assert_object_equals({2: 99, 0: 23, 1: 45}, [23, 45, 99]); +}, 'recognizes equivalence of actual object and expected array'); + +test(function() { + assert_object_equals([23, 45, 99], {2: 99, 0: 23, 1: 45}); +}, 'recognizes equivalence of actual array and expected object'); + +test(function() { + var actual = {}; + var expected = {}; + Object.defineProperty(actual, 'a', { value: 1, enumerable: false }); + + assert_not_equals(actual.a, expected.a); + assert_object_equals(actual, expected); +}, 'non-enumerable properties in actual value ignored'); + +test(function() { + var actual = {}; + var expected = {}; + Object.defineProperty(expected, 'a', { value: 1, enumerable: false }); + + assert_not_equals(actual.a, expected.a); + assert_object_equals(actual, expected); +}, 'non-enumerable properties in expected value ignored'); + +test(function() { + assert_object_equals({c: 3, a: 1, b: 2}, {a: 1, b: 2, c: 3}); +}, 'equivalent objects - "flat" object'); + +test(function() { + assert_object_equals( + {c: {e: 5, d: 4}, b: 2, a: 1}, + {a: 1, b: 2, c: {d: 4, e: 5}} + ); +}, 'equivalent objects - nested object'); + +test(function() { + assert_object_equals( + {c: [4, 5], b: 2, a: 1}, + {a: 1, b: 2, c: [4, 5]} + ); +}, 'equivalent objects - nested array'); + +test(function() { + assert_object_equals({a: NaN}, {a: NaN}); +}, 'equivalent objects - NaN value'); + +test(function() { + assert_object_equals({a: -0}, {a: -0}); +}, 'equivalent objects - negative zero value'); + +test_failure(function() { + assert_object_equals(undefined, {}); +}, 'invalid actual value: undefined'); + +test_failure(function() { + assert_object_equals(null, {}); +}, 'invalid actual value: null'); + +test_failure(function() { + assert_object_equals(34, {}); +}, 'invalid actual value: number'); + +test_failure(function() { + assert_object_equals({c: 3, a: 1, b: 2}, {a: 1, b: 1, c: 3}); +}, 'unequal property value - "flat" object'); + +test_failure(function() { + var actual = Object.create({a: undefined}); + var expected = {}; + + assert_object_equals(actual, expected); +}, 'non-own properties in actual value verified'); + +test_failure(function() { + var actual = {}; + var expected = Object.create({a: undefined}); + + assert_object_equals(actual, expected); +}, 'non-own properties in expected value verified'); + +test_failure(function() { + assert_object_equals( + {a: 1, b: 2, c: {d: 5, e: 5, f: 6}}, + {a: 1, b: 2, c: {d: 5, e: 7, f: 6}} + ); +}, 'unequal property value - nested object'); + +test_failure(function() { + assert_object_equals( + {a: 1, b: 2, c: [4, 5, 6]}, + {a: 1, b: 2, c: [4, 7, 6]} + ); +}, 'unequal property value - nested array'); + +test_failure(function() { + assert_object_equals({a: NaN}, {a: 0}); +}, 'equivalent objects - NaN actual value'); + +test_failure(function() { + assert_object_equals({a: 0}, {a: NaN}); +}, 'equivalent objects - NaN expected value'); + +test_failure(function() { + assert_object_equals({a: -0}, {a: 0}); +}, 'equivalent objects - negative zero actual value'); + +test_failure(function() { + assert_object_equals({a: 0}, {a: -0}); +}, 'equivalent objects - negative zero expected value'); + +test_failure(function() { + assert_object_equals({a: 1}, {}); +}, 'actual contains additional property'); + +test_failure(function() { + assert_object_equals({}, {a: 1}); +}, 'expected contains additional property'); +</script> diff --git a/testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html b/testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html new file mode 100644 index 0000000000..0fde2e2422 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/async-test-return-restrictions.html @@ -0,0 +1,135 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="/resources/testharness.js"></script> + <title>Restrictions on return value from `async_test`</title> +</head> +<body> +<script> +function makeTest(...bodies) { + const closeScript = '<' + '/script>'; + let src = ` +<!DOCTYPE HTML> +<html> +<head> +<title>Document title</title> +<script src="/resources/testharness.js?${Math.random()}">${closeScript} +</head> + +<body> +<div id="log"></div>`; + bodies.forEach((body) => { + src += '<script>(' + body + ')();' + closeScript; + }); + + const iframe = document.createElement('iframe'); + + document.body.appendChild(iframe); + iframe.contentDocument.write(src); + + return new Promise((resolve) => { + window.addEventListener('message', function onMessage(e) { + if (e.source !== iframe.contentWindow) { + return; + } + if (!e.data || e.data.type !=='complete') { + return; + } + window.removeEventListener('message', onMessage); + resolve(e.data); + }); + + iframe.contentDocument.close(); + }).then(({ tests, status }) => { + const summary = { + harness: { + status: getEnumProp(status, status.status), + message: status.message + }, + tests: {} + }; + + tests.forEach((test) => { + summary.tests[test.name] = getEnumProp(test, test.status); + }); + + return summary; + }); +} + +function getEnumProp(object, value) { + for (let property in object) { + if (!/^[A-Z]+$/.test(property)) { + continue; + } + + if (object[property] === value) { + return property; + } + } +} + +promise_test(() => { + return makeTest( + () => { + async_test((t) => {t.done(); return undefined;}, 'before'); + async_test((t) => {t.done(); return null;}, 'null'); + async_test((t) => {t.done(); return undefined;}, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'ERROR'); + assert_equals( + harness.message, + 'Test named "null" passed a function to `async_test` that returned a value.' + ); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.null, 'PASS'); + // This test did not get the chance to start. + assert_equals(tests.after, undefined); + }); +}, 'test returning `null`'); + +promise_test(() => { + return makeTest( + () => { + async_test((t) => {t.done(); return undefined;}, 'before'); + async_test((t) => {t.done(); return {};}, 'object'); + async_test((t) => {t.done(); return undefined;}, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'ERROR'); + assert_equals( + harness.message, + 'Test named "object" passed a function to `async_test` that returned a value.' + ); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.object, 'PASS'); + // This test did not get the chance to start. + assert_equals(tests.after, undefined); + }); +}, 'test returning an ordinary object'); + +promise_test(() => { + return makeTest( + () => { + async_test((t) => {t.done(); return undefined;}, 'before'); + async_test((t) => {t.done(); return Promise.resolve(5);}, 'thenable'); + async_test((t) => {t.done(); return undefined;}, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'ERROR'); + assert_equals( + harness.message, + 'Test named "thenable" passed a function to `async_test` that returned a value. ' + + 'Consider using `promise_test` instead when using Promises or async/await.' + ); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.thenable, 'PASS'); + // This test did not get a chance to start. + assert_equals(tests.after, undefined); + }); +}, 'test returning a thenable object'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/basic.html b/testing/web-platform/tests/resources/test/tests/unit/basic.html new file mode 100644 index 0000000000..d52082f2e0 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/basic.html @@ -0,0 +1,48 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>idlharness basic</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/WebIDLParser.js"></script> +<script src="/resources/idlharness.js"></script> +<script> + "use strict"; + test(function() { + assert_true("IdlArray" in window); + }, 'IdlArray constructor should be a global object'); + test(function() { + assert_true(new IdlArray() instanceof IdlArray); + }, 'IdlArray constructor should be constructible'); + test(function() { + assert_true("WebIDL2" in window); + }, 'WebIDL2 namespace should be a global object'); + test(function() { + assert_equals(typeof WebIDL2.parse, "function"); + }, 'WebIDL2 namespace should have a parse method'); + test(function() { + try { + WebIDL2.parse("I'm a syntax error"); + throw new Error("Web IDL didn't throw"); + } catch (e) { + assert_equals(e.name, "WebIDLParseError"); + } + }, 'WebIDL2 parse method should bail on incorrect WebIDL'); + test(function() { + assert_equals(typeof WebIDL2.parse("interface Foo {};"), "object"); + }, 'WebIDL2 parse method should produce an AST for correct WebIDL'); + test(function () { + try { + let i = new IdlArray(); + i.add_untested_idls(`interface C {};`); + i.assert_throws('Anything', i => i.test()); + } catch (e) { + assert_true(e instanceof IdlHarnessError); + } + }, `assert_throws should throw if no IdlHarnessError thrown`); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html new file mode 100644 index 0000000000..760ac7154f --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases-timeouts.html @@ -0,0 +1,120 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <title>Exceptional cases - timeouts</title> +</head> +<body> +<p> + The tests in this file are executed in parallel to avoid exceeding the "long" + timeout duration. +</p> +<script> +function makeTest(...bodies) { + const closeScript = '<' + '/script>'; + let src = ` +<!DOCTYPE HTML> +<html> +<head> +<title>Document title</title> +<script src="/resources/testharness.js?${Math.random()}">${closeScript} +</head> + +<body> +<div id="log"></div>`; + bodies.forEach((body) => { + src += '<script>(' + body + ')();' + closeScript; + }); + + const iframe = document.createElement('iframe'); + + document.body.appendChild(iframe); + iframe.contentDocument.write(src); + + return new Promise((resolve) => { + window.addEventListener('message', function onMessage(e) { + if (e.source !== iframe.contentWindow) { + return; + } + if (!e.data || e.data.type !=='complete') { + return; + } + window.removeEventListener('message', onMessage); + resolve(e.data); + }); + + iframe.contentDocument.close(); + }).then(({ tests, status }) => { + const summary = { + harness: getEnumProp(status, status.status), + tests: {} + }; + + tests.forEach((test) => { + summary.tests[test.name] = getEnumProp(test, test.status); + }); + + return summary; + }); +} + +function getEnumProp(object, value) { + for (let property in object) { + if (!/^[A-Z]+$/.test(property)) { + continue; + } + + if (object[property] === value) { + return property; + } + } +} + +(() => { + window.asyncTestCleanupCount1 = 0; + const nestedTest = makeTest( + () => { + async_test((t) => { + t.add_cleanup(() => window.parent.asyncTestCleanupCount1 += 1); + setTimeout(() => { + throw new Error('this error is expected'); + }); + }, 'test'); + } + ); + promise_test(() => { + return nestedTest.then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.test, 'TIMEOUT'); + assert_equals(window.asyncTestCleanupCount1, 1); + }); + }, 'uncaught exception during async_test which times out'); +})(); + +(() => { + window.promiseTestCleanupCount2 = 0; + const nestedTest = makeTest( + () => { + promise_test((t) => { + t.add_cleanup(() => window.parent.promiseTestCleanupCount2 += 1); + setTimeout(() => { + throw new Error('this error is expected'); + }); + + return new Promise(() => {}); + }, 'test'); + } + ); + promise_test(() => { + return nestedTest.then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.test, 'TIMEOUT'); + assert_equals(window.promiseTestCleanupCount2, 1); + }); + }, 'uncaught exception during promise_test which times out'); +})(); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html new file mode 100644 index 0000000000..4054d0311d --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/exceptional-cases.html @@ -0,0 +1,392 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <title>Exceptional cases</title> +</head> +<body> +<script> +function makeTest(...bodies) { + const closeScript = '<' + '/script>'; + let src = ` +<!DOCTYPE HTML> +<html> +<head> +<title>Document title</title> +<script src="/resources/testharness.js?${Math.random()}">${closeScript} +</head> + +<body> +<div id="log"></div>`; + bodies.forEach((body) => { + src += '<script>(' + body + ')();' + closeScript; + }); + + const iframe = document.createElement('iframe'); + + document.body.appendChild(iframe); + iframe.contentDocument.write(src); + + return new Promise((resolve) => { + window.addEventListener('message', function onMessage(e) { + if (e.source !== iframe.contentWindow) { + return; + } + if (!e.data || e.data.type !=='complete') { + return; + } + window.removeEventListener('message', onMessage); + resolve(e.data); + }); + + iframe.contentDocument.close(); + }).then(({ tests, status }) => { + const summary = { + harness: getEnumProp(status, status.status), + tests: {} + }; + + tests.forEach((test) => { + summary.tests[test.name] = getEnumProp(test, test.status); + }); + + return summary; + }); +} + +function getEnumProp(object, value) { + for (let property in object) { + if (!/^[A-Z]+$/.test(property)) { + continue; + } + + if (object[property] === value) { + return property; + } + } +} + +promise_test(() => { + return makeTest( + () => { done(); } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_array_equals(Object.keys(tests), []); + }); +}, 'completion signaled before testing begins'); + +promise_test(() => { + return makeTest( + () => { assert_true(true); done(); } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_array_equals(Object.keys(tests), []); + }); +}, 'passing assertion before testing begins'); + +promise_test(() => { + return makeTest( + () => { assert_false(true); } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_array_equals(Object.keys(tests), []); + }); +}, 'failing assertion before testing begins'); + +promise_test(() => { + return makeTest( + () => { throw new Error('this error is expected'); } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_array_equals(Object.keys(tests), []); + }); +}, 'uncaught exception before testing begins'); + +promise_test(() => { + return makeTest( + () => { + setup({ allow_uncaught_exception: true }); + throw new Error('this error is expected'); + }, + () => { + test(function() {}, 'a'); + test(function() {}, 'b'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests.a, 'PASS'); + assert_equals(tests.b, 'PASS'); + }); +}, 'uncaught exception with subsequent subtest'); + +promise_test(() => { + return makeTest( + () => { + async_test((t) => { + setTimeout(() => { + setTimeout(() => t.done(), 0); + async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after'); + throw new Error('this error is expected'); + }, 0); + }, 'during'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.during, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'uncaught exception during async_test'); + +promise_test(() => { + return makeTest( + () => { + promise_test(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + promise_test(() => Promise.resolve(), 'after'); + throw new Error('this error is expected'); + }, 0); + }); + }, 'during'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.during, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'uncaught exception during promise_test'); + +promise_test(() => { + return makeTest( + () => { test(() => {}, 'before'); }, + () => { throw new Error('this error is expected'); }, + () => { test(() => {}, 'after'); } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'uncaught exception between tests'); + +promise_test(() => { + return makeTest( + () => { promise_test(() => Promise.resolve(), 'before'); }, + () => { throw new Error('this error is expected'); }, + () => { promise_test(() => Promise.resolve(), 'after'); } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'uncaught exception between promise_tests'); + + +// This feature of testharness.js is only observable in browsers which +// implement the `unhandledrejection` event. +if ('onunhandledrejection' in window) { + + promise_test(() => { + return makeTest( + () => { Promise.reject(new Error('this error is expected')); } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_array_equals(Object.keys(tests), []); + }); + }, 'unhandled rejection before testing begins'); + + promise_test(() => { + return makeTest( + () => { + async_test((t) => { + Promise.reject(new Error('this error is expected')); + + window.addEventListener('unhandledrejection', () => { + setTimeout(() => t.done(), 0); + async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after'); + t.done(); + }); + }, 'during'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.during, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); + }, 'unhandled rejection during async_test'); + + promise_test(() => { + return makeTest( + () => { + promise_test(() => { + return new Promise((resolve) => { + Promise.reject(new Error('this error is expected')); + + window.addEventListener('unhandledrejection', () => { + resolve(); + promise_test(() => Promise.resolve(), 'after'); + throw new Error('this error is expected'); + }, 0); + }); + }, 'during'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.during, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); + }, 'unhandled rejection during promise_test'); + + promise_test(() => { + return makeTest( + () => { + setup({ explicit_done: true }); + test(() => {}, 'before'); + Promise.reject(new Error('this error is expected')); + window.addEventListener('unhandledrejection', () => { + test(() => {}, 'after'); + done(); + }); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_true('after' in tests); + }); + }, 'unhandled rejection between tests'); + + promise_test(() => { + return makeTest( + () => { + setup({ explicit_done: true }); + async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'before'); + Promise.reject(new Error('this error is expected')); + window.addEventListener('unhandledrejection', () => { + async_test((t) => { setTimeout(t.done.bind(t), 0); }, 'after'); + done(); + }); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); + }, 'unhandled rejection between async_tests'); + + promise_test(() => { + return makeTest( + () => { + setup({ explicit_done: true }); + promise_test(() => Promise.resolve(), 'before'); + Promise.reject(new Error('this error is expected')); + window.addEventListener('unhandledrejection', () => { + promise_test(() => Promise.resolve(), 'after'); + done(); + }); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_true('after' in tests); + }); + }, 'unhandled rejection between promise_tests'); + + promise_test(() => { + return makeTest( + () => { + test((t) => { + t.add_cleanup(() => { throw new Error('this error is expected'); }); + }, 'during'); + test((t) => {}, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.during, 'PASS'); + assert_equals(tests.after, 'NOTRUN'); + }); + }, 'exception in `add_cleanup` of a test'); + +} + + +promise_test(() => { + return makeTest( + () => { + setup({explicit_done: true}); + window.addEventListener('DOMContentLoaded', () => { + async_test((t) => { + t.add_cleanup(() => { + setTimeout(() => { + async_test((t) => t.done(), 'after'); + done(); + }, 0); + throw new Error('this error is expected'); + }); + setTimeout(t.done.bind(t), 0); + }, 'during'); + }); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.during, 'PASS'); + assert_equals(tests.after, 'NOTRUN'); + }); +}, 'exception in `add_cleanup` of an async_test'); + +promise_test(() => { + return makeTest( + () => { + promise_test((t) => { + t.add_cleanup(() => { throw new Error('this error is expected'); }); + return Promise.resolve(); + }, 'test'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.test, 'PASS'); + }); +}, 'exception in `add_cleanup` of a promise_test'); + +promise_test(() => { + return makeTest( + () => { + promise_test((t) => { + t.step(() => { + throw new Error('this error is expected'); + }); + }, 'test'); + async_test((t) => t.done(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests.test, 'FAIL'); + assert_equals(tests.after, 'PASS'); + }); +}, 'exception in `step` of an async_test'); + +promise_test(() => { + return makeTest( + () => { + promise_test((t) => { + t.step(() => { + throw new Error('this error is expected'); + }); + + return new Promise(() => {}); + }, 'test'); + + // This following test should be run to completion despite the fact + // that the promise returned by the previous test never resolves. + promise_test((t) => Promise.resolve(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests.test, 'FAIL'); + assert_equals(tests.after, 'PASS'); + }); +}, 'exception in `step` of a promise_test'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/format-value.html b/testing/web-platform/tests/resources/test/tests/unit/format-value.html new file mode 100644 index 0000000000..13d01b81f3 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/format-value.html @@ -0,0 +1,123 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>format_value utility function</title> + <meta charset="utf-8"> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +"use strict"; + +test(function() { + assert_equals(format_value(null), "null"); +}, "null"); + +test(function() { + assert_equals(format_value(undefined), "undefined"); +}, "undefined"); + +test(function() { + assert_equals(format_value(true), "true"); + assert_equals(format_value(false), "false"); +}, "boolean values"); + +test(function() { + assert_equals(format_value(0.4), "0.4"); + assert_equals(format_value(0), "0"); + assert_equals(format_value(-0), "-0"); +}, "number values"); + +test(function() { + assert_equals(format_value("a string"), "\"a string\""); + assert_equals(format_value("new\nline"), "\"new\\nline\""); +}, "string values"); + +test(function() { + var node = document.createElement("span"); + node.setAttribute("data-foo", "bar"); + assert_true( + /<span\b/i.test(format_value(node)), "element includes tag name" + ); + assert_true( + /data-foo=["']?bar["']?/i.test(format_value(node)), + "element includes attributes" + ); +}, "node value: element node"); + +test(function() { + var text = document.createTextNode("wpt"); + assert_equals(format_value(text), "Text node \"wpt\""); +}, "node value: text node"); + +test(function() { + var node = document.createProcessingInstruction("wpt1", "wpt2"); + assert_equals( + format_value(node), + "ProcessingInstruction node with target \"wpt1\" and data \"wpt2\"" + ); +}, "node value: ProcessingInstruction node"); + +test(function() { + var node = document.createComment("wpt"); + assert_equals(format_value(node), "Comment node <!--wpt-->"); +}, "node value: comment node"); + +test(function() { + var node = document.implementation.createDocument( + "application/xhtml+xml", "", null + ); + + assert_equals(format_value(node), "Document node with 0 children"); + + node.appendChild(document.createElement('html')); + + assert_equals(format_value(node), "Document node with 1 child"); +}, "node value: document node"); + +test(function() { + var node = document.implementation.createDocumentType("foo", "baz", "baz"); + + assert_equals(format_value(node), "DocumentType node"); +}, "node value: DocumentType node"); + +test(function() { + var node = document.createDocumentFragment(); + + assert_equals(format_value(node), "DocumentFragment node with 0 children"); + + node.appendChild(document.createElement("span")); + + assert_equals(format_value(node), "DocumentFragment node with 1 child"); + + node.appendChild(document.createElement("span")); + + assert_equals(format_value(node), "DocumentFragment node with 2 children"); +}, "node value: DocumentFragment node"); + +test(function() { + assert_equals(format_value(Symbol("wpt")), "symbol \"Symbol(wpt)\""); +}, "symbol value"); + +test(function() { + assert_equals(format_value([]), "[]"); + assert_equals(format_value(["one"]), "[\"one\"]"); + assert_equals(format_value(["one", "two"]), "[\"one\", \"two\"]"); +}, "array values"); + +test(function() { + var obj = { + toString: function() { + throw "wpt"; + } + }; + + assert_equals( + format_value(obj), "[stringifying object threw wpt with type string]" + ); +}, "object value with faulty `toString`"); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/helpers.js b/testing/web-platform/tests/resources/test/tests/unit/helpers.js new file mode 100644 index 0000000000..ca378a27c9 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/helpers.js @@ -0,0 +1,21 @@ +// Helper for testing assertion failure cases for a testharness.js API +// +// The `assert_throws_*` functions cannot be used for this purpose because they +// always fail in response to AssertionError exceptions, even when this is +// expressed as the expected error. +function test_failure(fn, name) { + test(function() { + try { + fn(); + } catch (err) { + if (err instanceof AssertionError) { + return; + } + throw new AssertionError('Expected an AssertionError, but' + err); + } + throw new AssertionError( + 'Expected an AssertionError, but no error was thrown' + ); + }, name); +} + diff --git a/testing/web-platform/tests/resources/test/tests/unit/late-test.html b/testing/web-platform/tests/resources/test/tests/unit/late-test.html new file mode 100644 index 0000000000..c9f8ec61fe --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/late-test.html @@ -0,0 +1,56 @@ +<!DOCTYPE HTML> +<html> +<head> +<title>Test declared after harness completion</title> +</head> +<body> +<div id="log"></div> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<p>This test simulates an automated test running scenario, where the test +results emitted by testharness.js may be interpreted after some delay. It is +intended to demonstrate that in such cases, any additional tests which are +executed during that delay are included in the dataset.</p> + +<p>Although these "late tests" are likely an indication of a mistake in test +design, they are also recorded. Previously, "late tests" were ignored. +This test changed to assert "late tests" were no longer ignored after +https://github.com/web-platform-tests/wpt/pull/38806 was introduced.</p> +<script> +async_test(function(t) { + var source = [ + "<div id='log'></div>", + "<script src='/resources/testharness.js'></" + "script>", + "<script src='/resources/testharnessreport.js'></" + "script>", + "<script>", + "parent.childReady(window);", + "setup({ explicit_done: true });", + "test(function() {}, 'acceptable test');", + "onload = function() {", + " done();", + " test(function() {}, 'test registered in onload handler');", + "};", + "</" + "script>" + ].join("\n"); + var iframe = document.createElement("iframe"); + + document.body.appendChild(iframe); + window.childReady = t.step_func(function(childWindow) { + childWindow.add_completion_callback(t.step_func(function(tests, status) { + t.step_timeout(t.step_func(function() { + assert_equals(tests.length, 2); + assert_equals(tests[0].name, "acceptable test"); + assert_equals(tests[1].name, "test registered in onload handler"); + assert_equals(status.status, status.OK); + t.done(); + }), 0); + })); + }); + + iframe.contentDocument.open(); + iframe.contentDocument.write(source); + iframe.contentDocument.close(); +}); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html b/testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html new file mode 100644 index 0000000000..c4947feef4 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/promise_setup-timeout.html @@ -0,0 +1,28 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="../../nested-testharness.js"></script> + <title>promise_setup - timeout</title> +</head> +<body> +<script> +'use strict'; +promise_test(() => { + return makeTest( + () => { + test(() => {}, 'before'); + promise_setup(() => new Promise(() => {})); + promise_test(() => Promise.resolve(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'TIMEOUT'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'NOTRUN'); + }); +}, 'timeout when returned promise does not settle'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/promise_setup.html b/testing/web-platform/tests/resources/test/tests/unit/promise_setup.html new file mode 100644 index 0000000000..2abb10a476 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/promise_setup.html @@ -0,0 +1,333 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="/resources/testharness.js"></script> + <script src="../../nested-testharness.js"></script> + <title>promise_setup</title> +</head> +<body> +<script> +'use strict'; +promise_test(() => { + return makeTest( + () => { + // Ensure that the harness error is the result of explicit error + // handling + setup({ allow_uncaught_exception: true }); + + test(() => {}, 'before'); + promise_setup({}); + promise_test(() => Promise.resolve(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, undefined); + }); +}, 'Error when no function provided'); + +promise_test(() => { + return makeTest( + () => { + test(() => {}, 'before'); + promise_setup(() => Promise.resolve(), {}); + promise_test(() => Promise.resolve(), 'after'); + throw new Error('this error is expected'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'Does not apply unspecified configuration properties'); + +promise_test(() => { + return makeTest( + () => { + var properties = { + allow_uncaught_exception: true + }; + test(() => {}, 'before'); + promise_setup(() => Promise.resolve(), properties); + promise_test(() => Promise.resolve(), 'after'); + throw new Error('this error is expected'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'Ignores configuration properties when some tests have already run'); + +promise_test(() => { + return makeTest( + () => { + var properties = { + allow_uncaught_exception: true + }; + promise_setup(() => Promise.resolve(), properties); + promise_test(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + throw new Error('this error is expected'); + }); + }); + }, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests.after, 'PASS'); + }); +}, 'Honors configuration properties'); + +promise_test(() => { + return makeTest( + () => { + // Ensure that the harness error is the result of explicit error + // handling + setup({ allow_uncaught_exception: true }); + + test(() => {}, 'before'); + promise_setup(() => { throw new Error('this error is expected'); }); + promise_test(() => Promise.resolve(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'NOTRUN'); + }); +}, 'Error for synchronous exceptions'); + +promise_test(() => { + return makeTest( + () => { + // Ensure that the harness error is the result of explicit error + // handling + setup({ allow_uncaught_exception: true }); + + test(() => {}, 'before'); + promise_setup(() => undefined); + promise_test(() => Promise.resolve(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'NOTRUN'); + }); +}, 'Error for missing return value'); + +promise_test(() => { + return makeTest( + () => { + // Ensure that the harness error is the result of explicit error + // handling + setup({ allow_uncaught_exception: true }); + + test(() => {}, 'before'); + var noThen = Promise.resolve(); + noThen.then = undefined; + promise_setup(() => noThen); + promise_test(() => Promise.resolve(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'NOTRUN'); + }); +}, 'Error for non-thenable return value'); + +promise_test(() => { + return makeTest( + () => { + // Ensure that the harness error is the result of explicit error + // handling + setup({ allow_uncaught_exception: true }); + + test(() => {}, 'before'); + var poisonedThen = { + get then() { + throw new Error('this error is expected'); + } + }; + promise_setup(() => poisonedThen); + promise_test(() => Promise.resolve(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'NOTRUN'); + }); +}, 'Error for "poisoned" `then` property'); + +promise_test(() => { + return makeTest( + () => { + // Ensure that the harness error is the result of explicit error + // handling + setup({ allow_uncaught_exception: true }); + + test(() => {}, 'before'); + var badThen = { + then() { + throw new Error('this error is expected'); + } + }; + promise_setup(() => badThen); + promise_test(() => Promise.resolve(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'NOTRUN'); + }); +}, 'Error for synchronous error from `then` method'); + +promise_test(() => { + return makeTest( + () => { + // Ensure that the harness error is the result of explicit error + // handling + setup({ allow_uncaught_exception: true }); + + test(() => {}, 'before'); + promise_setup(() => Promise.resolve()); + test(() => {}, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, undefined); + }); +}, 'Error for subsequent invocation of `test`'); + +promise_test(() => { + return makeTest( + () => { + // Ensure that the harness error is the result of explicit error + // handling + setup({ allow_uncaught_exception: true }); + + test(() => {}, 'before'); + promise_setup(() => Promise.resolve()); + async_test((t) => t.done(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, undefined); + }); +}, 'Error for subsequent invocation of `async_test`'); + +promise_test(() => { + return makeTest( + () => { + // Ensure that the harness error is the result of explicit error + // handling + setup({ allow_uncaught_exception: true }); + + test(() => {}, 'before'); + promise_setup(() => Promise.reject()); + promise_test(() => Promise.resolve(), 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'NOTRUN'); + }); +}, 'Error for rejected promise'); + +promise_test(() => { + var expected_sequence = [ + 'test body', + 'promise_setup begin', + 'promise_setup end', + 'promise_test body' + ]; + var actual_sequence = window.actual_sequence = []; + + return makeTest( + () => { + test(() => { parent.actual_sequence.push('test body'); }, 'before'); + promise_setup(() => { + parent.actual_sequence.push('promise_setup begin'); + + return Promise.resolve() + .then(() => new Promise((resolve) => setTimeout(resolve, 300))) + .then(() => parent.actual_sequence.push('promise_setup end')); + }); + promise_test(() => { + parent.actual_sequence.push('promise_test body'); + return Promise.resolve(); + }, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'PASS'); + assert_array_equals(actual_sequence, expected_sequence); + }); +}, 'Waits for promise to settle'); + +promise_test(() => { + var expected_sequence = [ + 'promise_test 1 begin', + 'promise_test 1 end', + 'promise_setup begin', + 'promise_setup end', + 'promise_test 2 body' + ]; + var actual_sequence = window.actual_sequence = []; + + return makeTest( + () => { + promise_test((t) => { + parent.actual_sequence.push('promise_test 1 begin'); + + return Promise.resolve() + .then(() => new Promise((resolve) => t.step_timeout(resolve, 300))) + .then(() => parent.actual_sequence.push('promise_test 1 end')); + }, 'before'); + promise_setup(() => { + parent.actual_sequence.push('promise_setup begin'); + + return Promise.resolve() + .then(() => new Promise((resolve) => setTimeout(resolve, 300))) + .then(() => parent.actual_sequence.push('promise_setup end')); + }); + promise_test(() => { + parent.actual_sequence.push('promise_test 2 body'); + return Promise.resolve(); + }, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.after, 'PASS'); + assert_array_equals(actual_sequence, expected_sequence); + }); +}, 'Waits for existing promise_test to complete'); + +promise_test(() => { + return makeTest( + () => { + var properties = { allow_uncaught_exception: true }; + promise_test(() => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(); + throw new Error('this error is expected'); + }); + }); + }, 'before'); + promise_setup(() => Promise.resolve(), properties); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'ERROR'); + assert_equals(tests.before, 'PASS'); + }); +}, 'Defers application of setup properties'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/single_test.html b/testing/web-platform/tests/resources/test/tests/unit/single_test.html new file mode 100644 index 0000000000..ff766e66ce --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/single_test.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <script src="../../nested-testharness.js"></script> + <title>single_test</title> +</head> +<body> +<script> +promise_test(() => { + return makeTest( + () => { + setup({ single_test: true }); + done(); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'Expected usage'); + +promise_test(() => { + return makeTest( + () => { + setup({ single_test: true }); + throw new Error('this error is expected'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'Uncaught exception'); + +promise_test(() => { + return makeTest( + () => { + setup({ single_test: true }); + Promise.reject(new Error('this error is expected')); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'Unhandled rejection'); + +promise_test(() => { + return makeTest( + () => { + setup({ single_test: true }); + test(function() {}, 'sync test'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + assert_equals( + Object.keys(tests).length, 1, 'no additional subtests created' + ); + }); +}, 'Erroneous usage: subtest declaration (synchronous test)'); + +promise_test(() => { + return makeTest( + () => { + setup({ single_test: true }); + async_test(function(t) { t.done(); }, 'async test'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + assert_equals( + Object.keys(tests).length, 1, 'no additional subtests created' + ); + }); +}, 'Erroneous usage: subtest declaration (asynchronous test)'); + +promise_test(() => { + return makeTest( + () => { + setup({ single_test: true }); + promise_test(function() { return Promise.resolve(); }, 'promise test'); + } + ).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + assert_equals( + Object.keys(tests).length, 1, 'no additional subtests created' + ); + }); +}, 'Erroneous usage: subtest declaration (promise test)'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html b/testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html new file mode 100644 index 0000000000..0295c5214d --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/test-return-restrictions.html @@ -0,0 +1,156 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="/resources/testharness.js"></script> + <title>Restrictions on return value from `test`</title> +</head> +<body> +<script> +function makeTest(...bodies) { + const closeScript = '<' + '/script>'; + let src = ` +<!DOCTYPE HTML> +<html> +<head> +<title>Document title</title> +<script src="/resources/testharness.js?${Math.random()}">${closeScript} +</head> + +<body> +<div id="log"></div>`; + bodies.forEach((body) => { + src += '<script>(' + body + ')();' + closeScript; + }); + + const iframe = document.createElement('iframe'); + + document.body.appendChild(iframe); + iframe.contentDocument.write(src); + + return new Promise((resolve) => { + window.addEventListener('message', function onMessage(e) { + if (e.source !== iframe.contentWindow) { + return; + } + if (!e.data || e.data.type !=='complete') { + return; + } + window.removeEventListener('message', onMessage); + resolve(e.data); + }); + + iframe.contentDocument.close(); + }).then(({ tests, status }) => { + const summary = { + harness: { + status: getEnumProp(status, status.status), + message: status.message + }, + tests: {} + }; + + tests.forEach((test) => { + summary.tests[test.name] = getEnumProp(test, test.status); + }); + + return summary; + }); +} + +function getEnumProp(object, value) { + for (let property in object) { + if (!/^[A-Z]+$/.test(property)) { + continue; + } + + if (object[property] === value) { + return property; + } + } +} + +promise_test(() => { + return makeTest( + () => { + test(() => undefined, 'before'); + test(() => null, 'null'); + test(() => undefined, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'ERROR'); + assert_equals( + harness.message, + 'Test named "null" passed a function to `test` that returned a value.' + ); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.null, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'test returning `null`'); + +promise_test(() => { + return makeTest( + () => { + test(() => undefined, 'before'); + test(() => ({}), 'object'); + test(() => undefined, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'ERROR'); + assert_equals( + harness.message, + 'Test named "object" passed a function to `test` that returned a value.' + ); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.object, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'test returning an ordinary object'); + +promise_test(() => { + return makeTest( + () => { + test(() => undefined, 'before'); + test(() => Promise.resolve(5), 'thenable'); + test(() => undefined, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'ERROR'); + assert_equals( + harness.message, + 'Test named "thenable" passed a function to `test` that returned a value. ' + + 'Consider using `promise_test` instead when using Promises or async/await.' + ); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.thenable, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'test returning a thenable object'); + +promise_test(() => { + return makeTest( + () => { + test(() => undefined, 'before'); + test(() => { + const iframe = document.createElement('iframe'); + iframe.setAttribute('sandbox', ''); + document.body.appendChild(iframe); + return iframe.contentWindow; + }, 'restricted'); + test(() => undefined, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'ERROR'); + assert_equals( + harness.message, + 'Test named "restricted" passed a function to `test` that returned a value.' + ); + assert_equals(tests.before, 'PASS'); + assert_equals(tests.restricted, 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'test returning a restricted object'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html b/testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html new file mode 100644 index 0000000000..a36a56043c --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/throwing-assertions.html @@ -0,0 +1,268 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <meta name="timeout" content="long"> + <script src="/resources/testharness.js"></script> + <title>Test the methods that make assertions about exceptions</title> +</head> +<body> +<script> +function makeTest(...bodies) { + const closeScript = '<' + '/script>'; + let src = ` +<!DOCTYPE HTML> +<html> +<head> +<title>Document title</title> +<script src="/resources/testharness.js?${Math.random()}">${closeScript} +</head> + +<body> +<div id="log"></div>`; + bodies.forEach((body) => { + src += '<script>(' + body + ')();' + closeScript; + }); + + const iframe = document.createElement('iframe'); + + document.body.appendChild(iframe); + iframe.contentDocument.write(src); + + return new Promise((resolve) => { + window.addEventListener('message', function onMessage(e) { + if (e.source !== iframe.contentWindow) { + return; + } + if (!e.data || e.data.type !=='complete') { + return; + } + window.removeEventListener('message', onMessage); + resolve(e.data); + }); + + iframe.contentDocument.close(); + }).then(({ tests, status }) => { + const summary = { + harness: getEnumProp(status, status.status), + tests: {} + }; + + tests.forEach((test) => { + summary.tests[test.name] = getEnumProp(test, test.status); + }); + + return summary; + }); +} + +function getEnumProp(object, value) { + for (let property in object) { + if (!/^[A-Z]+$/.test(property)) { + continue; + } + + if (object[property] === value) { + return property; + } + } +} + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_js(TypeError, () => { throw new TypeError(); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'assert_throws_js on a TypeError'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_js(RangeError, () => { throw new RangeError(); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'assert_throws_js on a RangeError'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_js(TypeError, () => { throw new RangeError(); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'assert_throws_js on a TypeError when RangeError is thrown'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_js(Error, () => { throw new TypeError(); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'assert_throws_js on an Error when TypeError is thrown'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_js(Error, + () => { throw new DOMException("hello", "Error"); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'assert_throws_js on an Error when a DOMException is thrown'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_js(SyntaxError, + () => { throw new DOMException("hey", "SyntaxError"); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'assert_throws_js on a SyntaxError when a DOMException is thrown'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_dom("SyntaxError", + () => { throw new DOMException("x", "SyntaxError"); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'assert_throws_dom basic sanity'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_dom(12, + () => { throw new DOMException("x", "SyntaxError"); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'assert_throws_dom with numeric code'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_dom("SYNTAX_ERR", + () => { throw new DOMException("x", "SyntaxError"); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'assert_throws_dom with string name for code'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_dom("DataError", + () => { throw new DOMException("x", "DataError"); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'assert_throws_dom for a code-less DOMException type'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_dom("NoSuchError", + () => { throw new DOMException("x", "NoSuchError"); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'assert_throws_dom for a nonexistent DOMException type'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_dom("SyntaxError", () => { throw new SyntaxError(); }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'assert_throws_dom when a non-DOM exception is thrown'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_exactly(5, () => { throw 5; }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'assert_throws_exactly with number'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_exactly("foo", () => { throw "foo"; }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'assert_throws_exactly with string'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_exactly({}, () => { throw {}; }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'assert_throws_exactly with different objects'); + +promise_test(() => { + return makeTest(() => { + test(() => { + var obj = {}; + assert_throws_exactly(obj, () => { throw obj; }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'PASS'); + }); +}, 'assert_throws_exactly with same object'); + +promise_test(() => { + return makeTest(() => { + test(() => { + assert_throws_exactly(TypeError, () => { throw new TypeError; }); + }); + }).then(({harness, tests}) => { + assert_equals(harness, 'OK'); + assert_equals(tests['Document title'], 'FAIL'); + }); +}, 'assert_throws_exactly with bogus TypeError bits '); + +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html b/testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html new file mode 100644 index 0000000000..b232111326 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tests/unit/unpaired-surrogates.html @@ -0,0 +1,143 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <script src="/resources/testharness.js"></script> + <title>Restrictions on return value from `test`</title> +</head> +<body> +<script> +function makeTest(...bodies) { + const closeScript = '<' + '/script>'; + let src = ` +<!DOCTYPE HTML> +<html> +<head> +<title>Document title</title> +<script src="/resources/testharness.js?${Math.random()}">${closeScript} +</head> + +<body> +<div id="log"></div>`; + bodies.forEach((body) => { + src += '<script>(' + body + ')();' + closeScript; + }); + + const iframe = document.createElement('iframe'); + + document.body.appendChild(iframe); + iframe.contentDocument.write(src); + + return new Promise((resolve) => { + window.addEventListener('message', function onMessage(e) { + if (e.source !== iframe.contentWindow) { + return; + } + if (!e.data || e.data.type !=='complete') { + return; + } + window.removeEventListener('message', onMessage); + resolve(e.data); + }); + + iframe.contentDocument.close(); + }).then(({ tests, status }) => { + const summary = { + harness: { + status: getEnumProp(status, status.status), + message: status.message + }, + tests: {} + }; + + tests.forEach((test) => { + summary.tests[test.name] = getEnumProp(test, test.status); + }); + + return summary; + }); +} + +function getEnumProp(object, value) { + for (let property in object) { + if (!/^[A-Z]+$/.test(property)) { + continue; + } + + if (object[property] === value) { + return property; + } + } +} + +promise_test(() => { + return makeTest( + () => { + test(() => {}, 'before'); + test(() => {}, 'U+d7ff is not modified: \ud7ff'); + test(() => {}, 'U+e000 is not modified: \ue000'); + test(() => {}, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'OK'); + assert_equals(harness.message, null); + assert_equals(tests.before, 'PASS'); + assert_equals(tests['U+d7ff is not modified: \ud7ff'], 'PASS'); + assert_equals(tests['U+e000 is not modified: \ue000'], 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'sub-test names which include valid code units'); + +promise_test(() => { + return makeTest( + () => { + test(() => {}, 'before'); + test(() => {}, 'U+d800U+dfff is not modified: \ud800\udfff'); + test(() => {}, 'U+dbffU+dc00 is not modified: \udbff\udc00'); + test(() => {}, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'OK'); + assert_equals(harness.message, null); + assert_equals(tests.before, 'PASS'); + assert_equals(tests['U+d800U+dfff is not modified: \ud800\udfff'], 'PASS'); + assert_equals(tests['U+dbffU+dc00 is not modified: \udbff\udc00'], 'PASS'); + assert_equals(tests.after, 'PASS'); + }); +}, 'sub-test names which include paired surrogates'); + +promise_test(() => { + return makeTest( + () => { + test(() => {}, 'before'); + test(() => {}, 'U+d800 must be sanitized: \ud800'); + test(() => {}, 'U+d800U+d801 must be sanitized: \ud800\ud801'); + test(() => {}, 'U+dfff must be sanitized: \udfff'); + test(() => {}, 'U+dc00U+d800U+dc00U+d800 must be sanitized: \udc00\ud800\udc00\ud800'); + test(() => {}, 'U+dbffU+dfffU+dfff must be sanitized: \udbff\udfff\udfff'); + test(() => {}, 'after'); + } + ).then(({harness, tests}) => { + assert_equals(harness.status, 'OK'); + assert_equals(harness.message, null); + assert_equals(tests.before, 'PASS'); + assert_equals(tests['U+d800 must be sanitized: U+d800'], 'PASS'); + assert_equals(tests['U+dfff must be sanitized: U+dfff'], 'PASS'); + assert_equals( + tests['U+d800U+d801 must be sanitized: U+d800U+d801'], + 'PASS' + ); + assert_equals( + tests['U+dc00U+d800U+dc00U+d800 must be sanitized: U+dc00\ud800\udc00U+d800'], + 'PASS' + ); + assert_equals( + tests['U+dbffU+dfffU+dfff must be sanitized: \udbff\udfffU+dfff'], + 'PASS' + ); + assert_equals(tests.after, 'PASS'); + }); +}, 'sub-test names which include unpaired surrogates'); +</script> +</body> +</html> diff --git a/testing/web-platform/tests/resources/test/tox.ini b/testing/web-platform/tests/resources/test/tox.ini new file mode 100644 index 0000000000..12013a1a70 --- /dev/null +++ b/testing/web-platform/tests/resources/test/tox.ini @@ -0,0 +1,13 @@ +[tox] +envlist = py37,py38,py39,py310,py311 +skipsdist=True + +[testenv] +passenv=DISPLAY # Necessary for the spawned GeckoDriver process to connect to + # the appropriate display. + +deps = + -r{toxinidir}/../../tools/requirements_pytest.txt + -r{toxinidir}/requirements.txt + +commands = pytest -vv {posargs} diff --git a/testing/web-platform/tests/resources/test/wptserver.py b/testing/web-platform/tests/resources/test/wptserver.py new file mode 100644 index 0000000000..1f913dd96d --- /dev/null +++ b/testing/web-platform/tests/resources/test/wptserver.py @@ -0,0 +1,58 @@ +import logging +import os +import subprocess +import time +import sys +import urllib + + +class WPTServer(object): + def __init__(self, wpt_root): + self.logger = logging.getLogger() + self.wpt_root = wpt_root + + # This is a terrible hack to get the default config of wptserve. + sys.path.insert(0, os.path.join(wpt_root, "tools")) + from serve.serve import build_config + with build_config(self.logger) as config: + self.host = config["browser_host"] + self.http_port = config["ports"]["http"][0] + self.https_port = config["ports"]["https"][0] + + self.base_url = 'http://%s:%s' % (self.host, self.http_port) + self.https_base_url = 'https://%s:%s' % (self.host, self.https_port) + + def start(self, ssl_context): + self.devnull = open(os.devnull, 'w') + wptserve_cmd = [os.path.join(self.wpt_root, 'wpt'), 'serve'] + if sys.executable: + wptserve_cmd[0:0] = [sys.executable] + self.logger.info('Executing %s' % ' '.join(wptserve_cmd)) + self.proc = subprocess.Popen( + wptserve_cmd, + stderr=self.devnull, + cwd=self.wpt_root) + + for retry in range(5): + # Exponential backoff. + time.sleep(2 ** retry) + exit_code = self.proc.poll() + if exit_code != None: + logging.warning('Command "%s" exited with %s', ' '.join(wptserve_cmd), exit_code) + break + try: + urllib.request.urlopen(self.base_url, timeout=1) + urllib.request.urlopen(self.https_base_url, timeout=1, context=ssl_context) + return + except urllib.error.URLError: + pass + + raise Exception('Could not start wptserve on %s' % self.base_url) + + def stop(self): + self.proc.terminate() + self.proc.wait() + self.devnull.close() + + def url(self, abs_path): + return self.https_base_url + '/' + os.path.relpath(abs_path, self.wpt_root) diff --git a/testing/web-platform/tests/resources/testdriver-actions.js b/testing/web-platform/tests/resources/testdriver-actions.js new file mode 100644 index 0000000000..3e5ba74b4c --- /dev/null +++ b/testing/web-platform/tests/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; +})(); diff --git a/testing/web-platform/tests/resources/testdriver-vendor.js b/testing/web-platform/tests/resources/testdriver-vendor.js new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/web-platform/tests/resources/testdriver-vendor.js diff --git a/testing/web-platform/tests/resources/testdriver-vendor.js.headers b/testing/web-platform/tests/resources/testdriver-vendor.js.headers new file mode 100644 index 0000000000..5e8f640c66 --- /dev/null +++ b/testing/web-platform/tests/resources/testdriver-vendor.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript; charset=utf-8 +Cache-Control: max-age=3600 diff --git a/testing/web-platform/tests/resources/testdriver.js b/testing/web-platform/tests/resources/testdriver.js new file mode 100644 index 0000000000..20140b2fc0 --- /dev/null +++ b/testing/web-platform/tests/resources/testdriver.js @@ -0,0 +1,1208 @@ +(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); + } + }; + + 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"); + } + }; +})(); diff --git a/testing/web-platform/tests/resources/testdriver.js.headers b/testing/web-platform/tests/resources/testdriver.js.headers new file mode 100644 index 0000000000..5e8f640c66 --- /dev/null +++ b/testing/web-platform/tests/resources/testdriver.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript; charset=utf-8 +Cache-Control: max-age=3600 diff --git a/testing/web-platform/tests/resources/testharness.js b/testing/web-platform/tests/resources/testharness.js new file mode 100644 index 0000000000..1a6a4bb341 --- /dev/null +++ b/testing/web-platform/tests/resources/testharness.js @@ -0,0 +1,4975 @@ +/*global self*/ +/*jshint latedef: nofunc*/ + +/* Documentation: https://web-platform-tests.org/writing-tests/testharness-api.html + * (../docs/_writing-tests/testharness-api.md) */ + +(function (global_scope) +{ + // default timeout is 10 seconds, test can override if needed + var settings = { + output:true, + harness_timeout:{ + "normal":10000, + "long":60000 + }, + test_timeout:null, + message_events: ["start", "test_state", "result", "completion"], + debug: false, + }; + + var xhtml_ns = "http://www.w3.org/1999/xhtml"; + + /* + * TestEnvironment is an abstraction for the environment in which the test + * harness is used. Each implementation of a test environment has to provide + * the following interface: + * + * interface TestEnvironment { + * // Invoked after the global 'tests' object has been created and it's + * // safe to call add_*_callback() to register event handlers. + * void on_tests_ready(); + * + * // Invoked after setup() has been called to notify the test environment + * // of changes to the test harness properties. + * void on_new_harness_properties(object properties); + * + * // Should return a new unique default test name. + * DOMString next_default_test_name(); + * + * // Should return the test harness timeout duration in milliseconds. + * float test_timeout(); + * }; + */ + + /* + * A test environment with a DOM. The global object is 'window'. By default + * test results are displayed in a table. Any parent windows receive + * callbacks or messages via postMessage() when test events occur. See + * apisample11.html and apisample12.html. + */ + function WindowTestEnvironment() { + this.name_counter = 0; + this.window_cache = null; + this.output_handler = null; + this.all_loaded = false; + var this_obj = this; + this.message_events = []; + this.dispatched_messages = []; + + this.message_functions = { + start: [add_start_callback, remove_start_callback, + function (properties) { + this_obj._dispatch("start_callback", [properties], + {type: "start", properties: properties}); + }], + + test_state: [add_test_state_callback, remove_test_state_callback, + function(test) { + this_obj._dispatch("test_state_callback", [test], + {type: "test_state", + test: test.structured_clone()}); + }], + result: [add_result_callback, remove_result_callback, + function (test) { + this_obj.output_handler.show_status(); + this_obj._dispatch("result_callback", [test], + {type: "result", + test: test.structured_clone()}); + }], + completion: [add_completion_callback, remove_completion_callback, + function (tests, harness_status, asserts) { + var cloned_tests = map(tests, function(test) { + return test.structured_clone(); + }); + this_obj._dispatch("completion_callback", [tests, harness_status], + {type: "complete", + tests: cloned_tests, + status: harness_status.structured_clone(), + asserts: asserts.map(assert => assert.structured_clone())}); + }] + } + + on_event(window, 'load', function() { + setTimeout(() => { + this_obj.all_loaded = true; + if (tests.all_done()) { + tests.complete(); + } + },0); + }); + + on_event(window, 'message', function(event) { + if (event.data && event.data.type === "getmessages" && event.source) { + // A window can post "getmessages" to receive a duplicate of every + // message posted by this environment so far. This allows subscribers + // from fetch_tests_from_window to 'catch up' to the current state of + // this environment. + for (var i = 0; i < this_obj.dispatched_messages.length; ++i) + { + event.source.postMessage(this_obj.dispatched_messages[i], "*"); + } + } + }); + } + + WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) { + this.dispatched_messages.push(message_arg); + this._forEach_windows( + function(w, same_origin) { + if (same_origin) { + try { + var has_selector = selector in w; + } catch(e) { + // If document.domain was set at some point same_origin can be + // wrong and the above will fail. + has_selector = false; + } + if (has_selector) { + try { + w[selector].apply(undefined, callback_args); + } catch (e) {} + } + } + if (w !== self) { + w.postMessage(message_arg, "*"); + } + }); + }; + + WindowTestEnvironment.prototype._forEach_windows = function(callback) { + // Iterate over the windows [self ... top, opener]. The callback is passed + // two objects, the first one is the window object itself, the second one + // is a boolean indicating whether or not it's on the same origin as the + // current window. + var cache = this.window_cache; + if (!cache) { + cache = [[self, true]]; + var w = self; + var i = 0; + var so; + while (w != w.parent) { + w = w.parent; + so = is_same_origin(w); + cache.push([w, so]); + i++; + } + w = window.opener; + if (w) { + cache.push([w, is_same_origin(w)]); + } + this.window_cache = cache; + } + + forEach(cache, + function(a) { + callback.apply(null, a); + }); + }; + + WindowTestEnvironment.prototype.on_tests_ready = function() { + var output = new Output(); + this.output_handler = output; + + var this_obj = this; + + add_start_callback(function (properties) { + this_obj.output_handler.init(properties); + }); + + add_test_state_callback(function(test) { + this_obj.output_handler.show_status(); + }); + + add_result_callback(function (test) { + this_obj.output_handler.show_status(); + }); + + add_completion_callback(function (tests, harness_status, asserts_run) { + this_obj.output_handler.show_results(tests, harness_status, asserts_run); + }); + this.setup_messages(settings.message_events); + }; + + WindowTestEnvironment.prototype.setup_messages = function(new_events) { + var this_obj = this; + forEach(settings.message_events, function(x) { + var current_dispatch = this_obj.message_events.indexOf(x) !== -1; + var new_dispatch = new_events.indexOf(x) !== -1; + if (!current_dispatch && new_dispatch) { + this_obj.message_functions[x][0](this_obj.message_functions[x][2]); + } else if (current_dispatch && !new_dispatch) { + this_obj.message_functions[x][1](this_obj.message_functions[x][2]); + } + }); + this.message_events = new_events; + } + + WindowTestEnvironment.prototype.next_default_test_name = function() { + var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; + this.name_counter++; + return get_title() + suffix; + }; + + WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) { + this.output_handler.setup(properties); + if (properties.hasOwnProperty("message_events")) { + this.setup_messages(properties.message_events); + } + }; + + WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) { + on_event(window, 'load', callback); + }; + + WindowTestEnvironment.prototype.test_timeout = function() { + var metas = document.getElementsByTagName("meta"); + for (var i = 0; i < metas.length; i++) { + if (metas[i].name == "timeout") { + if (metas[i].content == "long") { + return settings.harness_timeout.long; + } + break; + } + } + return settings.harness_timeout.normal; + }; + + /* + * Base TestEnvironment implementation for a generic web worker. + * + * Workers accumulate test results. One or more clients can connect and + * retrieve results from a worker at any time. + * + * WorkerTestEnvironment supports communicating with a client via a + * MessagePort. The mechanism for determining the appropriate MessagePort + * for communicating with a client depends on the type of worker and is + * implemented by the various specializations of WorkerTestEnvironment + * below. + * + * A client document using testharness can use fetch_tests_from_worker() to + * retrieve results from a worker. See apisample16.html. + */ + function WorkerTestEnvironment() { + this.name_counter = 0; + this.all_loaded = true; + this.message_list = []; + this.message_ports = []; + } + + WorkerTestEnvironment.prototype._dispatch = function(message) { + this.message_list.push(message); + for (var i = 0; i < this.message_ports.length; ++i) + { + this.message_ports[i].postMessage(message); + } + }; + + // The only requirement is that port has a postMessage() method. It doesn't + // have to be an instance of a MessagePort, and often isn't. + WorkerTestEnvironment.prototype._add_message_port = function(port) { + this.message_ports.push(port); + for (var i = 0; i < this.message_list.length; ++i) + { + port.postMessage(this.message_list[i]); + } + }; + + WorkerTestEnvironment.prototype.next_default_test_name = function() { + var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; + this.name_counter++; + return get_title() + suffix; + }; + + WorkerTestEnvironment.prototype.on_new_harness_properties = function() {}; + + WorkerTestEnvironment.prototype.on_tests_ready = function() { + var this_obj = this; + add_start_callback( + function(properties) { + this_obj._dispatch({ + type: "start", + properties: properties, + }); + }); + add_test_state_callback( + function(test) { + this_obj._dispatch({ + type: "test_state", + test: test.structured_clone() + }); + }); + add_result_callback( + function(test) { + this_obj._dispatch({ + type: "result", + test: test.structured_clone() + }); + }); + add_completion_callback( + function(tests, harness_status, asserts) { + this_obj._dispatch({ + type: "complete", + tests: map(tests, + function(test) { + return test.structured_clone(); + }), + status: harness_status.structured_clone(), + asserts: asserts.map(assert => assert.structured_clone()), + }); + }); + }; + + WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {}; + + WorkerTestEnvironment.prototype.test_timeout = function() { + // Tests running in a worker don't have a default timeout. I.e. all + // worker tests behave as if settings.explicit_timeout is true. + return null; + }; + + /* + * Dedicated web workers. + * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope + * + * This class is used as the test_environment when testharness is running + * inside a dedicated worker. + */ + function DedicatedWorkerTestEnvironment() { + WorkerTestEnvironment.call(this); + // self is an instance of DedicatedWorkerGlobalScope which exposes + // a postMessage() method for communicating via the message channel + // established when the worker is created. + this._add_message_port(self); + } + DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() { + WorkerTestEnvironment.prototype.on_tests_ready.call(this); + // In the absence of an onload notification, we a require dedicated + // workers to explicitly signal when the tests are done. + tests.wait_for_finish = true; + }; + + /* + * Shared web workers. + * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope + * + * This class is used as the test_environment when testharness is running + * inside a shared web worker. + */ + function SharedWorkerTestEnvironment() { + WorkerTestEnvironment.call(this); + var this_obj = this; + // Shared workers receive message ports via the 'onconnect' event for + // each connection. + self.addEventListener("connect", + function(message_event) { + this_obj._add_message_port(message_event.source); + }, false); + } + SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + SharedWorkerTestEnvironment.prototype.on_tests_ready = function() { + WorkerTestEnvironment.prototype.on_tests_ready.call(this); + // In the absence of an onload notification, we a require shared + // workers to explicitly signal when the tests are done. + tests.wait_for_finish = true; + }; + + /* + * Service workers. + * http://www.w3.org/TR/service-workers/ + * + * This class is used as the test_environment when testharness is running + * inside a service worker. + */ + function ServiceWorkerTestEnvironment() { + WorkerTestEnvironment.call(this); + this.all_loaded = false; + this.on_loaded_callback = null; + var this_obj = this; + self.addEventListener("message", + function(event) { + if (event.data && event.data.type && event.data.type === "connect") { + this_obj._add_message_port(event.source); + } + }, false); + + // The oninstall event is received after the service worker script and + // all imported scripts have been fetched and executed. It's the + // equivalent of an onload event for a document. All tests should have + // been added by the time this event is received, thus it's not + // necessary to wait until the onactivate event. However, tests for + // installed service workers need another event which is equivalent to + // the onload event because oninstall is fired only on installation. The + // onmessage event is used for that purpose since tests using + // testharness.js should ask the result to its service worker by + // PostMessage. If the onmessage event is triggered on the service + // worker's context, that means the worker's script has been evaluated. + on_event(self, "install", on_all_loaded); + on_event(self, "message", on_all_loaded); + function on_all_loaded() { + if (this_obj.all_loaded) + return; + this_obj.all_loaded = true; + if (this_obj.on_loaded_callback) { + this_obj.on_loaded_callback(); + } + } + } + + ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) { + if (this.all_loaded) { + callback(); + } else { + this.on_loaded_callback = callback; + } + }; + + /* + * Shadow realms. + * https://github.com/tc39/proposal-shadowrealm + * + * This class is used as the test_environment when testharness is running + * inside a shadow realm. + */ + function ShadowRealmTestEnvironment() { + WorkerTestEnvironment.call(this); + this.all_loaded = false; + this.on_loaded_callback = null; + } + + ShadowRealmTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); + + /** + * Signal to the test environment that the tests are ready and the on-loaded + * callback should be run. + * + * Shadow realms are not *really* a DOM context: they have no `onload` or similar + * event for us to use to set up the test environment; so, instead, this method + * is manually triggered from the incubating realm + * + * @param {Function} message_destination - a function that receives JSON-serializable + * data to send to the incubating realm, in the same format as used by RemoteContext + */ + ShadowRealmTestEnvironment.prototype.begin = function(message_destination) { + if (this.all_loaded) { + throw new Error("Tried to start a shadow realm test environment after it has already started"); + } + var fakeMessagePort = {}; + fakeMessagePort.postMessage = message_destination; + this._add_message_port(fakeMessagePort); + this.all_loaded = true; + if (this.on_loaded_callback) { + this.on_loaded_callback(); + } + }; + + ShadowRealmTestEnvironment.prototype.add_on_loaded_callback = function(callback) { + if (this.all_loaded) { + callback(); + } else { + this.on_loaded_callback = callback; + } + }; + + /* + * JavaScript shells. + * + * This class is used as the test_environment when testharness is running + * inside a JavaScript shell. + */ + function ShellTestEnvironment() { + this.name_counter = 0; + this.all_loaded = false; + this.on_loaded_callback = null; + Promise.resolve().then(function() { + this.all_loaded = true + if (this.on_loaded_callback) { + this.on_loaded_callback(); + } + }.bind(this)); + this.message_list = []; + this.message_ports = []; + } + + ShellTestEnvironment.prototype.next_default_test_name = function() { + var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; + this.name_counter++; + return get_title() + suffix; + }; + + ShellTestEnvironment.prototype.on_new_harness_properties = function() {}; + + ShellTestEnvironment.prototype.on_tests_ready = function() {}; + + ShellTestEnvironment.prototype.add_on_loaded_callback = function(callback) { + if (this.all_loaded) { + callback(); + } else { + this.on_loaded_callback = callback; + } + }; + + ShellTestEnvironment.prototype.test_timeout = function() { + // Tests running in a shell don't have a default timeout, so behave as + // if settings.explicit_timeout is true. + return null; + }; + + function create_test_environment() { + if ('document' in global_scope) { + return new WindowTestEnvironment(); + } + if ('DedicatedWorkerGlobalScope' in global_scope && + global_scope instanceof DedicatedWorkerGlobalScope) { + return new DedicatedWorkerTestEnvironment(); + } + if ('SharedWorkerGlobalScope' in global_scope && + global_scope instanceof SharedWorkerGlobalScope) { + return new SharedWorkerTestEnvironment(); + } + if ('ServiceWorkerGlobalScope' in global_scope && + global_scope instanceof ServiceWorkerGlobalScope) { + return new ServiceWorkerTestEnvironment(); + } + if ('WorkerGlobalScope' in global_scope && + global_scope instanceof WorkerGlobalScope) { + return new DedicatedWorkerTestEnvironment(); + } + /* Shadow realm global objects are _ordinary_ objects (i.e. their prototype is + * Object) so we don't have a nice `instanceof` test to use; instead, we + * check if the there is a GLOBAL.isShadowRealm() property + * on the global object. that was set by the test harness when it + * created the ShadowRealm. + */ + if (global_scope.GLOBAL && global_scope.GLOBAL.isShadowRealm()) { + return new ShadowRealmTestEnvironment(); + } + + return new ShellTestEnvironment(); + } + + var test_environment = create_test_environment(); + + function is_shared_worker(worker) { + return 'SharedWorker' in global_scope && worker instanceof SharedWorker; + } + + function is_service_worker(worker) { + // The worker object may be from another execution context, + // so do not use instanceof here. + return 'ServiceWorker' in global_scope && + Object.prototype.toString.call(worker) == '[object ServiceWorker]'; + } + + var seen_func_name = Object.create(null); + + function get_test_name(func, name) + { + if (name) { + return name; + } + + if (func) { + var func_code = func.toString(); + + // Try and match with brackets, but fallback to matching without + var arrow = func_code.match(/^\(\)\s*=>\s*(?:{(.*)}\s*|(.*))$/); + + // Check for JS line separators + if (arrow !== null && !/[\u000A\u000D\u2028\u2029]/.test(func_code)) { + var trimmed = (arrow[1] !== undefined ? arrow[1] : arrow[2]).trim(); + // drop trailing ; if there's no earlier ones + trimmed = trimmed.replace(/^([^;]*)(;\s*)+$/, "$1"); + + if (trimmed) { + let name = trimmed; + if (seen_func_name[trimmed]) { + // This subtest name already exists, so add a suffix. + name += " " + seen_func_name[trimmed]; + } else { + seen_func_name[trimmed] = 0; + } + seen_func_name[trimmed] += 1; + return name; + } + } + } + + return test_environment.next_default_test_name(); + } + + /** + * @callback TestFunction + * @param {Test} test - The test currnetly being run. + * @param {Any[]} args - Additional args to pass to function. + * + */ + + /** + * Create a synchronous test + * + * @param {TestFunction} func - Test function. This is executed + * immediately. If it returns without error, the test status is + * set to ``PASS``. If it throws an :js:class:`AssertionError`, or + * any other exception, the test status is set to ``FAIL`` + * (typically from an `assert` function). + * @param {String} name - Test name. This must be unique in a + * given file and must be invariant between runs. + */ + function test(func, name, properties) + { + if (tests.promise_setup_called) { + tests.status.status = tests.status.ERROR; + tests.status.message = '`test` invoked after `promise_setup`'; + tests.complete(); + } + var test_name = get_test_name(func, name); + var test_obj = new Test(test_name, properties); + var value = test_obj.step(func, test_obj, test_obj); + + if (value !== undefined) { + var msg = 'Test named "' + test_name + + '" passed a function to `test` that returned a value.'; + + try { + if (value && typeof value.then === 'function') { + msg += ' Consider using `promise_test` instead when ' + + 'using Promises or async/await.'; + } + } catch (err) {} + + tests.status.status = tests.status.ERROR; + tests.status.message = msg; + } + + if (test_obj.phase === test_obj.phases.STARTED) { + test_obj.done(); + } + } + + /** + * Create an asynchronous test + * + * @param {TestFunction|string} funcOrName - Initial step function + * to call immediately with the test name as an argument (if any), + * or name of the test. + * @param {String} name - Test name (if a test function was + * provided). This must be unique in a given file and must be + * invariant between runs. + * @returns {Test} An object representing the ongoing test. + */ + function async_test(func, name, properties) + { + if (tests.promise_setup_called) { + tests.status.status = tests.status.ERROR; + tests.status.message = '`async_test` invoked after `promise_setup`'; + tests.complete(); + } + if (typeof func !== "function") { + properties = name; + name = func; + func = null; + } + var test_name = get_test_name(func, name); + var test_obj = new Test(test_name, properties); + if (func) { + var value = test_obj.step(func, test_obj, test_obj); + + // Test authors sometimes return values to async_test, expecting us + // to handle the value somehow. Make doing so a harness error to be + // clear this is invalid, and point authors to promise_test if it + // may be appropriate. + // + // Note that we only perform this check on the initial function + // passed to async_test, not on any later steps - we haven't seen a + // consistent problem with those (and it's harder to check). + if (value !== undefined) { + var msg = 'Test named "' + test_name + + '" passed a function to `async_test` that returned a value.'; + + try { + if (value && typeof value.then === 'function') { + msg += ' Consider using `promise_test` instead when ' + + 'using Promises or async/await.'; + } + } catch (err) {} + + tests.set_status(tests.status.ERROR, msg); + tests.complete(); + } + } + return test_obj; + } + + /** + * Create a promise test. + * + * Promise tests are tests which are represented by a promise + * object. If the promise is fulfilled the test passes, if it's + * rejected the test fails, otherwise the test passes. + * + * @param {TestFunction} func - Test function. This must return a + * promise. The test is automatically marked as complete once the + * promise settles. + * @param {String} name - Test name. This must be unique in a + * given file and must be invariant between runs. + */ + function promise_test(func, name, properties) { + if (typeof func !== "function") { + properties = name; + name = func; + func = null; + } + var test_name = get_test_name(func, name); + var test = new Test(test_name, properties); + test._is_promise_test = true; + + // If there is no promise tests queue make one. + if (!tests.promise_tests) { + tests.promise_tests = Promise.resolve(); + } + tests.promise_tests = tests.promise_tests.then(function() { + return new Promise(function(resolve) { + var promise = test.step(func, test, test); + + test.step(function() { + assert(!!promise, "promise_test", null, + "test body must return a 'thenable' object (received ${value})", + {value:promise}); + assert(typeof promise.then === "function", "promise_test", null, + "test body must return a 'thenable' object (received an object with no `then` method)", + null); + }); + + // Test authors may use the `step` method within a + // `promise_test` even though this reflects a mixture of + // asynchronous control flow paradigms. The "done" callback + // should be registered prior to the resolution of the + // user-provided Promise to avoid timeouts in cases where the + // Promise does not settle but a `step` function has thrown an + // error. + add_test_done_callback(test, resolve); + + Promise.resolve(promise) + .catch(test.step_func( + function(value) { + if (value instanceof AssertionError) { + throw value; + } + assert(false, "promise_test", null, + "Unhandled rejection with value: ${value}", {value:value}); + })) + .then(function() { + test.done(); + }); + }); + }); + } + + /** + * Make a copy of a Promise in the current realm. + * + * @param {Promise} promise the given promise that may be from a different + * realm + * @returns {Promise} + * + * An arbitrary promise provided by the caller may have originated + * in another frame that have since navigated away, rendering the + * frame's document inactive. Such a promise cannot be used with + * `await` or Promise.resolve(), as microtasks associated with it + * may be prevented from being run. See `issue + * 5319<https://github.com/whatwg/html/issues/5319>`_ for a + * particular case. + * + * In functions we define here, there is an expectation from the caller + * that the promise is from the current realm, that can always be used with + * `await`, etc. We therefore create a new promise in this realm that + * inherit the value and status from the given promise. + */ + + function bring_promise_to_current_realm(promise) { + return new Promise(promise.then.bind(promise)); + } + + /** + * Assert that a Promise is rejected with the right ECMAScript exception. + * + * @param {Test} test - the `Test` to use for the assertion. + * @param {Function} constructor - The expected exception constructor. + * @param {Promise} promise - The promise that's expected to + * reject with the given exception. + * @param {string} [description] Error message to add to assert in case of + * failure. + */ + function promise_rejects_js(test, constructor, promise, description) { + return bring_promise_to_current_realm(promise) + .then(test.unreached_func("Should have rejected: " + description)) + .catch(function(e) { + assert_throws_js_impl(constructor, function() { throw e }, + description, "promise_rejects_js"); + }); + } + + /** + * Assert that a Promise is rejected with the right DOMException. + * + * For the remaining arguments, there are two ways of calling + * promise_rejects_dom: + * + * 1) If the DOMException is expected to come from the current global, the + * third argument should be the promise expected to reject, and a fourth, + * optional, argument is the assertion description. + * + * 2) If the DOMException is expected to come from some other global, the + * third argument should be the DOMException constructor from that global, + * the fourth argument the promise expected to reject, and the fifth, + * optional, argument the assertion description. + * + * @param {Test} test - the `Test` to use for the assertion. + * @param {number|string} type - See documentation for + * `assert_throws_dom <#assert_throws_dom>`_. + * @param {Function} promiseOrConstructor - Either the constructor + * for the expected exception (if the exception comes from another + * global), or the promise that's expected to reject (if the + * exception comes from the current global). + * @param {Function|string} descriptionOrPromise - Either the + * promise that's expected to reject (if the exception comes from + * another global), or the optional description of the condition + * being tested (if the exception comes from the current global). + * @param {string} [description] - Description of the condition + * being tested (if the exception comes from another global). + * + */ + function promise_rejects_dom(test, type, promiseOrConstructor, descriptionOrPromise, maybeDescription) { + let constructor, promise, description; + if (typeof promiseOrConstructor === "function" && + promiseOrConstructor.name === "DOMException") { + constructor = promiseOrConstructor; + promise = descriptionOrPromise; + description = maybeDescription; + } else { + constructor = self.DOMException; + promise = promiseOrConstructor; + description = descriptionOrPromise; + assert(maybeDescription === undefined, + "Too many args pased to no-constructor version of promise_rejects_dom"); + } + return bring_promise_to_current_realm(promise) + .then(test.unreached_func("Should have rejected: " + description)) + .catch(function(e) { + assert_throws_dom_impl(type, function() { throw e }, description, + "promise_rejects_dom", constructor); + }); + } + + /** + * Assert that a Promise is rejected with the provided value. + * + * @param {Test} test - the `Test` to use for the assertion. + * @param {Any} exception - The expected value of the rejected promise. + * @param {Promise} promise - The promise that's expected to + * reject. + * @param {string} [description] Error message to add to assert in case of + * failure. + */ + function promise_rejects_exactly(test, exception, promise, description) { + return bring_promise_to_current_realm(promise) + .then(test.unreached_func("Should have rejected: " + description)) + .catch(function(e) { + assert_throws_exactly_impl(exception, function() { throw e }, + description, "promise_rejects_exactly"); + }); + } + + /** + * Allow DOM events to be handled using Promises. + * + * This can make it a lot easier to test a very specific series of events, + * including ensuring that unexpected events are not fired at any point. + * + * `EventWatcher` will assert if an event occurs while there is no `wait_for` + * created Promise waiting to be fulfilled, or if the event is of a different type + * to the type currently expected. This ensures that only the events that are + * expected occur, in the correct order, and with the correct timing. + * + * @constructor + * @param {Test} test - The `Test` to use for the assertion. + * @param {EventTarget} watchedNode - The target expected to receive the events. + * @param {string[]} eventTypes - List of events to watch for. + * @param {Promise} timeoutPromise - Promise that will cause the + * test to be set to `TIMEOUT` once fulfilled. + * + */ + function EventWatcher(test, watchedNode, eventTypes, timeoutPromise) + { + if (typeof eventTypes == 'string') { + eventTypes = [eventTypes]; + } + + var waitingFor = null; + + // This is null unless we are recording all events, in which case it + // will be an Array object. + var recordedEvents = null; + + var eventHandler = test.step_func(function(evt) { + assert_true(!!waitingFor, + 'Not expecting event, but got ' + evt.type + ' event'); + assert_equals(evt.type, waitingFor.types[0], + 'Expected ' + waitingFor.types[0] + ' event, but got ' + + evt.type + ' event instead'); + + if (Array.isArray(recordedEvents)) { + recordedEvents.push(evt); + } + + if (waitingFor.types.length > 1) { + // Pop first event from array + waitingFor.types.shift(); + return; + } + // We need to null out waitingFor before calling the resolve function + // since the Promise's resolve handlers may call wait_for() which will + // need to set waitingFor. + var resolveFunc = waitingFor.resolve; + waitingFor = null; + // Likewise, we should reset the state of recordedEvents. + var result = recordedEvents || evt; + recordedEvents = null; + resolveFunc(result); + }); + + for (var i = 0; i < eventTypes.length; i++) { + watchedNode.addEventListener(eventTypes[i], eventHandler, false); + } + + /** + * Returns a Promise that will resolve after the specified event or + * series of events has occurred. + * + * @param {Object} options An optional options object. If the 'record' property + * on this object has the value 'all', when the Promise + * returned by this function is resolved, *all* Event + * objects that were waited for will be returned as an + * array. + * + * @example + * const watcher = new EventWatcher(t, div, [ 'animationstart', + * 'animationiteration', + * 'animationend' ]); + * return watcher.wait_for([ 'animationstart', 'animationend' ], + * { record: 'all' }).then(evts => { + * assert_equals(evts[0].elapsedTime, 0.0); + * assert_equals(evts[1].elapsedTime, 2.0); + * }); + */ + this.wait_for = function(types, options) { + if (waitingFor) { + return Promise.reject('Already waiting for an event or events'); + } + if (typeof types == 'string') { + types = [types]; + } + if (options && options.record && options.record === 'all') { + recordedEvents = []; + } + return new Promise(function(resolve, reject) { + var timeout = test.step_func(function() { + // If the timeout fires after the events have been received + // or during a subsequent call to wait_for, ignore it. + if (!waitingFor || waitingFor.resolve !== resolve) + return; + + // This should always fail, otherwise we should have + // resolved the promise. + assert_true(waitingFor.types.length == 0, + 'Timed out waiting for ' + waitingFor.types.join(', ')); + var result = recordedEvents; + recordedEvents = null; + var resolveFunc = waitingFor.resolve; + waitingFor = null; + resolveFunc(result); + }); + + if (timeoutPromise) { + timeoutPromise().then(timeout); + } + + waitingFor = { + types: types, + resolve: resolve, + reject: reject + }; + }); + }; + + /** + * Stop listening for events + */ + function stop_watching() { + for (var i = 0; i < eventTypes.length; i++) { + watchedNode.removeEventListener(eventTypes[i], eventHandler, false); + } + }; + + test._add_cleanup(stop_watching); + + return this; + } + expose(EventWatcher, 'EventWatcher'); + + /** + * @typedef {Object} SettingsObject + * @property {bool} single_test - Use the single-page-test + * mode. In this mode the Document represents a single + * `async_test`. Asserts may be used directly without requiring + * `Test.step` or similar wrappers, and any exceptions set the + * status of the test rather than the status of the harness. + * @property {bool} allow_uncaught_exception - don't treat an + * uncaught exception as an error; needed when e.g. testing the + * `window.onerror` handler. + * @property {boolean} explicit_done - Wait for a call to `done()` + * before declaring all tests complete (this is always true for + * single-page tests). + * @property hide_test_state - hide the test state output while + * the test is running; This is helpful when the output of the test state + * may interfere the test results. + * @property {bool} explicit_timeout - disable file timeout; only + * stop waiting for results when the `timeout()` function is + * called This should typically only be set for manual tests, or + * by a test runner that providees its own timeout mechanism. + * @property {number} timeout_multiplier - Multiplier to apply to + * per-test timeouts. This should only be set by a test runner. + * @property {Document} output_document - The document to which + * results should be logged. By default this is the current + * document but could be an ancestor document in some cases e.g. a + * SVG test loaded in an HTML wrapper + * + */ + + /** + * Configure the harness + * + * @param {Function|SettingsObject} funcOrProperties - Either a + * setup function to run, or a set of properties. If this is a + * function that function is run synchronously. Any exception in + * the function will set the overall harness status to `ERROR`. + * @param {SettingsObject} maybeProperties - An object containing + * the settings to use, if the first argument is a function. + * + */ + function setup(func_or_properties, maybe_properties) + { + var func = null; + var properties = {}; + if (arguments.length === 2) { + func = func_or_properties; + properties = maybe_properties; + } else if (func_or_properties instanceof Function) { + func = func_or_properties; + } else { + properties = func_or_properties; + } + tests.setup(func, properties); + test_environment.on_new_harness_properties(properties); + } + + /** + * Configure the harness, waiting for a promise to resolve + * before running any `promise_test` tests. + * + * @param {Function} func - Function returning a promise that's + * run synchronously. Promise tests are not run until after this + * function has resolved. + * @param {SettingsObject} [properties] - An object containing + * the harness settings to use. + * + */ + function promise_setup(func, properties={}) + { + if (typeof func !== "function") { + tests.set_status(tests.status.ERROR, + "promise_test invoked without a function"); + tests.complete(); + return; + } + tests.promise_setup_called = true; + + if (!tests.promise_tests) { + tests.promise_tests = Promise.resolve(); + } + + tests.promise_tests = tests.promise_tests + .then(function() + { + var result; + + tests.setup(null, properties); + result = func(); + test_environment.on_new_harness_properties(properties); + + if (!result || typeof result.then !== "function") { + throw "Non-thenable returned by function passed to `promise_setup`"; + } + return result; + }) + .catch(function(e) + { + tests.set_status(tests.status.ERROR, + String(e), + e && e.stack); + tests.complete(); + }); + } + + /** + * Mark test loading as complete. + * + * Typically this function is called implicitly on page load; it's + * only necessary for users to call this when either the + * ``explicit_done`` or ``single_page`` properties have been set + * via the :js:func:`setup` function. + * + * For single page tests this marks the test as complete and sets its status. + * For other tests, this marks test loading as complete, but doesn't affect ongoing tests. + */ + function done() { + if (tests.tests.length === 0) { + // `done` is invoked after handling uncaught exceptions, so if the + // harness status is already set, the corresponding message is more + // descriptive than the generic message defined here. + if (tests.status.status === null) { + tests.status.status = tests.status.ERROR; + tests.status.message = "done() was called without first defining any tests"; + } + + tests.complete(); + return; + } + if (tests.file_is_test) { + // file is test files never have asynchronous cleanup logic, + // meaning the fully-synchronous `done` function can be used here. + tests.tests[0].done(); + } + tests.end_wait(); + } + + /** + * @deprecated generate a list of tests from a function and list of arguments + * + * This is deprecated because it runs all the tests outside of the test functions + * and as a result any test throwing an exception will result in no tests being + * run. In almost all cases, you should simply call test within the loop you would + * use to generate the parameter list array. + * + * @param {Function} func - The function that will be called for each generated tests. + * @param {Any[][]} args - An array of arrays. Each nested array + * has the structure `[testName, ...testArgs]`. For each of these nested arrays + * array, a test is generated with name `testName` and test function equivalent to + * `func(..testArgs)`. + */ + function generate_tests(func, args, properties) { + forEach(args, function(x, i) + { + var name = x[0]; + test(function() + { + func.apply(this, x.slice(1)); + }, + name, + Array.isArray(properties) ? properties[i] : properties); + }); + } + + /** + * @deprecated + * + * Register a function as a DOM event listener to the + * given object for the event bubbling phase. + * + * @param {EventTarget} object - Event target + * @param {string} event - Event name + * @param {Function} callback - Event handler. + */ + function on_event(object, event, callback) + { + object.addEventListener(event, callback, false); + } + + // Internal helper function to provide timeout-like functionality in + // environments where there is no setTimeout(). (No timeout ID or + // clearTimeout().) + function fake_set_timeout(callback, delay) { + var p = Promise.resolve(); + var start = Date.now(); + var end = start + delay; + function check() { + if ((end - Date.now()) > 0) { + p.then(check); + } else { + callback(); + } + } + p.then(check); + } + + /** + * Global version of :js:func:`Test.step_timeout` for use in single page tests. + * + * @param {Function} func - Function to run after the timeout + * @param {number} timeout - Time in ms to wait before running the + * test step. The actual wait time is ``timeout`` x + * ``timeout_multiplier``. + */ + function step_timeout(func, timeout) { + var outer_this = this; + var args = Array.prototype.slice.call(arguments, 2); + var local_set_timeout = typeof global_scope.setTimeout === "undefined" ? fake_set_timeout : setTimeout; + return local_set_timeout(function() { + func.apply(outer_this, args); + }, timeout * tests.timeout_multiplier); + } + + expose(test, 'test'); + expose(async_test, 'async_test'); + expose(promise_test, 'promise_test'); + expose(promise_rejects_js, 'promise_rejects_js'); + expose(promise_rejects_dom, 'promise_rejects_dom'); + expose(promise_rejects_exactly, 'promise_rejects_exactly'); + expose(generate_tests, 'generate_tests'); + expose(setup, 'setup'); + expose(promise_setup, 'promise_setup'); + expose(done, 'done'); + expose(on_event, 'on_event'); + expose(step_timeout, 'step_timeout'); + + /* + * Return a string truncated to the given length, with ... added at the end + * if it was longer. + */ + function truncate(s, len) + { + if (s.length > len) { + return s.substring(0, len - 3) + "..."; + } + return s; + } + + /* + * Return true if object is probably a Node object. + */ + function is_node(object) + { + // I use duck-typing instead of instanceof, because + // instanceof doesn't work if the node is from another window (like an + // iframe's contentWindow): + // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 + try { + var has_node_properties = ("nodeType" in object && + "nodeName" in object && + "nodeValue" in object && + "childNodes" in object); + } catch (e) { + // We're probably cross-origin, which means we aren't a node + return false; + } + + if (has_node_properties) { + try { + object.nodeType; + } catch (e) { + // The object is probably Node.prototype or another prototype + // object that inherits from it, and not a Node instance. + return false; + } + return true; + } + return false; + } + + var replacements = { + "0": "0", + "1": "x01", + "2": "x02", + "3": "x03", + "4": "x04", + "5": "x05", + "6": "x06", + "7": "x07", + "8": "b", + "9": "t", + "10": "n", + "11": "v", + "12": "f", + "13": "r", + "14": "x0e", + "15": "x0f", + "16": "x10", + "17": "x11", + "18": "x12", + "19": "x13", + "20": "x14", + "21": "x15", + "22": "x16", + "23": "x17", + "24": "x18", + "25": "x19", + "26": "x1a", + "27": "x1b", + "28": "x1c", + "29": "x1d", + "30": "x1e", + "31": "x1f", + "0xfffd": "ufffd", + "0xfffe": "ufffe", + "0xffff": "uffff", + }; + + /** + * Convert a value to a nice, human-readable string + * + * When many JavaScript Object values are coerced to a String, the + * resulting value will be ``"[object Object]"``. This obscures + * helpful information, making the coerced value unsuitable for + * use in assertion messages, test names, and debugging + * statements. `format_value` produces more distinctive string + * representations of many kinds of objects, including arrays and + * the more important DOM Node types. It also translates String + * values containing control characters to include human-readable + * representations. + * + * @example + * // "Document node with 2 children" + * format_value(document); + * @example + * // "\"foo\\uffffbar\"" + * format_value("foo\uffffbar"); + * @example + * // "[-0, Infinity]" + * format_value([-0, Infinity]); + * @param {Any} val - The value to convert to a string. + * @returns {string} - A string representation of ``val``, optimised for human readability. + */ + function format_value(val, seen) + { + if (!seen) { + seen = []; + } + if (typeof val === "object" && val !== null) { + if (seen.indexOf(val) >= 0) { + return "[...]"; + } + seen.push(val); + } + if (Array.isArray(val)) { + let output = "["; + if (val.beginEllipsis !== undefined) { + output += "…, "; + } + output += val.map(function(x) {return format_value(x, seen);}).join(", "); + if (val.endEllipsis !== undefined) { + output += ", …"; + } + return output + "]"; + } + + switch (typeof val) { + case "string": + val = val.replace(/\\/g, "\\\\"); + for (var p in replacements) { + var replace = "\\" + replacements[p]; + val = val.replace(RegExp(String.fromCharCode(p), "g"), replace); + } + return '"' + val.replace(/"/g, '\\"') + '"'; + case "boolean": + case "undefined": + return String(val); + case "number": + // In JavaScript, -0 === 0 and String(-0) == "0", so we have to + // special-case. + if (val === -0 && 1/val === -Infinity) { + return "-0"; + } + return String(val); + case "object": + if (val === null) { + return "null"; + } + + // Special-case Node objects, since those come up a lot in my tests. I + // ignore namespaces. + if (is_node(val)) { + switch (val.nodeType) { + case Node.ELEMENT_NODE: + var ret = "<" + val.localName; + for (var i = 0; i < val.attributes.length; i++) { + ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"'; + } + ret += ">" + val.innerHTML + "</" + val.localName + ">"; + return "Element node " + truncate(ret, 60); + case Node.TEXT_NODE: + return 'Text node "' + truncate(val.data, 60) + '"'; + case Node.PROCESSING_INSTRUCTION_NODE: + return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60)); + case Node.COMMENT_NODE: + return "Comment node <!--" + truncate(val.data, 60) + "-->"; + case Node.DOCUMENT_NODE: + return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); + case Node.DOCUMENT_TYPE_NODE: + return "DocumentType node"; + case Node.DOCUMENT_FRAGMENT_NODE: + return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); + default: + return "Node object of unknown type"; + } + } + + /* falls through */ + default: + try { + return typeof val + ' "' + truncate(String(val), 1000) + '"'; + } catch(e) { + return ("[stringifying object threw " + String(e) + + " with type " + String(typeof e) + "]"); + } + } + } + expose(format_value, "format_value"); + + /* + * Assertions + */ + + function expose_assert(f, name) { + function assert_wrapper(...args) { + let status = Test.statuses.TIMEOUT; + let stack = null; + let new_assert_index = null; + try { + if (settings.debug) { + console.debug("ASSERT", name, tests.current_test && tests.current_test.name, args); + } + if (tests.output) { + tests.set_assert(name, args); + // Remember the newly pushed assert's index, because `apply` + // below might push new asserts. + new_assert_index = tests.asserts_run.length - 1; + } + const rv = f.apply(undefined, args); + status = Test.statuses.PASS; + return rv; + } catch(e) { + status = Test.statuses.FAIL; + stack = e.stack ? e.stack : null; + throw e; + } finally { + if (tests.output && !stack) { + stack = get_stack(); + } + if (tests.output) { + tests.set_assert_status(new_assert_index, status, stack); + } + } + } + expose(assert_wrapper, name); + } + + /** + * Assert that ``actual`` is strictly true + * + * @param {Any} actual - Value that is asserted to be true + * @param {string} [description] - Description of the condition being tested + */ + function assert_true(actual, description) + { + assert(actual === true, "assert_true", description, + "expected true got ${actual}", {actual:actual}); + } + expose_assert(assert_true, "assert_true"); + + /** + * Assert that ``actual`` is strictly false + * + * @param {Any} actual - Value that is asserted to be false + * @param {string} [description] - Description of the condition being tested + */ + function assert_false(actual, description) + { + assert(actual === false, "assert_false", description, + "expected false got ${actual}", {actual:actual}); + } + expose_assert(assert_false, "assert_false"); + + function same_value(x, y) { + if (y !== y) { + //NaN case + return x !== x; + } + if (x === 0 && y === 0) { + //Distinguish +0 and -0 + return 1/x === 1/y; + } + return x === y; + } + + /** + * Assert that ``actual`` is the same value as ``expected``. + * + * For objects this compares by object identity; for primitives + * this distinguishes between 0 and -0, and has correct handling + * of NaN. + * + * @param {Any} actual - Test value. + * @param {Any} expected - Expected value. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_equals(actual, expected, description) + { + /* + * Test if two primitives are equal or two objects + * are the same object + */ + if (typeof actual != typeof expected) { + assert(false, "assert_equals", description, + "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}", + {expected:expected, actual:actual}); + return; + } + assert(same_value(actual, expected), "assert_equals", description, + "expected ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose_assert(assert_equals, "assert_equals"); + + /** + * Assert that ``actual`` is not the same value as ``expected``. + * + * Comparison is as for :js:func:`assert_equals`. + * + * @param {Any} actual - Test value. + * @param {Any} expected - The value ``actual`` is expected to be different to. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_not_equals(actual, expected, description) + { + assert(!same_value(actual, expected), "assert_not_equals", description, + "got disallowed value ${actual}", + {actual:actual}); + } + expose_assert(assert_not_equals, "assert_not_equals"); + + /** + * Assert that ``expected`` is an array and ``actual`` is one of the members. + * This is implemented using ``indexOf``, so doesn't handle NaN or ±0 correctly. + * + * @param {Any} actual - Test value. + * @param {Array} expected - An array that ``actual`` is expected to + * be a member of. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_in_array(actual, expected, description) + { + assert(expected.indexOf(actual) != -1, "assert_in_array", description, + "value ${actual} not in array ${expected}", + {actual:actual, expected:expected}); + } + expose_assert(assert_in_array, "assert_in_array"); + + // This function was deprecated in July of 2015. + // See https://github.com/web-platform-tests/wpt/issues/2033 + /** + * @deprecated + * Recursively compare two objects for equality. + * + * See `Issue 2033 + * <https://github.com/web-platform-tests/wpt/issues/2033>`_ for + * more information. + * + * @param {Object} actual - Test value. + * @param {Object} expected - Expected value. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_object_equals(actual, expected, description) + { + assert(typeof actual === "object" && actual !== null, "assert_object_equals", description, + "value is ${actual}, expected object", + {actual: actual}); + //This needs to be improved a great deal + function check_equal(actual, expected, stack) + { + stack.push(actual); + + var p; + for (p in actual) { + assert(expected.hasOwnProperty(p), "assert_object_equals", description, + "unexpected property ${p}", {p:p}); + + if (typeof actual[p] === "object" && actual[p] !== null) { + if (stack.indexOf(actual[p]) === -1) { + check_equal(actual[p], expected[p], stack); + } + } else { + assert(same_value(actual[p], expected[p]), "assert_object_equals", description, + "property ${p} expected ${expected} got ${actual}", + {p:p, expected:expected[p], actual:actual[p]}); + } + } + for (p in expected) { + assert(actual.hasOwnProperty(p), + "assert_object_equals", description, + "expected property ${p} missing", {p:p}); + } + stack.pop(); + } + check_equal(actual, expected, []); + } + expose_assert(assert_object_equals, "assert_object_equals"); + + /** + * Assert that ``actual`` and ``expected`` are both arrays, and that the array properties of + * ``actual`` and ``expected`` are all the same value (as for :js:func:`assert_equals`). + * + * @param {Array} actual - Test array. + * @param {Array} expected - Array that is expected to contain the same values as ``actual``. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_array_equals(actual, expected, description) + { + const max_array_length = 20; + function shorten_array(arr, offset = 0) { + // Make ", …" only show up when it would likely reduce the length, not accounting for + // fonts. + if (arr.length < max_array_length + 2) { + return arr; + } + // By default we want half the elements after the offset and half before + // But if that takes us past the end of the array, we have more before, and + // if it takes us before the start we have more after. + const length_after_offset = Math.floor(max_array_length / 2); + let upper_bound = Math.min(length_after_offset + offset, arr.length); + const lower_bound = Math.max(upper_bound - max_array_length, 0); + + if (lower_bound === 0) { + upper_bound = max_array_length; + } + + const output = arr.slice(lower_bound, upper_bound); + if (lower_bound > 0) { + output.beginEllipsis = true; + } + if (upper_bound < arr.length) { + output.endEllipsis = true; + } + return output; + } + + assert(typeof actual === "object" && actual !== null && "length" in actual, + "assert_array_equals", description, + "value is ${actual}, expected array", + {actual:actual}); + assert(actual.length === expected.length, + "assert_array_equals", description, + "lengths differ, expected array ${expected} length ${expectedLength}, got ${actual} length ${actualLength}", + {expected:shorten_array(expected, expected.length - 1), expectedLength:expected.length, + actual:shorten_array(actual, actual.length - 1), actualLength:actual.length + }); + + for (var i = 0; i < actual.length; i++) { + assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), + "assert_array_equals", description, + "expected property ${i} to be ${expected} but was ${actual} (expected array ${arrayExpected} got ${arrayActual})", + {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", + actual:actual.hasOwnProperty(i) ? "present" : "missing", + arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)}); + assert(same_value(expected[i], actual[i]), + "assert_array_equals", description, + "expected property ${i} to be ${expected} but got ${actual} (expected array ${arrayExpected} got ${arrayActual})", + {i:i, expected:expected[i], actual:actual[i], + arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)}); + } + } + expose_assert(assert_array_equals, "assert_array_equals"); + + /** + * Assert that each array property in ``actual`` is a number within + * ± `epsilon` of the corresponding property in `expected`. + * + * @param {Array} actual - Array of test values. + * @param {Array} expected - Array of values expected to be close to the values in ``actual``. + * @param {number} epsilon - Magnitude of allowed difference + * between each value in ``actual`` and ``expected``. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_array_approx_equals(actual, expected, epsilon, description) + { + /* + * Test if two primitive arrays are equal within +/- epsilon + */ + assert(actual.length === expected.length, + "assert_array_approx_equals", description, + "lengths differ, expected ${expected} got ${actual}", + {expected:expected.length, actual:actual.length}); + + for (var i = 0; i < actual.length; i++) { + assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), + "assert_array_approx_equals", description, + "property ${i}, property expected to be ${expected} but was ${actual}", + {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", + actual:actual.hasOwnProperty(i) ? "present" : "missing"}); + assert(typeof actual[i] === "number", + "assert_array_approx_equals", description, + "property ${i}, expected a number but got a ${type_actual}", + {i:i, type_actual:typeof actual[i]}); + assert(Math.abs(actual[i] - expected[i]) <= epsilon, + "assert_array_approx_equals", description, + "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}", + {i:i, expected:expected[i], actual:actual[i], epsilon:epsilon}); + } + } + expose_assert(assert_array_approx_equals, "assert_array_approx_equals"); + + /** + * Assert that ``actual`` is within ± ``epsilon`` of ``expected``. + * + * @param {number} actual - Test value. + * @param {number} expected - Value number is expected to be close to. + * @param {number} epsilon - Magnitude of allowed difference between ``actual`` and ``expected``. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_approx_equals(actual, expected, epsilon, description) + { + /* + * Test if two primitive numbers are equal within +/- epsilon + */ + assert(typeof actual === "number", + "assert_approx_equals", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + // The epsilon math below does not place nice with NaN and Infinity + // But in this case Infinity = Infinity and NaN = NaN + if (isFinite(actual) || isFinite(expected)) { + assert(Math.abs(actual - expected) <= epsilon, + "assert_approx_equals", description, + "expected ${expected} +/- ${epsilon} but got ${actual}", + {expected:expected, actual:actual, epsilon:epsilon}); + } else { + assert_equals(actual, expected); + } + } + expose_assert(assert_approx_equals, "assert_approx_equals"); + + /** + * Assert that ``actual`` is a number less than ``expected``. + * + * @param {number} actual - Test value. + * @param {number} expected - Number that ``actual`` must be less than. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_less_than(actual, expected, description) + { + /* + * Test if a primitive number is less than another + */ + assert(typeof actual === "number", + "assert_less_than", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual < expected, + "assert_less_than", description, + "expected a number less than ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose_assert(assert_less_than, "assert_less_than"); + + /** + * Assert that ``actual`` is a number greater than ``expected``. + * + * @param {number} actual - Test value. + * @param {number} expected - Number that ``actual`` must be greater than. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_greater_than(actual, expected, description) + { + /* + * Test if a primitive number is greater than another + */ + assert(typeof actual === "number", + "assert_greater_than", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual > expected, + "assert_greater_than", description, + "expected a number greater than ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose_assert(assert_greater_than, "assert_greater_than"); + + /** + * Assert that ``actual`` is a number greater than ``lower`` and less + * than ``upper`` but not equal to either. + * + * @param {number} actual - Test value. + * @param {number} lower - Number that ``actual`` must be greater than. + * @param {number} upper - Number that ``actual`` must be less than. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_between_exclusive(actual, lower, upper, description) + { + /* + * Test if a primitive number is between two others + */ + assert(typeof actual === "number", + "assert_between_exclusive", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual > lower && actual < upper, + "assert_between_exclusive", description, + "expected a number greater than ${lower} " + + "and less than ${upper} but got ${actual}", + {lower:lower, upper:upper, actual:actual}); + } + expose_assert(assert_between_exclusive, "assert_between_exclusive"); + + /** + * Assert that ``actual`` is a number less than or equal to ``expected``. + * + * @param {number} actual - Test value. + * @param {number} expected - Number that ``actual`` must be less + * than or equal to. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_less_than_equal(actual, expected, description) + { + /* + * Test if a primitive number is less than or equal to another + */ + assert(typeof actual === "number", + "assert_less_than_equal", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual <= expected, + "assert_less_than_equal", description, + "expected a number less than or equal to ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose_assert(assert_less_than_equal, "assert_less_than_equal"); + + /** + * Assert that ``actual`` is a number greater than or equal to ``expected``. + * + * @param {number} actual - Test value. + * @param {number} expected - Number that ``actual`` must be greater + * than or equal to. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_greater_than_equal(actual, expected, description) + { + /* + * Test if a primitive number is greater than or equal to another + */ + assert(typeof actual === "number", + "assert_greater_than_equal", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual >= expected, + "assert_greater_than_equal", description, + "expected a number greater than or equal to ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose_assert(assert_greater_than_equal, "assert_greater_than_equal"); + + /** + * Assert that ``actual`` is a number greater than or equal to ``lower`` and less + * than or equal to ``upper``. + * + * @param {number} actual - Test value. + * @param {number} lower - Number that ``actual`` must be greater than or equal to. + * @param {number} upper - Number that ``actual`` must be less than or equal to. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_between_inclusive(actual, lower, upper, description) + { + /* + * Test if a primitive number is between to two others or equal to either of them + */ + assert(typeof actual === "number", + "assert_between_inclusive", description, + "expected a number but got a ${type_actual}", + {type_actual:typeof actual}); + + assert(actual >= lower && actual <= upper, + "assert_between_inclusive", description, + "expected a number greater than or equal to ${lower} " + + "and less than or equal to ${upper} but got ${actual}", + {lower:lower, upper:upper, actual:actual}); + } + expose_assert(assert_between_inclusive, "assert_between_inclusive"); + + /** + * Assert that ``actual`` matches the RegExp ``expected``. + * + * @param {String} actual - Test string. + * @param {RegExp} expected - RegExp ``actual`` must match. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_regexp_match(actual, expected, description) { + /* + * Test if a string (actual) matches a regexp (expected) + */ + assert(expected.test(actual), + "assert_regexp_match", description, + "expected ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose_assert(assert_regexp_match, "assert_regexp_match"); + + /** + * Assert that the class string of ``object`` as returned in + * ``Object.prototype.toString`` is equal to ``class_name``. + * + * @param {Object} object - Object to stringify. + * @param {string} class_string - Expected class string for ``object``. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_class_string(object, class_string, description) { + var actual = {}.toString.call(object); + var expected = "[object " + class_string + "]"; + assert(same_value(actual, expected), "assert_class_string", description, + "expected ${expected} but got ${actual}", + {expected:expected, actual:actual}); + } + expose_assert(assert_class_string, "assert_class_string"); + + /** + * Assert that ``object`` has an own property with name ``property_name``. + * + * @param {Object} object - Object that should have the given property. + * @param {string} property_name - Expected property name. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_own_property(object, property_name, description) { + assert(object.hasOwnProperty(property_name), + "assert_own_property", description, + "expected property ${p} missing", {p:property_name}); + } + expose_assert(assert_own_property, "assert_own_property"); + + /** + * Assert that ``object`` does not have an own property with name ``property_name``. + * + * @param {Object} object - Object that should not have the given property. + * @param {string} property_name - Property name to test. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_not_own_property(object, property_name, description) { + assert(!object.hasOwnProperty(property_name), + "assert_not_own_property", description, + "unexpected property ${p} is found on object", {p:property_name}); + } + expose_assert(assert_not_own_property, "assert_not_own_property"); + + function _assert_inherits(name) { + return function (object, property_name, description) + { + assert((typeof object === "object" && object !== null) || + typeof object === "function" || + // Or has [[IsHTMLDDA]] slot + String(object) === "[object HTMLAllCollection]", + name, description, + "provided value is not an object"); + + assert("hasOwnProperty" in object, + name, description, + "provided value is an object but has no hasOwnProperty method"); + + assert(!object.hasOwnProperty(property_name), + name, description, + "property ${p} found on object expected in prototype chain", + {p:property_name}); + + assert(property_name in object, + name, description, + "property ${p} not found in prototype chain", + {p:property_name}); + }; + } + + /** + * Assert that ``object`` does not have an own property with name + * ``property_name``, but inherits one through the prototype chain. + * + * @param {Object} object - Object that should have the given property in its prototype chain. + * @param {string} property_name - Expected property name. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_inherits(object, property_name, description) { + return _assert_inherits("assert_inherits")(object, property_name, description); + } + expose_assert(assert_inherits, "assert_inherits"); + + /** + * Alias for :js:func:`insert_inherits`. + * + * @param {Object} object - Object that should have the given property in its prototype chain. + * @param {string} property_name - Expected property name. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_idl_attribute(object, property_name, description) { + return _assert_inherits("assert_idl_attribute")(object, property_name, description); + } + expose_assert(assert_idl_attribute, "assert_idl_attribute"); + + + /** + * Assert that ``object`` has a property named ``property_name`` and that the property is readonly. + * + * Note: The implementation tries to update the named property, so + * any side effects of updating will be triggered. Users are + * encouraged to instead inspect the property descriptor of ``property_name`` on ``object``. + * + * @param {Object} object - Object that should have the given property in its prototype chain. + * @param {string} property_name - Expected property name. + * @param {string} [description] - Description of the condition being tested. + */ + function assert_readonly(object, property_name, description) + { + var initial_value = object[property_name]; + try { + //Note that this can have side effects in the case where + //the property has PutForwards + object[property_name] = initial_value + "a"; //XXX use some other value here? + assert(same_value(object[property_name], initial_value), + "assert_readonly", description, + "changing property ${p} succeeded", + {p:property_name}); + } finally { + object[property_name] = initial_value; + } + } + expose_assert(assert_readonly, "assert_readonly"); + + /** + * Assert a JS Error with the expected constructor is thrown. + * + * @param {object} constructor The expected exception constructor. + * @param {Function} func Function which should throw. + * @param {string} [description] Error description for the case that the error is not thrown. + */ + function assert_throws_js(constructor, func, description) + { + assert_throws_js_impl(constructor, func, description, + "assert_throws_js"); + } + expose_assert(assert_throws_js, "assert_throws_js"); + + /** + * Like assert_throws_js but allows specifying the assertion type + * (assert_throws_js or promise_rejects_js, in practice). + */ + function assert_throws_js_impl(constructor, func, description, + assertion_type) + { + try { + func.call(this); + assert(false, assertion_type, description, + "${func} did not throw", {func:func}); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + + // Basic sanity-checks on the thrown exception. + assert(typeof e === "object", + assertion_type, description, + "${func} threw ${e} with type ${type}, not an object", + {func:func, e:e, type:typeof e}); + + assert(e !== null, + assertion_type, description, + "${func} threw null, not an object", + {func:func}); + + // Basic sanity-check on the passed-in constructor + assert(typeof constructor == "function", + assertion_type, description, + "${constructor} is not a constructor", + {constructor:constructor}); + var obj = constructor; + while (obj) { + if (typeof obj === "function" && + obj.name === "Error") { + break; + } + obj = Object.getPrototypeOf(obj); + } + assert(obj != null, + assertion_type, description, + "${constructor} is not an Error subtype", + {constructor:constructor}); + + // And checking that our exception is reasonable + assert(e.constructor === constructor && + e.name === constructor.name, + assertion_type, description, + "${func} threw ${actual} (${actual_name}) expected instance of ${expected} (${expected_name})", + {func:func, actual:e, actual_name:e.name, + expected:constructor, + expected_name:constructor.name}); + } + } + + // TODO: Figure out how to document the overloads better. + // sphinx-js doesn't seem to handle @variation correctly, + // and only expects a single JSDoc entry per function. + /** + * Assert a DOMException with the expected type is thrown. + * + * There are two ways of calling assert_throws_dom: + * + * 1) If the DOMException is expected to come from the current global, the + * second argument should be the function expected to throw and a third, + * optional, argument is the assertion description. + * + * 2) If the DOMException is expected to come from some other global, the + * second argument should be the DOMException constructor from that global, + * the third argument the function expected to throw, and the fourth, optional, + * argument the assertion description. + * + * @param {number|string} type - The expected exception name or + * code. See the `table of names and codes + * <https://webidl.spec.whatwg.org/#dfn-error-names-table>`_. If a + * number is passed it should be one of the numeric code values in + * that table (e.g. 3, 4, etc). If a string is passed it can + * either be an exception name (e.g. "HierarchyRequestError", + * "WrongDocumentError") or the name of the corresponding error + * code (e.g. "``HIERARCHY_REQUEST_ERR``", "``WRONG_DOCUMENT_ERR``"). + * @param {Function} descriptionOrFunc - The function expected to + * throw (if the exception comes from another global), or the + * optional description of the condition being tested (if the + * exception comes from the current global). + * @param {string} [description] - Description of the condition + * being tested (if the exception comes from another global). + * + */ + function assert_throws_dom(type, funcOrConstructor, descriptionOrFunc, maybeDescription) + { + let constructor, func, description; + if (funcOrConstructor.name === "DOMException") { + constructor = funcOrConstructor; + func = descriptionOrFunc; + description = maybeDescription; + } else { + constructor = self.DOMException; + func = funcOrConstructor; + description = descriptionOrFunc; + assert(maybeDescription === undefined, + "Too many args pased to no-constructor version of assert_throws_dom"); + } + assert_throws_dom_impl(type, func, description, "assert_throws_dom", constructor) + } + expose_assert(assert_throws_dom, "assert_throws_dom"); + + /** + * Similar to assert_throws_dom but allows specifying the assertion type + * (assert_throws_dom or promise_rejects_dom, in practice). The + * "constructor" argument must be the DOMException constructor from the + * global we expect the exception to come from. + */ + function assert_throws_dom_impl(type, func, description, assertion_type, constructor) + { + try { + func.call(this); + assert(false, assertion_type, description, + "${func} did not throw", {func:func}); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + + // Basic sanity-checks on the thrown exception. + assert(typeof e === "object", + assertion_type, description, + "${func} threw ${e} with type ${type}, not an object", + {func:func, e:e, type:typeof e}); + + assert(e !== null, + assertion_type, description, + "${func} threw null, not an object", + {func:func}); + + // Sanity-check our type + assert(typeof type == "number" || + typeof type == "string", + assertion_type, description, + "${type} is not a number or string", + {type:type}); + + var codename_name_map = { + INDEX_SIZE_ERR: 'IndexSizeError', + HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', + WRONG_DOCUMENT_ERR: 'WrongDocumentError', + INVALID_CHARACTER_ERR: 'InvalidCharacterError', + NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', + NOT_FOUND_ERR: 'NotFoundError', + NOT_SUPPORTED_ERR: 'NotSupportedError', + INUSE_ATTRIBUTE_ERR: 'InUseAttributeError', + INVALID_STATE_ERR: 'InvalidStateError', + SYNTAX_ERR: 'SyntaxError', + INVALID_MODIFICATION_ERR: 'InvalidModificationError', + NAMESPACE_ERR: 'NamespaceError', + INVALID_ACCESS_ERR: 'InvalidAccessError', + TYPE_MISMATCH_ERR: 'TypeMismatchError', + SECURITY_ERR: 'SecurityError', + NETWORK_ERR: 'NetworkError', + ABORT_ERR: 'AbortError', + URL_MISMATCH_ERR: 'URLMismatchError', + QUOTA_EXCEEDED_ERR: 'QuotaExceededError', + TIMEOUT_ERR: 'TimeoutError', + INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', + DATA_CLONE_ERR: 'DataCloneError' + }; + + var name_code_map = { + IndexSizeError: 1, + HierarchyRequestError: 3, + WrongDocumentError: 4, + InvalidCharacterError: 5, + NoModificationAllowedError: 7, + NotFoundError: 8, + NotSupportedError: 9, + InUseAttributeError: 10, + InvalidStateError: 11, + SyntaxError: 12, + InvalidModificationError: 13, + NamespaceError: 14, + InvalidAccessError: 15, + TypeMismatchError: 17, + SecurityError: 18, + NetworkError: 19, + AbortError: 20, + URLMismatchError: 21, + QuotaExceededError: 22, + TimeoutError: 23, + InvalidNodeTypeError: 24, + DataCloneError: 25, + + EncodingError: 0, + NotReadableError: 0, + UnknownError: 0, + ConstraintError: 0, + DataError: 0, + TransactionInactiveError: 0, + ReadOnlyError: 0, + VersionError: 0, + OperationError: 0, + NotAllowedError: 0, + OptOutError: 0 + }; + + var code_name_map = {}; + for (var key in name_code_map) { + if (name_code_map[key] > 0) { + code_name_map[name_code_map[key]] = key; + } + } + + var required_props = {}; + var name; + + if (typeof type === "number") { + if (type === 0) { + throw new AssertionError('Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()'); + } else if (!(type in code_name_map)) { + throw new AssertionError('Test bug: unrecognized DOMException code "' + type + '" passed to assert_throws_dom()'); + } + name = code_name_map[type]; + required_props.code = type; + } else if (typeof type === "string") { + name = type in codename_name_map ? codename_name_map[type] : type; + if (!(name in name_code_map)) { + throw new AssertionError('Test bug: unrecognized DOMException code name or name "' + type + '" passed to assert_throws_dom()'); + } + + required_props.code = name_code_map[name]; + } + + if (required_props.code === 0 || + ("name" in e && + e.name !== e.name.toUpperCase() && + e.name !== "DOMException")) { + // New style exception: also test the name property. + required_props.name = name; + } + + for (var prop in required_props) { + assert(prop in e && e[prop] == required_props[prop], + assertion_type, description, + "${func} threw ${e} that is not a DOMException " + type + ": property ${prop} is equal to ${actual}, expected ${expected}", + {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]}); + } + + // Check that the exception is from the right global. This check is last + // so more specific, and more informative, checks on the properties can + // happen in case a totally incorrect exception is thrown. + assert(e.constructor === constructor, + assertion_type, description, + "${func} threw an exception from the wrong global", + {func}); + + } + } + + /** + * Assert the provided value is thrown. + * + * @param {value} exception The expected exception. + * @param {Function} func Function which should throw. + * @param {string} [description] Error description for the case that the error is not thrown. + */ + function assert_throws_exactly(exception, func, description) + { + assert_throws_exactly_impl(exception, func, description, + "assert_throws_exactly"); + } + expose_assert(assert_throws_exactly, "assert_throws_exactly"); + + /** + * Like assert_throws_exactly but allows specifying the assertion type + * (assert_throws_exactly or promise_rejects_exactly, in practice). + */ + function assert_throws_exactly_impl(exception, func, description, + assertion_type) + { + try { + func.call(this); + assert(false, assertion_type, description, + "${func} did not throw", {func:func}); + } catch (e) { + if (e instanceof AssertionError) { + throw e; + } + + assert(same_value(e, exception), assertion_type, description, + "${func} threw ${e} but we expected it to throw ${exception}", + {func:func, e:e, exception:exception}); + } + } + + /** + * Asserts if called. Used to ensure that a specific codepath is + * not taken e.g. that an error event isn't fired. + * + * @param {string} [description] - Description of the condition being tested. + */ + function assert_unreached(description) { + assert(false, "assert_unreached", description, + "Reached unreachable code"); + } + expose_assert(assert_unreached, "assert_unreached"); + + /** + * @callback AssertFunc + * @param {Any} actual + * @param {Any} expected + * @param {Any[]} args + */ + + /** + * Asserts that ``actual`` matches at least one value of ``expected`` + * according to a comparison defined by ``assert_func``. + * + * Note that tests with multiple allowed pass conditions are bad + * practice unless the spec specifically allows multiple + * behaviours. Test authors should not use this method simply to + * hide UA bugs. + * + * @param {AssertFunc} assert_func - Function to compare actual + * and expected. It must throw when the comparison fails and + * return when the comparison passes. + * @param {Any} actual - Test value. + * @param {Array} expected_array - Array of possible expected values. + * @param {Any[]} args - Additional arguments to pass to ``assert_func``. + */ + function assert_any(assert_func, actual, expected_array, ...args) + { + var errors = []; + var passed = false; + forEach(expected_array, + function(expected) + { + try { + assert_func.apply(this, [actual, expected].concat(args)); + passed = true; + } catch (e) { + errors.push(e.message); + } + }); + if (!passed) { + throw new AssertionError(errors.join("\n\n")); + } + } + // FIXME: assert_any cannot use expose_assert, because assert_wrapper does + // not support nested assert calls (e.g. to assert_func). We need to + // support bypassing assert_wrapper for the inner asserts here. + expose(assert_any, "assert_any"); + + /** + * Assert that a feature is implemented, based on a 'truthy' condition. + * + * This function should be used to early-exit from tests in which there is + * no point continuing without support for a non-optional spec or spec + * feature. For example: + * + * assert_implements(window.Foo, 'Foo is not supported'); + * + * @param {object} condition The truthy value to test + * @param {string} [description] Error description for the case that the condition is not truthy. + */ + function assert_implements(condition, description) { + assert(!!condition, "assert_implements", description); + } + expose_assert(assert_implements, "assert_implements") + + /** + * Assert that an optional feature is implemented, based on a 'truthy' condition. + * + * This function should be used to early-exit from tests in which there is + * no point continuing without support for an explicitly optional spec or + * spec feature. For example: + * + * assert_implements_optional(video.canPlayType("video/webm"), + * "webm video playback not supported"); + * + * @param {object} condition The truthy value to test + * @param {string} [description] Error description for the case that the condition is not truthy. + */ + function assert_implements_optional(condition, description) { + if (!condition) { + throw new OptionalFeatureUnsupportedError(description); + } + } + expose_assert(assert_implements_optional, "assert_implements_optional"); + + /** + * @class + * + * A single subtest. A Test is not constructed directly but via the + * :js:func:`test`, :js:func:`async_test` or :js:func:`promise_test` functions. + * + * @param {string} name - This must be unique in a given file and must be + * invariant between runs. + * + */ + function Test(name, properties) + { + if (tests.file_is_test && tests.tests.length) { + throw new Error("Tried to create a test with file_is_test"); + } + /** The test name. */ + this.name = name; + + this.phase = (tests.is_aborted || tests.phase === tests.phases.COMPLETE) ? + this.phases.COMPLETE : this.phases.INITIAL; + + /** The test status code.*/ + this.status = this.NOTRUN; + this.timeout_id = null; + this.index = null; + + this.properties = properties || {}; + this.timeout_length = settings.test_timeout; + if (this.timeout_length !== null) { + this.timeout_length *= tests.timeout_multiplier; + } + + /** A message indicating the reason for test failure. */ + this.message = null; + /** Stack trace in case of failure. */ + this.stack = null; + + this.steps = []; + this._is_promise_test = false; + + this.cleanup_callbacks = []; + this._user_defined_cleanup_count = 0; + this._done_callbacks = []; + + if (typeof AbortController === "function") { + this._abortController = new AbortController(); + } + + // Tests declared following harness completion are likely an indication + // of a programming error, but they cannot be reported + // deterministically. + if (tests.phase === tests.phases.COMPLETE) { + return; + } + + tests.push(this); + } + + /** + * Enum of possible test statuses. + * + * :values: + * - ``PASS`` + * - ``FAIL`` + * - ``TIMEOUT`` + * - ``NOTRUN`` + * - ``PRECONDITION_FAILED`` + */ + Test.statuses = { + PASS:0, + FAIL:1, + TIMEOUT:2, + NOTRUN:3, + PRECONDITION_FAILED:4 + }; + + Test.prototype = merge({}, Test.statuses); + + Test.prototype.phases = { + INITIAL:0, + STARTED:1, + HAS_RESULT:2, + CLEANING:3, + COMPLETE:4 + }; + + Test.prototype.status_formats = { + 0: "Pass", + 1: "Fail", + 2: "Timeout", + 3: "Not Run", + 4: "Optional Feature Unsupported", + } + + Test.prototype.format_status = function() { + return this.status_formats[this.status]; + } + + Test.prototype.structured_clone = function() + { + if (!this._structured_clone) { + var msg = this.message; + msg = msg ? String(msg) : msg; + this._structured_clone = merge({ + name:String(this.name), + properties:merge({}, this.properties), + phases:merge({}, this.phases) + }, Test.statuses); + } + this._structured_clone.status = this.status; + this._structured_clone.message = this.message; + this._structured_clone.stack = this.stack; + this._structured_clone.index = this.index; + this._structured_clone.phase = this.phase; + return this._structured_clone; + }; + + /** + * Run a single step of an ongoing test. + * + * @param {string} func - Callback function to run as a step. If + * this throws an :js:func:`AssertionError`, or any other + * exception, the :js:class:`Test` status is set to ``FAIL``. + * @param {Object} [this_obj] - The object to use as the this + * value when calling ``func``. Defaults to the :js:class:`Test` object. + */ + Test.prototype.step = function(func, this_obj) + { + if (this.phase > this.phases.STARTED) { + return; + } + + if (settings.debug && this.phase !== this.phases.STARTED) { + console.log("TEST START", this.name); + } + this.phase = this.phases.STARTED; + //If we don't get a result before the harness times out that will be a test timeout + this.set_status(this.TIMEOUT, "Test timed out"); + + tests.started = true; + tests.current_test = this; + tests.notify_test_state(this); + + if (this.timeout_id === null) { + this.set_timeout(); + } + + this.steps.push(func); + + if (arguments.length === 1) { + this_obj = this; + } + + if (settings.debug) { + console.debug("TEST STEP", this.name); + } + + try { + return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)); + } catch (e) { + if (this.phase >= this.phases.HAS_RESULT) { + return; + } + var status = e instanceof OptionalFeatureUnsupportedError ? this.PRECONDITION_FAILED : this.FAIL; + var message = String((typeof e === "object" && e !== null) ? e.message : e); + var stack = e.stack ? e.stack : null; + + this.set_status(status, message, stack); + this.phase = this.phases.HAS_RESULT; + this.done(); + } finally { + this.current_test = null; + } + }; + + /** + * Wrap a function so that it runs as a step of the current test. + * + * This allows creating a callback function that will run as a + * test step. + * + * @example + * let t = async_test("Example"); + * onload = t.step_func(e => { + * assert_equals(e.name, "load"); + * // Mark the test as complete. + * t.done(); + * }) + * + * @param {string} func - Function to run as a step. If this + * throws an :js:func:`AssertionError`, or any other exception, + * the :js:class:`Test` status is set to ``FAIL``. + * @param {Object} [this_obj] - The object to use as the this + * value when calling ``func``. Defaults to the :js:class:`Test` object. + */ + Test.prototype.step_func = function(func, this_obj) + { + var test_this = this; + + if (arguments.length === 1) { + this_obj = test_this; + } + + return function() + { + return test_this.step.apply(test_this, [func, this_obj].concat( + Array.prototype.slice.call(arguments))); + }; + }; + + /** + * Wrap a function so that it runs as a step of the current test, + * and automatically marks the test as complete if the function + * returns without error. + * + * @param {string} func - Function to run as a step. If this + * throws an :js:func:`AssertionError`, or any other exception, + * the :js:class:`Test` status is set to ``FAIL``. If it returns + * without error the status is set to ``PASS``. + * @param {Object} [this_obj] - The object to use as the this + * value when calling `func`. Defaults to the :js:class:`Test` object. + */ + Test.prototype.step_func_done = function(func, this_obj) + { + var test_this = this; + + if (arguments.length === 1) { + this_obj = test_this; + } + + return function() + { + if (func) { + test_this.step.apply(test_this, [func, this_obj].concat( + Array.prototype.slice.call(arguments))); + } + test_this.done(); + }; + }; + + /** + * Return a function that automatically sets the current test to + * ``FAIL`` if it's called. + * + * @param {string} [description] - Error message to add to assert + * in case of failure. + * + */ + Test.prototype.unreached_func = function(description) + { + return this.step_func(function() { + assert_unreached(description); + }); + }; + + /** + * Run a function as a step of the test after a given timeout. + * + * This multiplies the timeout by the global timeout multiplier to + * account for the expected execution speed of the current test + * environment. For example ``test.step_timeout(f, 2000)`` with a + * timeout multiplier of 2 will wait for 4000ms before calling ``f``. + * + * In general it's encouraged to use :js:func:`Test.step_wait` or + * :js:func:`step_wait_func` in preference to this function where possible, + * as they provide better test performance. + * + * @param {Function} func - Function to run as a test + * step. + * @param {number} timeout - Time in ms to wait before running the + * test step. The actual wait time is ``timeout`` x + * ``timeout_multiplier``. + * + */ + Test.prototype.step_timeout = function(func, timeout) { + var test_this = this; + var args = Array.prototype.slice.call(arguments, 2); + var local_set_timeout = typeof global_scope.setTimeout === "undefined" ? fake_set_timeout : setTimeout; + return local_set_timeout(this.step_func(function() { + return func.apply(test_this, args); + }), timeout * tests.timeout_multiplier); + }; + + /** + * Poll for a function to return true, and call a callback + * function once it does, or assert if a timeout is + * reached. This is preferred over a simple step_timeout + * whenever possible since it allows the timeout to be longer + * to reduce intermittents without compromising test execution + * speed when the condition is quickly met. + * + * @param {Function} cond A function taking no arguments and + * returning a boolean or a Promise. The callback is + * called when this function returns true, or the + * returned Promise is resolved with true. + * @param {Function} func A function taking no arguments to call once + * the condition is met. + * @param {string} [description] Error message to add to assert in case of + * failure. + * @param {number} timeout Timeout in ms. This is multiplied by the global + * timeout_multiplier + * @param {number} interval Polling interval in ms + * + */ + Test.prototype.step_wait_func = function(cond, func, description, + timeout=3000, interval=100) { + var timeout_full = timeout * tests.timeout_multiplier; + var remaining = Math.ceil(timeout_full / interval); + var test_this = this; + var local_set_timeout = typeof global_scope.setTimeout === 'undefined' ? fake_set_timeout : setTimeout; + + const step = test_this.step_func((result) => { + if (result) { + func(); + } else { + if (remaining === 0) { + assert(false, "step_wait_func", description, + "Timed out waiting on condition"); + } + remaining--; + local_set_timeout(wait_for_inner, interval); + } + }); + + var wait_for_inner = test_this.step_func(() => { + Promise.resolve(cond()).then( + step, + test_this.unreached_func("step_wait_func")); + }); + + wait_for_inner(); + }; + + /** + * Poll for a function to return true, and invoke a callback + * followed by this.done() once it does, or assert if a timeout + * is reached. This is preferred over a simple step_timeout + * whenever possible since it allows the timeout to be longer + * to reduce intermittents without compromising test execution speed + * when the condition is quickly met. + * + * @example + * async_test(t => { + * const popup = window.open("resources/coop-coep.py?coop=same-origin&coep=&navigate=about:blank"); + * t.add_cleanup(() => popup.close()); + * assert_equals(window, popup.opener); + * + * popup.onload = t.step_func(() => { + * assert_true(popup.location.href.endsWith("&navigate=about:blank")); + * // Use step_wait_func_done as about:blank cannot message back. + * t.step_wait_func_done(() => popup.location.href === "about:blank"); + * }); + * }, "Navigating a popup to about:blank"); + * + * @param {Function} cond A function taking no arguments and + * returning a boolean or a Promise. The callback is + * called when this function returns true, or the + * returned Promise is resolved with true. + * @param {Function} func A function taking no arguments to call once + * the condition is met. + * @param {string} [description] Error message to add to assert in case of + * failure. + * @param {number} timeout Timeout in ms. This is multiplied by the global + * timeout_multiplier + * @param {number} interval Polling interval in ms + * + */ + Test.prototype.step_wait_func_done = function(cond, func, description, + timeout=3000, interval=100) { + this.step_wait_func(cond, () => { + if (func) { + func(); + } + this.done(); + }, description, timeout, interval); + }; + + /** + * Poll for a function to return true, and resolve a promise + * once it does, or assert if a timeout is reached. This is + * preferred over a simple step_timeout whenever possible + * since it allows the timeout to be longer to reduce + * intermittents without compromising test execution speed + * when the condition is quickly met. + * + * @example + * promise_test(async t => { + * // … + * await t.step_wait(() => frame.contentDocument === null, "Frame navigated to a cross-origin document"); + * // … + * }, ""); + * + * @param {Function} cond A function taking no arguments and + * returning a boolean or a Promise. + * @param {string} [description] Error message to add to assert in case of + * failure. + * @param {number} timeout Timeout in ms. This is multiplied by the global + * timeout_multiplier + * @param {number} interval Polling interval in ms + * @returns {Promise} Promise resolved once cond is met. + * + */ + Test.prototype.step_wait = function(cond, description, timeout=3000, interval=100) { + return new Promise(resolve => { + this.step_wait_func(cond, resolve, description, timeout, interval); + }); + } + + /* + * Private method for registering cleanup functions. `testharness.js` + * internals should use this method instead of the public `add_cleanup` + * method in order to hide implementation details from the harness status + * message in the case errors. + */ + Test.prototype._add_cleanup = function(callback) { + this.cleanup_callbacks.push(callback); + }; + + /** + * Schedule a function to be run after the test result is known, regardless + * of passing or failing state. + * + * The behavior of this function will not + * influence the result of the test, but if an exception is thrown, the + * test harness will report an error. + * + * @param {Function} callback - The cleanup function to run. This + * is called with no arguments. + */ + Test.prototype.add_cleanup = function(callback) { + this._user_defined_cleanup_count += 1; + this._add_cleanup(callback); + }; + + Test.prototype.set_timeout = function() + { + if (this.timeout_length !== null) { + var this_obj = this; + this.timeout_id = setTimeout(function() + { + this_obj.timeout(); + }, this.timeout_length); + } + }; + + Test.prototype.set_status = function(status, message, stack) + { + this.status = status; + this.message = message; + this.stack = stack ? stack : null; + }; + + /** + * Manually set the test status to ``TIMEOUT``. + */ + Test.prototype.timeout = function() + { + this.timeout_id = null; + this.set_status(this.TIMEOUT, "Test timed out"); + this.phase = this.phases.HAS_RESULT; + this.done(); + }; + + /** + * Manually set the test status to ``TIMEOUT``. + * + * Alias for `Test.timeout <#Test.timeout>`_. + */ + Test.prototype.force_timeout = function() { + return this.timeout(); + }; + + /** + * Mark the test as complete. + * + * This sets the test status to ``PASS`` if no other status was + * already recorded. Any subsequent attempts to run additional + * test steps will be ignored. + * + * After setting the test status any test cleanup functions will + * be run. + */ + Test.prototype.done = function() + { + if (this.phase >= this.phases.CLEANING) { + return; + } + + if (this.phase <= this.phases.STARTED) { + this.set_status(this.PASS, null); + } + + if (global_scope.clearTimeout) { + clearTimeout(this.timeout_id); + } + + if (settings.debug) { + console.log("TEST DONE", + this.status, + this.name); + } + + this.cleanup(); + }; + + function add_test_done_callback(test, callback) + { + if (test.phase === test.phases.COMPLETE) { + callback(); + return; + } + + test._done_callbacks.push(callback); + } + + /* + * Invoke all specified cleanup functions. If one or more produce an error, + * the context is in an unpredictable state, so all further testing should + * be cancelled. + */ + Test.prototype.cleanup = function() { + var errors = []; + var bad_value_count = 0; + function on_error(e) { + errors.push(e); + // Abort tests immediately so that tests declared within subsequent + // cleanup functions are not run. + tests.abort(); + } + var this_obj = this; + var results = []; + + this.phase = this.phases.CLEANING; + + if (this._abortController) { + this._abortController.abort("Test cleanup"); + } + + forEach(this.cleanup_callbacks, + function(cleanup_callback) { + var result; + + try { + result = cleanup_callback(); + } catch (e) { + on_error(e); + return; + } + + if (!is_valid_cleanup_result(this_obj, result)) { + bad_value_count += 1; + // Abort tests immediately so that tests declared + // within subsequent cleanup functions are not run. + tests.abort(); + } + + results.push(result); + }); + + if (!this._is_promise_test) { + cleanup_done(this_obj, errors, bad_value_count); + } else { + all_async(results, + function(result, done) { + if (result && typeof result.then === "function") { + result + .then(null, on_error) + .then(done); + } else { + done(); + } + }, + function() { + cleanup_done(this_obj, errors, bad_value_count); + }); + } + }; + + /* + * Determine if the return value of a cleanup function is valid for a given + * test. Any test may return the value `undefined`. Tests created with + * `promise_test` may alternatively return "thenable" object values. + */ + function is_valid_cleanup_result(test, result) { + if (result === undefined) { + return true; + } + + if (test._is_promise_test) { + return result && typeof result.then === "function"; + } + + return false; + } + + function cleanup_done(test, errors, bad_value_count) { + if (errors.length || bad_value_count) { + var total = test._user_defined_cleanup_count; + + tests.status.status = tests.status.ERROR; + tests.status.stack = null; + tests.status.message = "Test named '" + test.name + + "' specified " + total + + " 'cleanup' function" + (total > 1 ? "s" : ""); + + if (errors.length) { + tests.status.message += ", and " + errors.length + " failed"; + tests.status.stack = ((typeof errors[0] === "object" && + errors[0].hasOwnProperty("stack")) ? + errors[0].stack : null); + } + + if (bad_value_count) { + var type = test._is_promise_test ? + "non-thenable" : "non-undefined"; + tests.status.message += ", and " + bad_value_count + + " returned a " + type + " value"; + } + + tests.status.message += "."; + } + + test.phase = test.phases.COMPLETE; + tests.result(test); + forEach(test._done_callbacks, + function(callback) { + callback(); + }); + test._done_callbacks.length = 0; + } + + /** + * Gives an AbortSignal that will be aborted when the test finishes. + */ + Test.prototype.get_signal = function() { + if (!this._abortController) { + throw new Error("AbortController is not supported in this browser"); + } + return this._abortController.signal; + } + + /** + * A RemoteTest object mirrors a Test object on a remote worker. The + * associated RemoteWorker updates the RemoteTest object in response to + * received events. In turn, the RemoteTest object replicates these events + * on the local document. This allows listeners (test result reporting + * etc..) to transparently handle local and remote events. + */ + function RemoteTest(clone) { + var this_obj = this; + Object.keys(clone).forEach( + function(key) { + this_obj[key] = clone[key]; + }); + this.index = null; + this.phase = this.phases.INITIAL; + this.update_state_from(clone); + this._done_callbacks = []; + tests.push(this); + } + + RemoteTest.prototype.structured_clone = function() { + var clone = {}; + Object.keys(this).forEach( + (function(key) { + var value = this[key]; + // `RemoteTest` instances are responsible for managing + // their own "done" callback functions, so those functions + // are not relevant in other execution contexts. Because of + // this (and because Function values cannot be serialized + // for cross-realm transmittance), the property should not + // be considered when cloning instances. + if (key === '_done_callbacks' ) { + return; + } + + if (typeof value === "object" && value !== null) { + clone[key] = merge({}, value); + } else { + clone[key] = value; + } + }).bind(this)); + clone.phases = merge({}, this.phases); + return clone; + }; + + /** + * `RemoteTest` instances are objects which represent tests running in + * another realm. They do not define "cleanup" functions (if necessary, + * such functions are defined on the associated `Test` instance within the + * external realm). However, `RemoteTests` may have "done" callbacks (e.g. + * as attached by the `Tests` instance responsible for tracking the overall + * test status in the parent realm). The `cleanup` method delegates to + * `done` in order to ensure that such callbacks are invoked following the + * completion of the `RemoteTest`. + */ + RemoteTest.prototype.cleanup = function() { + this.done(); + }; + RemoteTest.prototype.phases = Test.prototype.phases; + RemoteTest.prototype.update_state_from = function(clone) { + this.status = clone.status; + this.message = clone.message; + this.stack = clone.stack; + if (this.phase === this.phases.INITIAL) { + this.phase = this.phases.STARTED; + } + }; + RemoteTest.prototype.done = function() { + this.phase = this.phases.COMPLETE; + + forEach(this._done_callbacks, + function(callback) { + callback(); + }); + } + + RemoteTest.prototype.format_status = function() { + return Test.prototype.status_formats[this.status]; + } + + /* + * A RemoteContext listens for test events from a remote test context, such + * as another window or a worker. These events are then used to construct + * and maintain RemoteTest objects that mirror the tests running in the + * remote context. + * + * An optional third parameter can be used as a predicate to filter incoming + * MessageEvents. + */ + function RemoteContext(remote, message_target, message_filter) { + this.running = true; + this.started = false; + this.tests = new Array(); + this.early_exception = null; + + var this_obj = this; + // If remote context is cross origin assigning to onerror is not + // possible, so silently catch those errors. + try { + remote.onerror = function(error) { this_obj.remote_error(error); }; + } catch (e) { + // Ignore. + } + + // Keeping a reference to the remote object and the message handler until + // remote_done() is seen prevents the remote object and its message channel + // from going away before all the messages are dispatched. + this.remote = remote; + this.message_target = message_target; + this.message_handler = function(message) { + var passesFilter = !message_filter || message_filter(message); + // The reference to the `running` property in the following + // condition is unnecessary because that value is only set to + // `false` after the `message_handler` function has been + // unsubscribed. + // TODO: Simplify the condition by removing the reference. + if (this_obj.running && message.data && passesFilter && + (message.data.type in this_obj.message_handlers)) { + this_obj.message_handlers[message.data.type].call(this_obj, message.data); + } + }; + + if (self.Promise) { + this.done = new Promise(function(resolve) { + this_obj.doneResolve = resolve; + }); + } + + this.message_target.addEventListener("message", this.message_handler); + } + + RemoteContext.prototype.remote_error = function(error) { + if (error.preventDefault) { + error.preventDefault(); + } + + // Defer interpretation of errors until the testing protocol has + // started and the remote test's `allow_uncaught_exception` property + // is available. + if (!this.started) { + this.early_exception = error; + } else if (!this.allow_uncaught_exception) { + this.report_uncaught(error); + } + }; + + RemoteContext.prototype.report_uncaught = function(error) { + var message = error.message || String(error); + var filename = (error.filename ? " " + error.filename: ""); + // FIXME: Display remote error states separately from main document + // error state. + tests.set_status(tests.status.ERROR, + "Error in remote" + filename + ": " + message, + error.stack); + }; + + RemoteContext.prototype.start = function(data) { + this.started = true; + this.allow_uncaught_exception = data.properties.allow_uncaught_exception; + + if (this.early_exception && !this.allow_uncaught_exception) { + this.report_uncaught(this.early_exception); + } + }; + + RemoteContext.prototype.test_state = function(data) { + var remote_test = this.tests[data.test.index]; + if (!remote_test) { + remote_test = new RemoteTest(data.test); + this.tests[data.test.index] = remote_test; + } + remote_test.update_state_from(data.test); + tests.notify_test_state(remote_test); + }; + + RemoteContext.prototype.test_done = function(data) { + var remote_test = this.tests[data.test.index]; + remote_test.update_state_from(data.test); + remote_test.done(); + tests.result(remote_test); + }; + + RemoteContext.prototype.remote_done = function(data) { + if (tests.status.status === null && + data.status.status !== data.status.OK) { + tests.set_status(data.status.status, data.status.message, data.status.stack); + } + + for (let assert of data.asserts) { + var record = new AssertRecord(); + record.assert_name = assert.assert_name; + record.args = assert.args; + record.test = assert.test != null ? this.tests[assert.test.index] : null; + record.status = assert.status; + record.stack = assert.stack; + tests.asserts_run.push(record); + } + + this.message_target.removeEventListener("message", this.message_handler); + this.running = false; + + // If remote context is cross origin assigning to onerror is not + // possible, so silently catch those errors. + try { + this.remote.onerror = null; + } catch (e) { + // Ignore. + } + + this.remote = null; + this.message_target = null; + if (this.doneResolve) { + this.doneResolve(); + } + + if (tests.all_done()) { + tests.complete(); + } + }; + + RemoteContext.prototype.message_handlers = { + start: RemoteContext.prototype.start, + test_state: RemoteContext.prototype.test_state, + result: RemoteContext.prototype.test_done, + complete: RemoteContext.prototype.remote_done + }; + + /** + * @class + * Status of the overall harness + */ + function TestsStatus() + { + /** The status code */ + this.status = null; + /** Message in case of failure */ + this.message = null; + /** Stack trace in case of an exception. */ + this.stack = null; + } + + /** + * Enum of possible harness statuses. + * + * :values: + * - ``OK`` + * - ``ERROR`` + * - ``TIMEOUT`` + * - ``PRECONDITION_FAILED`` + */ + TestsStatus.statuses = { + OK:0, + ERROR:1, + TIMEOUT:2, + PRECONDITION_FAILED:3 + }; + + TestsStatus.prototype = merge({}, TestsStatus.statuses); + + TestsStatus.prototype.formats = { + 0: "OK", + 1: "Error", + 2: "Timeout", + 3: "Optional Feature Unsupported" + }; + + TestsStatus.prototype.structured_clone = function() + { + if (!this._structured_clone) { + var msg = this.message; + msg = msg ? String(msg) : msg; + this._structured_clone = merge({ + status:this.status, + message:msg, + stack:this.stack + }, TestsStatus.statuses); + } + return this._structured_clone; + }; + + TestsStatus.prototype.format_status = function() { + return this.formats[this.status]; + }; + + /** + * @class + * Record of an assert that ran. + * + * @param {Test} test - The test which ran the assert. + * @param {string} assert_name - The function name of the assert. + * @param {Any} args - The arguments passed to the assert function. + */ + function AssertRecord(test, assert_name, args = []) { + /** Name of the assert that ran */ + this.assert_name = assert_name; + /** Test that ran the assert */ + this.test = test; + // Avoid keeping complex objects alive + /** Stringification of the arguments that were passed to the assert function */ + this.args = args.map(x => format_value(x).replace(/\n/g, " ")); + /** Status of the assert */ + this.status = null; + } + + AssertRecord.prototype.structured_clone = function() { + return { + assert_name: this.assert_name, + test: this.test ? this.test.structured_clone() : null, + args: this.args, + status: this.status, + }; + }; + + function Tests() + { + this.tests = []; + this.num_pending = 0; + + this.phases = { + INITIAL:0, + SETUP:1, + HAVE_TESTS:2, + HAVE_RESULTS:3, + COMPLETE:4 + }; + this.phase = this.phases.INITIAL; + + this.properties = {}; + + this.wait_for_finish = false; + this.processing_callbacks = false; + + this.allow_uncaught_exception = false; + + this.file_is_test = false; + // This value is lazily initialized in order to avoid introducing a + // dependency on ECMAScript 2015 Promises to all tests. + this.promise_tests = null; + this.promise_setup_called = false; + + this.timeout_multiplier = 1; + this.timeout_length = test_environment.test_timeout(); + this.timeout_id = null; + + this.start_callbacks = []; + this.test_state_callbacks = []; + this.test_done_callbacks = []; + this.all_done_callbacks = []; + + this.hide_test_state = false; + this.pending_remotes = []; + + this.current_test = null; + this.asserts_run = []; + + // Track whether output is enabled, and thus whether or not we should + // track asserts. + // + // On workers we don't get properties set from testharnessreport.js, so + // we don't know whether or not to track asserts. To avoid the + // resulting performance hit, we assume we are not meant to. This means + // that assert tracking does not function on workers. + this.output = settings.output && 'document' in global_scope; + + this.status = new TestsStatus(); + + var this_obj = this; + + test_environment.add_on_loaded_callback(function() { + if (this_obj.all_done()) { + this_obj.complete(); + } + }); + + this.set_timeout(); + } + + Tests.prototype.setup = function(func, properties) + { + if (this.phase >= this.phases.HAVE_RESULTS) { + return; + } + + if (this.phase < this.phases.SETUP) { + this.phase = this.phases.SETUP; + } + + this.properties = properties; + + for (var p in properties) { + if (properties.hasOwnProperty(p)) { + var value = properties[p]; + if (p == "allow_uncaught_exception") { + this.allow_uncaught_exception = value; + } else if (p == "explicit_done" && value) { + this.wait_for_finish = true; + } else if (p == "explicit_timeout" && value) { + this.timeout_length = null; + if (this.timeout_id) + { + clearTimeout(this.timeout_id); + } + } else if (p == "single_test" && value) { + this.set_file_is_test(); + } else if (p == "timeout_multiplier") { + this.timeout_multiplier = value; + if (this.timeout_length) { + this.timeout_length *= this.timeout_multiplier; + } + } else if (p == "hide_test_state") { + this.hide_test_state = value; + } else if (p == "output") { + this.output = value; + } else if (p === "debug") { + settings.debug = value; + } + } + } + + if (func) { + try { + func(); + } catch (e) { + this.status.status = e instanceof OptionalFeatureUnsupportedError ? this.status.PRECONDITION_FAILED : this.status.ERROR; + this.status.message = String(e); + this.status.stack = e.stack ? e.stack : null; + this.complete(); + } + } + this.set_timeout(); + }; + + Tests.prototype.set_file_is_test = function() { + if (this.tests.length > 0) { + throw new Error("Tried to set file as test after creating a test"); + } + this.wait_for_finish = true; + this.file_is_test = true; + // Create the test, which will add it to the list of tests + tests.current_test = async_test(); + }; + + Tests.prototype.set_status = function(status, message, stack) + { + this.status.status = status; + this.status.message = message; + this.status.stack = stack ? stack : null; + }; + + Tests.prototype.set_timeout = function() { + if (global_scope.clearTimeout) { + var this_obj = this; + clearTimeout(this.timeout_id); + if (this.timeout_length !== null) { + this.timeout_id = setTimeout(function() { + this_obj.timeout(); + }, this.timeout_length); + } + } + }; + + Tests.prototype.timeout = function() { + var test_in_cleanup = null; + + if (this.status.status === null) { + forEach(this.tests, + function(test) { + // No more than one test is expected to be in the + // "CLEANUP" phase at any time + if (test.phase === test.phases.CLEANING) { + test_in_cleanup = test; + } + + test.phase = test.phases.COMPLETE; + }); + + // Timeouts that occur while a test is in the "cleanup" phase + // indicate that some global state was not properly reverted. This + // invalidates the overall test execution, so the timeout should be + // reported as an error and cancel the execution of any remaining + // tests. + if (test_in_cleanup) { + this.status.status = this.status.ERROR; + this.status.message = "Timeout while running cleanup for " + + "test named \"" + test_in_cleanup.name + "\"."; + tests.status.stack = null; + } else { + this.status.status = this.status.TIMEOUT; + } + } + + this.complete(); + }; + + Tests.prototype.end_wait = function() + { + this.wait_for_finish = false; + if (this.all_done()) { + this.complete(); + } + }; + + Tests.prototype.push = function(test) + { + if (this.phase < this.phases.HAVE_TESTS) { + this.start(); + } + this.num_pending++; + test.index = this.tests.push(test); + this.notify_test_state(test); + }; + + Tests.prototype.notify_test_state = function(test) { + var this_obj = this; + forEach(this.test_state_callbacks, + function(callback) { + callback(test, this_obj); + }); + }; + + Tests.prototype.all_done = function() { + return (this.tests.length > 0 || this.pending_remotes.length > 0) && + test_environment.all_loaded && + (this.num_pending === 0 || this.is_aborted) && !this.wait_for_finish && + !this.processing_callbacks && + !this.pending_remotes.some(function(w) { return w.running; }); + }; + + Tests.prototype.start = function() { + this.phase = this.phases.HAVE_TESTS; + this.notify_start(); + }; + + Tests.prototype.notify_start = function() { + var this_obj = this; + forEach (this.start_callbacks, + function(callback) + { + callback(this_obj.properties); + }); + }; + + Tests.prototype.result = function(test) + { + // If the harness has already transitioned beyond the `HAVE_RESULTS` + // phase, subsequent tests should not cause it to revert. + if (this.phase <= this.phases.HAVE_RESULTS) { + this.phase = this.phases.HAVE_RESULTS; + } + this.num_pending--; + this.notify_result(test); + }; + + Tests.prototype.notify_result = function(test) { + var this_obj = this; + this.processing_callbacks = true; + forEach(this.test_done_callbacks, + function(callback) + { + callback(test, this_obj); + }); + this.processing_callbacks = false; + if (this_obj.all_done()) { + this_obj.complete(); + } + }; + + Tests.prototype.complete = function() { + if (this.phase === this.phases.COMPLETE) { + return; + } + var this_obj = this; + var all_complete = function() { + this_obj.phase = this_obj.phases.COMPLETE; + this_obj.notify_complete(); + }; + var incomplete = filter(this.tests, + function(test) { + return test.phase < test.phases.COMPLETE; + }); + + /** + * To preserve legacy behavior, overall test completion must be + * signaled synchronously. + */ + if (incomplete.length === 0) { + all_complete(); + return; + } + + all_async(incomplete, + function(test, testDone) + { + if (test.phase === test.phases.INITIAL) { + test.phase = test.phases.COMPLETE; + testDone(); + } else { + add_test_done_callback(test, testDone); + test.cleanup(); + } + }, + all_complete); + }; + + Tests.prototype.set_assert = function(assert_name, args) { + this.asserts_run.push(new AssertRecord(this.current_test, assert_name, args)) + } + + Tests.prototype.set_assert_status = function(index, status, stack) { + let assert_record = this.asserts_run[index]; + assert_record.status = status; + assert_record.stack = stack; + } + + /** + * Update the harness status to reflect an unrecoverable harness error that + * should cancel all further testing. Update all previously-defined tests + * which have not yet started to indicate that they will not be executed. + */ + Tests.prototype.abort = function() { + this.status.status = this.status.ERROR; + this.is_aborted = true; + + forEach(this.tests, + function(test) { + if (test.phase === test.phases.INITIAL) { + test.phase = test.phases.COMPLETE; + } + }); + }; + + /* + * Determine if any tests share the same `name` property. Return an array + * containing the names of any such duplicates. + */ + Tests.prototype.find_duplicates = function() { + var names = Object.create(null); + var duplicates = []; + + forEach (this.tests, + function(test) + { + if (test.name in names && duplicates.indexOf(test.name) === -1) { + duplicates.push(test.name); + } + names[test.name] = true; + }); + + return duplicates; + }; + + function code_unit_str(char) { + return 'U+' + char.charCodeAt(0).toString(16); + } + + function sanitize_unpaired_surrogates(str) { + return str.replace( + /([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g, + function(_, low, prefix, high) { + var output = prefix || ""; // prefix may be undefined + var string = low || high; // only one of these alternates can match + for (var i = 0; i < string.length; i++) { + output += code_unit_str(string[i]); + } + return output; + }); + } + + function sanitize_all_unpaired_surrogates(tests) { + forEach (tests, + function (test) + { + var sanitized = sanitize_unpaired_surrogates(test.name); + + if (test.name !== sanitized) { + test.name = sanitized; + delete test._structured_clone; + } + }); + } + + Tests.prototype.notify_complete = function() { + var this_obj = this; + var duplicates; + + if (this.status.status === null) { + duplicates = this.find_duplicates(); + + // Some transports adhere to UTF-8's restriction on unpaired + // surrogates. Sanitize the titles so that the results can be + // consistently sent via all transports. + sanitize_all_unpaired_surrogates(this.tests); + + // Test names are presumed to be unique within test files--this + // allows consumers to use them for identification purposes. + // Duplicated names violate this expectation and should therefore + // be reported as an error. + if (duplicates.length) { + this.status.status = this.status.ERROR; + this.status.message = + duplicates.length + ' duplicate test name' + + (duplicates.length > 1 ? 's' : '') + ': "' + + duplicates.join('", "') + '"'; + } else { + this.status.status = this.status.OK; + } + } + + forEach (this.all_done_callbacks, + function(callback) + { + callback(this_obj.tests, this_obj.status, this_obj.asserts_run); + }); + }; + + /* + * Constructs a RemoteContext that tracks tests from a specific worker. + */ + Tests.prototype.create_remote_worker = function(worker) { + var message_port; + + if (is_service_worker(worker)) { + message_port = navigator.serviceWorker; + worker.postMessage({type: "connect"}); + } else if (is_shared_worker(worker)) { + message_port = worker.port; + message_port.start(); + } else { + message_port = worker; + } + + return new RemoteContext(worker, message_port); + }; + + /* + * Constructs a RemoteContext that tracks tests from a specific window. + */ + Tests.prototype.create_remote_window = function(remote) { + remote.postMessage({type: "getmessages"}, "*"); + return new RemoteContext( + remote, + window, + function(msg) { + return msg.source === remote; + } + ); + }; + + Tests.prototype.fetch_tests_from_worker = function(worker) { + if (this.phase >= this.phases.COMPLETE) { + return; + } + + var remoteContext = this.create_remote_worker(worker); + this.pending_remotes.push(remoteContext); + return remoteContext.done; + }; + + /** + * Get test results from a worker and include them in the current test. + * + * @param {Worker|SharedWorker|ServiceWorker|MessagePort} port - + * Either a worker object or a port connected to a worker which is + * running tests.. + * @returns {Promise} - A promise that's resolved once all the remote tests are complete. + */ + function fetch_tests_from_worker(port) { + return tests.fetch_tests_from_worker(port); + } + expose(fetch_tests_from_worker, 'fetch_tests_from_worker'); + + Tests.prototype.fetch_tests_from_window = function(remote) { + if (this.phase >= this.phases.COMPLETE) { + return; + } + + var remoteContext = this.create_remote_window(remote); + this.pending_remotes.push(remoteContext); + return remoteContext.done; + }; + + /** + * Aggregate tests from separate windows or iframes + * into the current document as if they were all part of the same test file. + * + * The document of the second window (or iframe) should include + * ``testharness.js``, but not ``testharnessreport.js``, and use + * :js:func:`test`, :js:func:`async_test`, and :js:func:`promise_test` in + * the usual manner. + * + * @param {Window} window - The window to fetch tests from. + */ + function fetch_tests_from_window(window) { + return tests.fetch_tests_from_window(window); + } + expose(fetch_tests_from_window, 'fetch_tests_from_window'); + + /** + * Get test results from a shadow realm and include them in the current test. + * + * @param {ShadowRealm} realm - A shadow realm also running the test harness + * @returns {Promise} - A promise that's resolved once all the remote tests are complete. + */ + function fetch_tests_from_shadow_realm(realm) { + var chan = new MessageChannel(); + function receiveMessage(msg_json) { + chan.port1.postMessage(JSON.parse(msg_json)); + } + var done = tests.fetch_tests_from_worker(chan.port2); + realm.evaluate("begin_shadow_realm_tests")(receiveMessage); + chan.port2.start(); + return done; + } + expose(fetch_tests_from_shadow_realm, 'fetch_tests_from_shadow_realm'); + + /** + * Begin running tests in this shadow realm test harness. + * + * To be called after all tests have been loaded; it is an error to call + * this more than once or in a non-Shadow Realm environment + * + * @param {Function} postMessage - A function to send test updates to the + * incubating realm-- accepts JSON-encoded messages in the format used by + * RemoteContext + */ + function begin_shadow_realm_tests(postMessage) { + if (!(test_environment instanceof ShadowRealmTestEnvironment)) { + throw new Error("begin_shadow_realm_tests called in non-Shadow Realm environment"); + } + + test_environment.begin(function (msg) { + postMessage(JSON.stringify(msg)); + }); + } + expose(begin_shadow_realm_tests, 'begin_shadow_realm_tests'); + + /** + * Timeout the tests. + * + * This only has an effect when ``explicit_timeout`` has been set + * in :js:func:`setup`. In other cases any call is a no-op. + * + */ + function timeout() { + if (tests.timeout_length === null) { + tests.timeout(); + } + } + expose(timeout, 'timeout'); + + /** + * Add a callback that's triggered when the first :js:class:`Test` is created. + * + * @param {Function} callback - Callback function. This is called + * without arguments. + */ + function add_start_callback(callback) { + tests.start_callbacks.push(callback); + } + + /** + * Add a callback that's triggered when a test state changes. + * + * @param {Function} callback - Callback function, called with the + * :js:class:`Test` as the only argument. + */ + function add_test_state_callback(callback) { + tests.test_state_callbacks.push(callback); + } + + /** + * Add a callback that's triggered when a test result is received. + * + * @param {Function} callback - Callback function, called with the + * :js:class:`Test` as the only argument. + */ + function add_result_callback(callback) { + tests.test_done_callbacks.push(callback); + } + + /** + * Add a callback that's triggered when all tests are complete. + * + * @param {Function} callback - Callback function, called with an + * array of :js:class:`Test` objects, a :js:class:`TestsStatus` + * object and an array of :js:class:`AssertRecord` objects. If the + * debug setting is ``false`` the final argument will be an empty + * array. + * + * For performance reasons asserts are only tracked when the debug + * setting is ``true``. In other cases the array of asserts will be + * empty. + */ + function add_completion_callback(callback) { + tests.all_done_callbacks.push(callback); + } + + expose(add_start_callback, 'add_start_callback'); + expose(add_test_state_callback, 'add_test_state_callback'); + expose(add_result_callback, 'add_result_callback'); + expose(add_completion_callback, 'add_completion_callback'); + + function remove(array, item) { + var index = array.indexOf(item); + if (index > -1) { + array.splice(index, 1); + } + } + + function remove_start_callback(callback) { + remove(tests.start_callbacks, callback); + } + + function remove_test_state_callback(callback) { + remove(tests.test_state_callbacks, callback); + } + + function remove_result_callback(callback) { + remove(tests.test_done_callbacks, callback); + } + + function remove_completion_callback(callback) { + remove(tests.all_done_callbacks, callback); + } + + /* + * Output listener + */ + + function Output() { + this.output_document = document; + this.output_node = null; + this.enabled = settings.output; + this.phase = this.INITIAL; + } + + Output.prototype.INITIAL = 0; + Output.prototype.STARTED = 1; + Output.prototype.HAVE_RESULTS = 2; + Output.prototype.COMPLETE = 3; + + Output.prototype.setup = function(properties) { + if (this.phase > this.INITIAL) { + return; + } + + //If output is disabled in testharnessreport.js the test shouldn't be + //able to override that + this.enabled = this.enabled && (properties.hasOwnProperty("output") ? + properties.output : settings.output); + }; + + Output.prototype.init = function(properties) { + if (this.phase >= this.STARTED) { + return; + } + if (properties.output_document) { + this.output_document = properties.output_document; + } else { + this.output_document = document; + } + this.phase = this.STARTED; + }; + + Output.prototype.resolve_log = function() { + var output_document; + if (this.output_node) { + return; + } + if (typeof this.output_document === "function") { + output_document = this.output_document.apply(undefined); + } else { + output_document = this.output_document; + } + if (!output_document) { + return; + } + var node = output_document.getElementById("log"); + if (!node) { + if (output_document.readyState === "loading") { + return; + } + node = output_document.createElementNS("http://www.w3.org/1999/xhtml", "div"); + node.id = "log"; + if (output_document.body) { + output_document.body.appendChild(node); + } else { + var root = output_document.documentElement; + var is_html = (root && + root.namespaceURI == "http://www.w3.org/1999/xhtml" && + root.localName == "html"); + var is_svg = (output_document.defaultView && + "SVGSVGElement" in output_document.defaultView && + root instanceof output_document.defaultView.SVGSVGElement); + if (is_svg) { + var foreignObject = output_document.createElementNS("http://www.w3.org/2000/svg", "foreignObject"); + foreignObject.setAttribute("width", "100%"); + foreignObject.setAttribute("height", "100%"); + root.appendChild(foreignObject); + foreignObject.appendChild(node); + } else if (is_html) { + root.appendChild(output_document.createElementNS("http://www.w3.org/1999/xhtml", "body")) + .appendChild(node); + } else { + root.appendChild(node); + } + } + } + this.output_document = output_document; + this.output_node = node; + }; + + Output.prototype.show_status = function() { + if (this.phase < this.STARTED) { + this.init({}); + } + if (!this.enabled || this.phase === this.COMPLETE) { + return; + } + this.resolve_log(); + if (this.phase < this.HAVE_RESULTS) { + this.phase = this.HAVE_RESULTS; + } + var done_count = tests.tests.length - tests.num_pending; + if (this.output_node && !tests.hide_test_state) { + if (done_count < 100 || + (done_count < 1000 && done_count % 100 === 0) || + done_count % 1000 === 0) { + this.output_node.textContent = "Running, " + + done_count + " complete, " + + tests.num_pending + " remain"; + } + } + }; + + Output.prototype.show_results = function (tests, harness_status, asserts_run) { + if (this.phase >= this.COMPLETE) { + return; + } + if (!this.enabled) { + return; + } + if (!this.output_node) { + this.resolve_log(); + } + this.phase = this.COMPLETE; + + var log = this.output_node; + if (!log) { + return; + } + var output_document = this.output_document; + + while (log.lastChild) { + log.removeChild(log.lastChild); + } + + var stylesheet = output_document.createElementNS(xhtml_ns, "style"); + stylesheet.textContent = stylesheetContent; + var heads = output_document.getElementsByTagName("head"); + if (heads.length) { + heads[0].appendChild(stylesheet); + } + + var status_number = {}; + forEach(tests, + function(test) { + var status = test.format_status(); + if (status_number.hasOwnProperty(status)) { + status_number[status] += 1; + } else { + status_number[status] = 1; + } + }); + + function status_class(status) + { + return status.replace(/\s/g, '').toLowerCase(); + } + + var summary_template = ["section", {"id":"summary"}, + ["h2", {}, "Summary"], + function() + { + var status = harness_status.format_status(); + var rv = [["section", {}, + ["p", {}, + "Harness status: ", + ["span", {"class":status_class(status)}, + status + ], + ], + ["button", + {"onclick": "let evt = new Event('__test_restart'); " + + "let canceled = !window.dispatchEvent(evt);" + + "if (!canceled) { location.reload() }"}, + "Rerun"] + ]]; + + if (harness_status.status === harness_status.ERROR) { + rv[0].push(["pre", {}, harness_status.message]); + if (harness_status.stack) { + rv[0].push(["pre", {}, harness_status.stack]); + } + } + return rv; + }, + ["p", {}, "Found ${num_tests} tests"], + function() { + var rv = [["div", {}]]; + var i = 0; + while (Test.prototype.status_formats.hasOwnProperty(i)) { + if (status_number.hasOwnProperty(Test.prototype.status_formats[i])) { + var status = Test.prototype.status_formats[i]; + rv[0].push(["div", {}, + ["label", {}, + ["input", {type:"checkbox", checked:"checked"}], + status_number[status] + " ", + ["span", {"class":status_class(status)}, status]]]); + } + i++; + } + return rv; + }, + ]; + + log.appendChild(render(summary_template, {num_tests:tests.length}, output_document)); + + forEach(output_document.querySelectorAll("section#summary label"), + function(element) + { + on_event(element, "click", + function(e) + { + if (output_document.getElementById("results") === null) { + e.preventDefault(); + return; + } + var result_class = element.querySelector("span[class]").getAttribute("class"); + var style_element = output_document.querySelector("style#hide-" + result_class); + var input_element = element.querySelector("input"); + if (!style_element && !input_element.checked) { + style_element = output_document.createElementNS(xhtml_ns, "style"); + style_element.id = "hide-" + result_class; + style_element.textContent = "table#results > tbody > tr.overall-"+result_class+"{display:none}"; + output_document.body.appendChild(style_element); + } else if (style_element && input_element.checked) { + style_element.parentNode.removeChild(style_element); + } + }); + }); + + // This use of innerHTML plus manual escaping is not recommended in + // general, but is necessary here for performance. Using textContent + // on each individual <td> adds tens of seconds of execution time for + // large test suites (tens of thousands of tests). + function escape_html(s) + { + return s.replace(/\&/g, "&") + .replace(/</g, "<") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function has_assertions() + { + for (var i = 0; i < tests.length; i++) { + if (tests[i].properties.hasOwnProperty("assert")) { + return true; + } + } + return false; + } + + function get_assertion(test) + { + if (test.properties.hasOwnProperty("assert")) { + if (Array.isArray(test.properties.assert)) { + return test.properties.assert.join(' '); + } + return test.properties.assert; + } + return ''; + } + + var asserts_run_by_test = new Map(); + asserts_run.forEach(assert => { + if (!asserts_run_by_test.has(assert.test)) { + asserts_run_by_test.set(assert.test, []); + } + asserts_run_by_test.get(assert.test).push(assert); + }); + + function get_asserts_output(test) { + var asserts = asserts_run_by_test.get(test); + if (!asserts) { + return "No asserts ran"; + } + rv = "<table>"; + rv += asserts.map(assert => { + var output_fn = "<strong>" + escape_html(assert.assert_name) + "</strong>("; + var prefix_len = output_fn.length; + var output_args = assert.args; + var output_len = output_args.reduce((prev, current) => prev+current, prefix_len); + if (output_len[output_len.length - 1] > 50) { + output_args = output_args.map((x, i) => + (i > 0 ? " ".repeat(prefix_len) : "" )+ x + (i < output_args.length - 1 ? ",\n" : "")); + } else { + output_args = output_args.map((x, i) => x + (i < output_args.length - 1 ? ", " : "")); + } + output_fn += escape_html(output_args.join("")); + output_fn += ')'; + var output_location; + if (assert.stack) { + output_location = assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " "); + } + return "<tr class='overall-" + + status_class(Test.prototype.status_formats[assert.status]) + "'>" + + "<td class='" + + status_class(Test.prototype.status_formats[assert.status]) + "'>" + + Test.prototype.status_formats[assert.status] + "</td>" + + "<td><pre>" + + output_fn + + (output_location ? "\n" + escape_html(output_location) : "") + + "</pre></td></tr>"; + } + ).join("\n"); + rv += "</table>"; + return rv; + } + + log.appendChild(document.createElementNS(xhtml_ns, "section")); + var assertions = has_assertions(); + var html = "<h2>Details</h2><table id='results' " + (assertions ? "class='assertions'" : "" ) + ">" + + "<thead><tr><th>Result</th><th>Test Name</th>" + + (assertions ? "<th>Assertion</th>" : "") + + "<th>Message</th></tr></thead>" + + "<tbody>"; + for (var i = 0; i < tests.length; i++) { + var test = tests[i]; + html += '<tr class="overall-' + + status_class(test.format_status()) + + '">' + + '<td class="' + + status_class(test.format_status()) + + '">' + + test.format_status() + + "</td><td>" + + escape_html(test.name) + + "</td><td>" + + (assertions ? escape_html(get_assertion(test)) + "</td><td>" : "") + + escape_html(test.message ? tests[i].message : " ") + + (tests[i].stack ? "<pre>" + + escape_html(tests[i].stack) + + "</pre>": ""); + if (!(test instanceof RemoteTest)) { + html += "<details><summary>Asserts run</summary>" + get_asserts_output(test) + "</details>" + } + html += "</td></tr>"; + } + html += "</tbody></table>"; + try { + log.lastChild.innerHTML = html; + } catch (e) { + log.appendChild(document.createElementNS(xhtml_ns, "p")) + .textContent = "Setting innerHTML for the log threw an exception."; + log.appendChild(document.createElementNS(xhtml_ns, "pre")) + .textContent = html; + } + }; + + /* + * Template code + * + * A template is just a JavaScript structure. An element is represented as: + * + * [tag_name, {attr_name:attr_value}, child1, child2] + * + * the children can either be strings (which act like text nodes), other templates or + * functions (see below) + * + * A text node is represented as + * + * ["{text}", value] + * + * String values have a simple substitution syntax; ${foo} represents a variable foo. + * + * It is possible to embed logic in templates by using a function in a place where a + * node would usually go. The function must either return part of a template or null. + * + * In cases where a set of nodes are required as output rather than a single node + * with children it is possible to just use a list + * [node1, node2, node3] + * + * Usage: + * + * render(template, substitutions) - take a template and an object mapping + * variable names to parameters and return either a DOM node or a list of DOM nodes + * + * substitute(template, substitutions) - take a template and variable mapping object, + * make the variable substitutions and return the substituted template + * + */ + + function is_single_node(template) + { + return typeof template[0] === "string"; + } + + function substitute(template, substitutions) + { + if (typeof template === "function") { + var replacement = template(substitutions); + if (!replacement) { + return null; + } + + return substitute(replacement, substitutions); + } + + if (is_single_node(template)) { + return substitute_single(template, substitutions); + } + + return filter(map(template, function(x) { + return substitute(x, substitutions); + }), function(x) {return x !== null;}); + } + + function substitute_single(template, substitutions) + { + var substitution_re = /\$\{([^ }]*)\}/g; + + function do_substitution(input) { + var components = input.split(substitution_re); + var rv = []; + for (var i = 0; i < components.length; i += 2) { + rv.push(components[i]); + if (components[i + 1]) { + rv.push(String(substitutions[components[i + 1]])); + } + } + return rv; + } + + function substitute_attrs(attrs, rv) + { + rv[1] = {}; + for (var name in template[1]) { + if (attrs.hasOwnProperty(name)) { + var new_name = do_substitution(name).join(""); + var new_value = do_substitution(attrs[name]).join(""); + rv[1][new_name] = new_value; + } + } + } + + function substitute_children(children, rv) + { + for (var i = 0; i < children.length; i++) { + if (children[i] instanceof Object) { + var replacement = substitute(children[i], substitutions); + if (replacement !== null) { + if (is_single_node(replacement)) { + rv.push(replacement); + } else { + extend(rv, replacement); + } + } + } else { + extend(rv, do_substitution(String(children[i]))); + } + } + return rv; + } + + var rv = []; + rv.push(do_substitution(String(template[0])).join("")); + + if (template[0] === "{text}") { + substitute_children(template.slice(1), rv); + } else { + substitute_attrs(template[1], rv); + substitute_children(template.slice(2), rv); + } + + return rv; + } + + function make_dom_single(template, doc) + { + var output_document = doc || document; + var element; + if (template[0] === "{text}") { + element = output_document.createTextNode(""); + for (var i = 1; i < template.length; i++) { + element.data += template[i]; + } + } else { + element = output_document.createElementNS(xhtml_ns, template[0]); + for (var name in template[1]) { + if (template[1].hasOwnProperty(name)) { + element.setAttribute(name, template[1][name]); + } + } + for (var i = 2; i < template.length; i++) { + if (template[i] instanceof Object) { + var sub_element = make_dom(template[i]); + element.appendChild(sub_element); + } else { + var text_node = output_document.createTextNode(template[i]); + element.appendChild(text_node); + } + } + } + + return element; + } + + function make_dom(template, substitutions, output_document) + { + if (is_single_node(template)) { + return make_dom_single(template, output_document); + } + + return map(template, function(x) { + return make_dom_single(x, output_document); + }); + } + + function render(template, substitutions, output_document) + { + return make_dom(substitute(template, substitutions), output_document); + } + + /* + * Utility functions + */ + function assert(expected_true, function_name, description, error, substitutions) + { + if (expected_true !== true) { + var msg = make_message(function_name, description, + error, substitutions); + throw new AssertionError(msg); + } + } + + /** + * @class + * Exception type that represents a failing assert. + * + * @param {string} message - Error message. + */ + function AssertionError(message) + { + if (typeof message == "string") { + message = sanitize_unpaired_surrogates(message); + } + this.message = message; + this.stack = get_stack(); + } + expose(AssertionError, "AssertionError"); + + AssertionError.prototype = Object.create(Error.prototype); + + const get_stack = function() { + var stack = new Error().stack; + + // 'Error.stack' is not supported in all browsers/versions + if (!stack) { + return "(Stack trace unavailable)"; + } + + var lines = stack.split("\n"); + + // Create a pattern to match stack frames originating within testharness.js. These include the + // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21'). + // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + // in case it contains RegExp characters. + var script_url = get_script_url(); + var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js"; + var re = new RegExp(re_text + ":\\d+:\\d+"); + + // Some browsers include a preamble that specifies the type of the error object. Skip this by + // advancing until we find the first stack frame originating from testharness.js. + var i = 0; + while (!re.test(lines[i]) && i < lines.length) { + i++; + } + + // Then skip the top frames originating from testharness.js to begin the stack at the test code. + while (re.test(lines[i]) && i < lines.length) { + i++; + } + + // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified. + if (i >= lines.length) { + return stack; + } + + return lines.slice(i).join("\n"); + } + + function OptionalFeatureUnsupportedError(message) + { + AssertionError.call(this, message); + } + OptionalFeatureUnsupportedError.prototype = Object.create(AssertionError.prototype); + expose(OptionalFeatureUnsupportedError, "OptionalFeatureUnsupportedError"); + + function make_message(function_name, description, error, substitutions) + { + for (var p in substitutions) { + if (substitutions.hasOwnProperty(p)) { + substitutions[p] = format_value(substitutions[p]); + } + } + var node_form = substitute(["{text}", "${function_name}: ${description}" + error], + merge({function_name:function_name, + description:(description?description + " ":"")}, + substitutions)); + return node_form.slice(1).join(""); + } + + function filter(array, callable, thisObj) { + var rv = []; + for (var i = 0; i < array.length; i++) { + if (array.hasOwnProperty(i)) { + var pass = callable.call(thisObj, array[i], i, array); + if (pass) { + rv.push(array[i]); + } + } + } + return rv; + } + + function map(array, callable, thisObj) + { + var rv = []; + rv.length = array.length; + for (var i = 0; i < array.length; i++) { + if (array.hasOwnProperty(i)) { + rv[i] = callable.call(thisObj, array[i], i, array); + } + } + return rv; + } + + function extend(array, items) + { + Array.prototype.push.apply(array, items); + } + + function forEach(array, callback, thisObj) + { + for (var i = 0; i < array.length; i++) { + if (array.hasOwnProperty(i)) { + callback.call(thisObj, array[i], i, array); + } + } + } + + /** + * Immediately invoke a "iteratee" function with a series of values in + * parallel and invoke a final "done" function when all of the "iteratee" + * invocations have signaled completion. + * + * If all callbacks complete synchronously (or if no callbacks are + * specified), the ``done_callback`` will be invoked synchronously. It is the + * responsibility of the caller to ensure asynchronicity in cases where + * that is desired. + * + * @param {array} value Zero or more values to use in the invocation of + * ``iter_callback`` + * @param {function} iter_callback A function that will be invoked + * once for each of the values min + * ``value``. Two arguments will + * be available in each + * invocation: the value from + * ``value`` and a function that + * must be invoked to signal + * completion + * @param {function} done_callback A function that will be invoked after + * all operations initiated by the + * ``iter_callback`` function have signaled + * completion + */ + function all_async(values, iter_callback, done_callback) + { + var remaining = values.length; + + if (remaining === 0) { + done_callback(); + } + + forEach(values, + function(element) { + var invoked = false; + var elDone = function() { + if (invoked) { + return; + } + + invoked = true; + remaining -= 1; + + if (remaining === 0) { + done_callback(); + } + }; + + iter_callback(element, elDone); + }); + } + + function merge(a,b) + { + var rv = {}; + var p; + for (p in a) { + rv[p] = a[p]; + } + for (p in b) { + rv[p] = b[p]; + } + return rv; + } + + function expose(object, name) + { + var components = name.split("."); + var target = global_scope; + for (var i = 0; i < components.length - 1; i++) { + if (!(components[i] in target)) { + target[components[i]] = {}; + } + target = target[components[i]]; + } + target[components[components.length - 1]] = object; + } + + function is_same_origin(w) { + try { + 'random_prop' in w; + return true; + } catch (e) { + return false; + } + } + + /** Returns the 'src' URL of the first <script> tag in the page to include the file 'testharness.js'. */ + function get_script_url() + { + if (!('document' in global_scope)) { + return undefined; + } + + var scripts = document.getElementsByTagName("script"); + for (var i = 0; i < scripts.length; i++) { + var src; + if (scripts[i].src) { + src = scripts[i].src; + } else if (scripts[i].href) { + //SVG case + src = scripts[i].href.baseVal; + } + + var matches = src && src.match(/^(.*\/|)testharness\.js$/); + if (matches) { + return src; + } + } + return undefined; + } + + /** Returns the <title> or filename or "Untitled" */ + function get_title() + { + if ('document' in global_scope) { + //Don't use document.title to work around an Opera/Presto bug in XHTML documents + var title = document.getElementsByTagName("title")[0]; + if (title && title.firstChild && title.firstChild.data) { + return title.firstChild.data; + } + } + if ('META_TITLE' in global_scope && META_TITLE) { + return META_TITLE; + } + if ('location' in global_scope && 'pathname' in location) { + return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.')); + } + return "Untitled"; + } + + /** Fetches a JSON resource and parses it */ + async function fetch_json(resource) { + const response = await fetch(resource); + return await response.json(); + } + if (!global_scope.GLOBAL || !global_scope.GLOBAL.isShadowRealm()) { + expose(fetch_json, 'fetch_json'); + } + + /** + * Setup globals + */ + + var tests = new Tests(); + + if (global_scope.addEventListener) { + var error_handler = function(error, message, stack) { + var optional_unsupported = error instanceof OptionalFeatureUnsupportedError; + if (tests.file_is_test) { + var test = tests.tests[0]; + if (test.phase >= test.phases.HAS_RESULT) { + return; + } + var status = optional_unsupported ? test.PRECONDITION_FAILED : test.FAIL; + test.set_status(status, message, stack); + test.phase = test.phases.HAS_RESULT; + } else if (!tests.allow_uncaught_exception) { + var status = optional_unsupported ? tests.status.PRECONDITION_FAILED : tests.status.ERROR; + tests.status.status = status; + tests.status.message = message; + tests.status.stack = stack; + } + + // Do not transition to the "complete" phase if the test has been + // configured to allow uncaught exceptions. This gives the test an + // opportunity to define subtests based on the exception reporting + // behavior. + if (!tests.allow_uncaught_exception) { + done(); + } + }; + + addEventListener("error", function(e) { + var message = e.message; + var stack; + if (e.error && e.error.stack) { + stack = e.error.stack; + } else { + stack = e.filename + ":" + e.lineno + ":" + e.colno; + } + error_handler(e.error, message, stack); + }, false); + + addEventListener("unhandledrejection", function(e) { + var message; + if (e.reason && e.reason.message) { + message = "Unhandled rejection: " + e.reason.message; + } else { + message = "Unhandled rejection"; + } + var stack; + if (e.reason && e.reason.stack) { + stack = e.reason.stack; + } + error_handler(e.reason, message, stack); + }, false); + } + + test_environment.on_tests_ready(); + + /** + * Stylesheet + */ + var stylesheetContent = "\ +html {\ + font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans;\ +}\ +\ +#log .warning,\ +#log .warning a {\ + color: black;\ + background: yellow;\ +}\ +\ +#log .error,\ +#log .error a {\ + color: white;\ + background: red;\ +}\ +\ +section#summary {\ + margin-bottom:1em;\ +}\ +\ +table#results {\ + border-collapse:collapse;\ + table-layout:fixed;\ + width:100%;\ +}\ +\ +table#results > thead > tr > th:first-child,\ +table#results > tbody > tr > td:first-child {\ + width:8em;\ +}\ +\ +table#results > thead > tr > th:last-child,\ +table#results > thead > tr > td:last-child {\ + width:50%;\ +}\ +\ +table#results.assertions > thead > tr > th:last-child,\ +table#results.assertions > tbody > tr > td:last-child {\ + width:35%;\ +}\ +\ +table#results > thead > > tr > th {\ + padding:0;\ + padding-bottom:0.5em;\ + border-bottom:medium solid black;\ +}\ +\ +table#results > tbody > tr> td {\ + padding:1em;\ + padding-bottom:0.5em;\ + border-bottom:thin solid black;\ +}\ +\ +.pass {\ + color:green;\ +}\ +\ +.fail {\ + color:red;\ +}\ +\ +tr.timeout {\ + color:red;\ +}\ +\ +tr.notrun {\ + color:blue;\ +}\ +\ +tr.optionalunsupported {\ + color:blue;\ +}\ +\ +.ok {\ + color:green;\ +}\ +\ +.error {\ + color:red;\ +}\ +\ +.pass, .fail, .timeout, .notrun, .optionalunsupported .ok, .timeout, .error {\ + font-variant:small-caps;\ +}\ +\ +table#results span {\ + display:block;\ +}\ +\ +table#results span.expected {\ + font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\ + white-space:pre;\ +}\ +\ +table#results span.actual {\ + font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace;\ + white-space:pre;\ +}\ +"; + +})(self); +// vim: set expandtab shiftwidth=4 tabstop=4: diff --git a/testing/web-platform/tests/resources/testharness.js.headers b/testing/web-platform/tests/resources/testharness.js.headers new file mode 100644 index 0000000000..5e8f640c66 --- /dev/null +++ b/testing/web-platform/tests/resources/testharness.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript; charset=utf-8 +Cache-Control: max-age=3600 diff --git a/testing/web-platform/tests/resources/testharnessreport.js b/testing/web-platform/tests/resources/testharnessreport.js new file mode 100644 index 0000000000..e5cb40fe0e --- /dev/null +++ b/testing/web-platform/tests/resources/testharnessreport.js @@ -0,0 +1,57 @@ +/* global add_completion_callback */ +/* global setup */ + +/* + * This file is intended for vendors to implement code needed to integrate + * testharness.js tests with their own test systems. + * + * Typically test system integration will attach callbacks when each test has + * run, using add_result_callback(callback(test)), or when the whole test file + * has completed, using + * add_completion_callback(callback(tests, harness_status)). + * + * For more documentation about the callback functions and the + * parameters they are called with see testharness.js + */ + +function dump_test_results(tests, status) { + var results_element = document.createElement("script"); + results_element.type = "text/json"; + results_element.id = "__testharness__results__"; + var test_results = tests.map(function(x) { + return {name:x.name, status:x.status, message:x.message, stack:x.stack} + }); + var data = {test:window.location.href, + tests:test_results, + status: status.status, + message: status.message, + stack: status.stack}; + results_element.textContent = JSON.stringify(data); + + // To avoid a HierarchyRequestError with XML documents, ensure that 'results_element' + // is inserted at a location that results in a valid document. + var parent = document.body + ? document.body // <body> is required in XHTML documents + : document.documentElement; // fallback for optional <body> in HTML5, SVG, etc. + + parent.appendChild(results_element); +} + +add_completion_callback(dump_test_results); + +/* If the parent window has a testharness_properties object, + * we use this to provide the test settings. This is used by the + * default in-browser runner to configure the timeout and the + * rendering of results + */ +try { + if (window.opener && "testharness_properties" in window.opener) { + /* If we pass the testharness_properties object as-is here without + * JSON stringifying and reparsing it, IE fails & emits the message + * "Could not complete the operation due to error 80700019". + */ + setup(JSON.parse(JSON.stringify(window.opener.testharness_properties))); + } +} catch (e) { +} +// vim: set expandtab shiftwidth=4 tabstop=4: diff --git a/testing/web-platform/tests/resources/testharnessreport.js.headers b/testing/web-platform/tests/resources/testharnessreport.js.headers new file mode 100644 index 0000000000..5e8f640c66 --- /dev/null +++ b/testing/web-platform/tests/resources/testharnessreport.js.headers @@ -0,0 +1,2 @@ +Content-Type: text/javascript; charset=utf-8 +Cache-Control: max-age=3600 diff --git a/testing/web-platform/tests/resources/webidl2/build.sh b/testing/web-platform/tests/resources/webidl2/build.sh new file mode 100755 index 0000000000..a631268224 --- /dev/null +++ b/testing/web-platform/tests/resources/webidl2/build.sh @@ -0,0 +1,12 @@ +set -ex + +if [ ! -d "webidl2.js" ]; then + git clone https://github.com/w3c/webidl2.js.git +fi +cd webidl2.js +npm install +npm run build-debug +HASH=$(git rev-parse HEAD) +cd .. +cp webidl2.js/dist/webidl2.js lib/ +echo "Currently using webidl2.js@${HASH}." > lib/VERSION.md diff --git a/testing/web-platform/tests/resources/webidl2/lib/README.md b/testing/web-platform/tests/resources/webidl2/lib/README.md new file mode 100644 index 0000000000..1bd583269d --- /dev/null +++ b/testing/web-platform/tests/resources/webidl2/lib/README.md @@ -0,0 +1,4 @@ +This directory contains a built version of the [webidl2.js library](https://github.com/w3c/webidl2.js). +It is built by running `npm run build-debug` at the root of that repository. + +The `webidl2.js.headers` file is a local addition to ensure the script is interpreted as UTF-8. diff --git a/testing/web-platform/tests/resources/webidl2/lib/VERSION.md b/testing/web-platform/tests/resources/webidl2/lib/VERSION.md new file mode 100644 index 0000000000..5a3726c6c0 --- /dev/null +++ b/testing/web-platform/tests/resources/webidl2/lib/VERSION.md @@ -0,0 +1 @@ +Currently using webidl2.js@6889aee6fc7d65915ab1267825248157dbc50486. diff --git a/testing/web-platform/tests/resources/webidl2/lib/webidl2.js b/testing/web-platform/tests/resources/webidl2/lib/webidl2.js new file mode 100644 index 0000000000..7161def899 --- /dev/null +++ b/testing/web-platform/tests/resources/webidl2/lib/webidl2.js @@ -0,0 +1,3824 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); + else if(typeof exports === 'object') + exports["WebIDL2"] = factory(); + else + root["WebIDL2"] = factory(); +})(globalThis, () => { +return /******/ (() => { // webpackBootstrap +/******/ "use strict"; +/******/ var __webpack_modules__ = ([ +/* 0 */, +/* 1 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "parse": () => (/* binding */ parse) +/* harmony export */ }); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); +/* harmony import */ var _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(15); +/* harmony import */ var _productions_includes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(16); +/* harmony import */ var _productions_extended_attributes_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8); +/* harmony import */ var _productions_typedef_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(17); +/* harmony import */ var _productions_callback_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(18); +/* harmony import */ var _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(19); +/* harmony import */ var _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(25); +/* harmony import */ var _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(26); +/* harmony import */ var _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(28); +/* harmony import */ var _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(29); +/* harmony import */ var _productions_helpers_js__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(4); +/* harmony import */ var _productions_token_js__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(10); + + + + + + + + + + + + + + +/** + * @param {Tokeniser} tokeniser + * @param {object} options + * @param {boolean} [options.concrete] + * @param {Function[]} [options.productions] + */ +function parseByTokens(tokeniser, options) { + const source = tokeniser.source; + + function error(str) { + tokeniser.error(str); + } + + function consume(...candidates) { + return tokeniser.consume(...candidates); + } + + function callback() { + const callback = consume("callback"); + if (!callback) return; + if (tokeniser.probe("interface")) { + return _productions_callback_interface_js__WEBPACK_IMPORTED_MODULE_10__.CallbackInterface.parse(tokeniser, callback); + } + return _productions_callback_js__WEBPACK_IMPORTED_MODULE_5__.CallbackFunction.parse(tokeniser, callback); + } + + function interface_(opts) { + const base = consume("interface"); + if (!base) return; + const ret = + _productions_mixin_js__WEBPACK_IMPORTED_MODULE_7__.Mixin.parse(tokeniser, base, opts) || + _productions_interface_js__WEBPACK_IMPORTED_MODULE_6__.Interface.parse(tokeniser, base, opts) || + error("Interface has no proper body"); + return ret; + } + + function partial() { + const partial = consume("partial"); + if (!partial) return; + return ( + _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser, { partial }) || + interface_({ partial }) || + _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser, { partial }) || + error("Partial doesn't apply to anything") + ); + } + + function definition() { + if (options.productions) { + for (const production of options.productions) { + const result = production(tokeniser); + if (result) { + return result; + } + } + } + + return ( + callback() || + interface_() || + partial() || + _productions_dictionary_js__WEBPACK_IMPORTED_MODULE_8__.Dictionary.parse(tokeniser) || + _productions_enum_js__WEBPACK_IMPORTED_MODULE_1__.Enum.parse(tokeniser) || + _productions_typedef_js__WEBPACK_IMPORTED_MODULE_4__.Typedef.parse(tokeniser) || + _productions_includes_js__WEBPACK_IMPORTED_MODULE_2__.Includes.parse(tokeniser) || + _productions_namespace_js__WEBPACK_IMPORTED_MODULE_9__.Namespace.parse(tokeniser) + ); + } + + function definitions() { + if (!source.length) return []; + const defs = []; + while (true) { + const ea = _productions_extended_attributes_js__WEBPACK_IMPORTED_MODULE_3__.ExtendedAttributes.parse(tokeniser); + const def = definition(); + if (!def) { + if (ea.length) error("Stray extended attributes"); + break; + } + (0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_11__.autoParenter)(def).extAttrs = ea; + defs.push(def); + } + const eof = _productions_token_js__WEBPACK_IMPORTED_MODULE_12__.Eof.parse(tokeniser); + if (options.concrete) { + defs.push(eof); + } + return defs; + } + const res = definitions(); + if (tokeniser.position < source.length) error("Unrecognised tokens"); + return res; +} + +/** + * @param {string} str + * @param {object} [options] + * @param {*} [options.sourceName] + * @param {boolean} [options.concrete] + * @param {Function[]} [options.productions] + * @return {import("./productions/base.js").Base[]} + */ +function parse(str, options = {}) { + const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__.Tokeniser(str); + if (typeof options.sourceName !== "undefined") { + // @ts-ignore (See Tokeniser.source in supplement.d.ts) + tokeniser.source.name = options.sourceName; + } + return parseByTokens(tokeniser, options); +} + + +/***/ }), +/* 2 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Tokeniser": () => (/* binding */ Tokeniser), +/* harmony export */ "WebIDLParseError": () => (/* binding */ WebIDLParseError), +/* harmony export */ "argumentNameKeywords": () => (/* binding */ argumentNameKeywords), +/* harmony export */ "stringTypes": () => (/* binding */ stringTypes), +/* harmony export */ "typeNameKeywords": () => (/* binding */ typeNameKeywords) +/* harmony export */ }); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); +/* harmony import */ var _productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +// These regular expressions use the sticky flag so they will only match at +// the current location (ie. the offset of lastIndex). +const tokenRe = { + // This expression uses a lookahead assertion to catch false matches + // against integers early. + decimal: + /-?(?=[0-9]*\.|[0-9]+[eE])(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/y, + integer: /-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/y, + identifier: /[_-]?[A-Za-z][0-9A-Z_a-z-]*/y, + string: /"[^"]*"/y, + whitespace: /[\t\n\r ]+/y, + comment: /\/\/.*|\/\*[\s\S]*?\*\//y, + other: /[^\t\n\r 0-9A-Za-z]/y, +}; + +const typeNameKeywords = [ + "ArrayBuffer", + "DataView", + "Int8Array", + "Int16Array", + "Int32Array", + "Uint8Array", + "Uint16Array", + "Uint32Array", + "Uint8ClampedArray", + "BigInt64Array", + "BigUint64Array", + "Float32Array", + "Float64Array", + "any", + "object", + "symbol", +]; + +const stringTypes = ["ByteString", "DOMString", "USVString"]; + +const argumentNameKeywords = [ + "async", + "attribute", + "callback", + "const", + "constructor", + "deleter", + "dictionary", + "enum", + "getter", + "includes", + "inherit", + "interface", + "iterable", + "maplike", + "namespace", + "partial", + "required", + "setlike", + "setter", + "static", + "stringifier", + "typedef", + "unrestricted", +]; + +const nonRegexTerminals = [ + "-Infinity", + "FrozenArray", + "Infinity", + "NaN", + "ObservableArray", + "Promise", + "bigint", + "boolean", + "byte", + "double", + "false", + "float", + "long", + "mixin", + "null", + "octet", + "optional", + "or", + "readonly", + "record", + "sequence", + "short", + "true", + "undefined", + "unsigned", + "void", +].concat(argumentNameKeywords, stringTypes, typeNameKeywords); + +const punctuations = [ + "(", + ")", + ",", + "...", + ":", + ";", + "<", + "=", + ">", + "?", + "*", + "[", + "]", + "{", + "}", +]; + +const reserved = [ + // "constructor" is now a keyword + "_constructor", + "toString", + "_toString", +]; + +/** + * @typedef {ArrayItemType<ReturnType<typeof tokenise>>} Token + * @param {string} str + */ +function tokenise(str) { + const tokens = []; + let lastCharIndex = 0; + let trivia = ""; + let line = 1; + let index = 0; + while (lastCharIndex < str.length) { + const nextChar = str.charAt(lastCharIndex); + let result = -1; + + if (/[\t\n\r ]/.test(nextChar)) { + result = attemptTokenMatch("whitespace", { noFlushTrivia: true }); + } else if (nextChar === "/") { + result = attemptTokenMatch("comment", { noFlushTrivia: true }); + } + + if (result !== -1) { + const currentTrivia = tokens.pop().value; + line += (currentTrivia.match(/\n/g) || []).length; + trivia += currentTrivia; + index -= 1; + } else if (/[-0-9.A-Z_a-z]/.test(nextChar)) { + result = attemptTokenMatch("decimal"); + if (result === -1) { + result = attemptTokenMatch("integer"); + } + if (result === -1) { + result = attemptTokenMatch("identifier"); + const lastIndex = tokens.length - 1; + const token = tokens[lastIndex]; + if (result !== -1) { + if (reserved.includes(token.value)) { + const message = `${(0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)( + token.value + )} is a reserved identifier and must not be used.`; + throw new WebIDLParseError( + (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(tokens, lastIndex, null, message) + ); + } else if (nonRegexTerminals.includes(token.value)) { + token.type = "inline"; + } + } + } + } else if (nextChar === '"') { + result = attemptTokenMatch("string"); + } + + for (const punctuation of punctuations) { + if (str.startsWith(punctuation, lastCharIndex)) { + tokens.push({ + type: "inline", + value: punctuation, + trivia, + line, + index, + }); + trivia = ""; + lastCharIndex += punctuation.length; + result = lastCharIndex; + break; + } + } + + // other as the last try + if (result === -1) { + result = attemptTokenMatch("other"); + } + if (result === -1) { + throw new Error("Token stream not progressing"); + } + lastCharIndex = result; + index += 1; + } + + // remaining trivia as eof + tokens.push({ + type: "eof", + value: "", + trivia, + line, + index, + }); + + return tokens; + + /** + * @param {keyof typeof tokenRe} type + * @param {object} options + * @param {boolean} [options.noFlushTrivia] + */ + function attemptTokenMatch(type, { noFlushTrivia } = {}) { + const re = tokenRe[type]; + re.lastIndex = lastCharIndex; + const result = re.exec(str); + if (result) { + tokens.push({ type, value: result[0], trivia, line, index }); + if (!noFlushTrivia) { + trivia = ""; + } + return re.lastIndex; + } + return -1; + } +} + +class Tokeniser { + /** + * @param {string} idl + */ + constructor(idl) { + this.source = tokenise(idl); + this.position = 0; + } + + /** + * @param {string} message + * @return {never} + */ + error(message) { + throw new WebIDLParseError( + (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.syntaxError)(this.source, this.position, this.current, message) + ); + } + + /** + * @param {string} type + */ + probeKind(type) { + return ( + this.source.length > this.position && + this.source[this.position].type === type + ); + } + + /** + * @param {string} value + */ + probe(value) { + return ( + this.probeKind("inline") && this.source[this.position].value === value + ); + } + + /** + * @param {...string} candidates + */ + consumeKind(...candidates) { + for (const type of candidates) { + if (!this.probeKind(type)) continue; + const token = this.source[this.position]; + this.position++; + return token; + } + } + + /** + * @param {...string} candidates + */ + consume(...candidates) { + if (!this.probeKind("inline")) return; + const token = this.source[this.position]; + for (const value of candidates) { + if (token.value !== value) continue; + this.position++; + return token; + } + } + + /** + * @param {string} value + */ + consumeIdentifier(value) { + if (!this.probeKind("identifier")) { + return; + } + if (this.source[this.position].value !== value) { + return; + } + return this.consumeKind("identifier"); + } + + /** + * @param {number} position + */ + unconsume(position) { + this.position = position; + } +} + +class WebIDLParseError extends Error { + /** + * @param {object} options + * @param {string} options.message + * @param {string} options.bareMessage + * @param {string} options.context + * @param {number} options.line + * @param {*} options.sourceName + * @param {string} options.input + * @param {*[]} options.tokens + */ + constructor({ + message, + bareMessage, + context, + line, + sourceName, + input, + tokens, + }) { + super(message); + + this.name = "WebIDLParseError"; // not to be mangled + this.bareMessage = bareMessage; + this.context = context; + this.line = line; + this.sourceName = sourceName; + this.input = input; + this.tokens = tokens; + } +} + + +/***/ }), +/* 3 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "syntaxError": () => (/* binding */ syntaxError), +/* harmony export */ "validationError": () => (/* binding */ validationError) +/* harmony export */ }); +/** + * @param {string} text + */ +function lastLine(text) { + const splitted = text.split("\n"); + return splitted[splitted.length - 1]; +} + +function appendIfExist(base, target) { + let result = base; + if (target) { + result += ` ${target}`; + } + return result; +} + +function contextAsText(node) { + const hierarchy = [node]; + while (node && node.parent) { + const { parent } = node; + hierarchy.unshift(parent); + node = parent; + } + return hierarchy.map((n) => appendIfExist(n.type, n.name)).join(" -> "); +} + +/** + * @typedef {object} WebIDL2ErrorOptions + * @property {"error" | "warning"} [level] + * @property {Function} [autofix] + * @property {string} [ruleName] + * + * @typedef {ReturnType<typeof error>} WebIDLErrorData + * + * @param {string} message error message + * @param {*} position + * @param {*} current + * @param {*} message + * @param {"Syntax" | "Validation"} kind error type + * @param {WebIDL2ErrorOptions=} options + */ +function error( + source, + position, + current, + message, + kind, + { level = "error", autofix, ruleName } = {} +) { + /** + * @param {number} count + */ + function sliceTokens(count) { + return count > 0 + ? source.slice(position, position + count) + : source.slice(Math.max(position + count, 0), position); + } + + /** + * @param {import("./tokeniser.js").Token[]} inputs + * @param {object} [options] + * @param {boolean} [options.precedes] + * @returns + */ + function tokensToText(inputs, { precedes } = {}) { + const text = inputs.map((t) => t.trivia + t.value).join(""); + const nextToken = source[position]; + if (nextToken.type === "eof") { + return text; + } + if (precedes) { + return text + nextToken.trivia; + } + return text.slice(nextToken.trivia.length); + } + + const maxTokens = 5; // arbitrary but works well enough + const line = + source[position].type !== "eof" + ? source[position].line + : source.length > 1 + ? source[position - 1].line + : 1; + + const precedingLastLine = lastLine( + tokensToText(sliceTokens(-maxTokens), { precedes: true }) + ); + + const subsequentTokens = sliceTokens(maxTokens); + const subsequentText = tokensToText(subsequentTokens); + const subsequentFirstLine = subsequentText.split("\n")[0]; + + const spaced = " ".repeat(precedingLastLine.length) + "^"; + const sourceContext = precedingLastLine + subsequentFirstLine + "\n" + spaced; + + const contextType = kind === "Syntax" ? "since" : "inside"; + const inSourceName = source.name ? ` in ${source.name}` : ""; + const grammaticalContext = + current && current.name + ? `, ${contextType} \`${current.partial ? "partial " : ""}${contextAsText( + current + )}\`` + : ""; + const context = `${kind} error at line ${line}${inSourceName}${grammaticalContext}:\n${sourceContext}`; + return { + message: `${context} ${message}`, + bareMessage: message, + context, + line, + sourceName: source.name, + level, + ruleName, + autofix, + input: subsequentText, + tokens: subsequentTokens, + }; +} + +/** + * @param {string} message error message + */ +function syntaxError(source, position, current, message) { + return error(source, position, current, message, "Syntax"); +} + +/** + * @param {string} message error message + * @param {WebIDL2ErrorOptions} [options] + */ +function validationError( + token, + current, + ruleName, + message, + options = {} +) { + options.ruleName = ruleName; + return error( + current.source, + token.index, + current, + message, + "Validation", + options + ); +} + + +/***/ }), +/* 4 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "argument_list": () => (/* binding */ argument_list), +/* harmony export */ "autoParenter": () => (/* binding */ autoParenter), +/* harmony export */ "autofixAddExposedWindow": () => (/* binding */ autofixAddExposedWindow), +/* harmony export */ "const_data": () => (/* binding */ const_data), +/* harmony export */ "const_value": () => (/* binding */ const_value), +/* harmony export */ "findLastIndex": () => (/* binding */ findLastIndex), +/* harmony export */ "getFirstToken": () => (/* binding */ getFirstToken), +/* harmony export */ "getLastIndentation": () => (/* binding */ getLastIndentation), +/* harmony export */ "getMemberIndentation": () => (/* binding */ getMemberIndentation), +/* harmony export */ "list": () => (/* binding */ list), +/* harmony export */ "primitive_type": () => (/* binding */ primitive_type), +/* harmony export */ "return_type": () => (/* binding */ return_type), +/* harmony export */ "stringifier": () => (/* binding */ stringifier), +/* harmony export */ "type_with_extended_attributes": () => (/* binding */ type_with_extended_attributes), +/* harmony export */ "unescape": () => (/* binding */ unescape) +/* harmony export */ }); +/* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5); +/* harmony import */ var _argument_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(11); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13); +/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(14); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(2); + + + + + + + +/** + * @param {string} identifier + */ +function unescape(identifier) { + return identifier.startsWith("_") ? identifier.slice(1) : identifier; +} + +/** + * Parses comma-separated list + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} args + * @param {Function} args.parser parser function for each item + * @param {boolean} [args.allowDangler] whether to allow dangling comma + * @param {string} [args.listName] the name to be shown on error messages + */ +function list(tokeniser, { parser, allowDangler, listName = "list" }) { + const first = parser(tokeniser); + if (!first) { + return []; + } + first.tokens.separator = tokeniser.consume(","); + const items = [first]; + while (first.tokens.separator) { + const item = parser(tokeniser); + if (!item) { + if (!allowDangler) { + tokeniser.error(`Trailing comma in ${listName}`); + } + break; + } + item.tokens.separator = tokeniser.consume(","); + items.push(item); + if (!item.tokens.separator) break; + } + return items; +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function const_value(tokeniser) { + return ( + tokeniser.consumeKind("decimal", "integer") || + tokeniser.consume("true", "false", "Infinity", "-Infinity", "NaN") + ); +} + +/** + * @param {object} token + * @param {string} token.type + * @param {string} token.value + */ +function const_data({ type, value }) { + switch (type) { + case "decimal": + case "integer": + return { type: "number", value }; + case "string": + return { type: "string", value: value.slice(1, -1) }; + } + + switch (value) { + case "true": + case "false": + return { type: "boolean", value: value === "true" }; + case "Infinity": + case "-Infinity": + return { type: "Infinity", negative: value.startsWith("-") }; + case "[": + return { type: "sequence", value: [] }; + case "{": + return { type: "dictionary" }; + default: + return { type: value }; + } +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function primitive_type(tokeniser) { + function integer_type() { + const prefix = tokeniser.consume("unsigned"); + const base = tokeniser.consume("short", "long"); + if (base) { + const postfix = tokeniser.consume("long"); + return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { prefix, base, postfix } }); + } + if (prefix) tokeniser.error("Failed to parse integer type"); + } + + function decimal_type() { + const prefix = tokeniser.consume("unrestricted"); + const base = tokeniser.consume("float", "double"); + if (base) { + return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { prefix, base } }); + } + if (prefix) tokeniser.error("Failed to parse float type"); + } + + const { source } = tokeniser; + const num_type = integer_type() || decimal_type(); + if (num_type) return num_type; + const base = tokeniser.consume( + "bigint", + "boolean", + "byte", + "octet", + "undefined" + ); + if (base) { + return new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ source, tokens: { base } }); + } +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function argument_list(tokeniser) { + return list(tokeniser, { + parser: _argument_js__WEBPACK_IMPORTED_MODULE_1__.Argument.parse, + listName: "arguments list", + }); +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string=} typeName (TODO: See Type.type for more details) + */ +function type_with_extended_attributes(tokeniser, typeName) { + const extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser); + const ret = _type_js__WEBPACK_IMPORTED_MODULE_0__.Type.parse(tokeniser, typeName); + if (ret) autoParenter(ret).extAttrs = extAttrs; + return ret; +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string=} typeName (TODO: See Type.type for more details) + */ +function return_type(tokeniser, typeName) { + const typ = _type_js__WEBPACK_IMPORTED_MODULE_0__.Type.parse(tokeniser, typeName || "return-type"); + if (typ) { + return typ; + } + const voidToken = tokeniser.consume("void"); + if (voidToken) { + const ret = new _type_js__WEBPACK_IMPORTED_MODULE_0__.Type({ + source: tokeniser.source, + tokens: { base: voidToken }, + }); + ret.type = "return-type"; + return ret; + } +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function stringifier(tokeniser) { + const special = tokeniser.consume("stringifier"); + if (!special) return; + const member = + _attribute_js__WEBPACK_IMPORTED_MODULE_4__.Attribute.parse(tokeniser, { special }) || + _operation_js__WEBPACK_IMPORTED_MODULE_3__.Operation.parse(tokeniser, { special }) || + tokeniser.error("Unterminated stringifier"); + return member; +} + +/** + * @param {string} str + */ +function getLastIndentation(str) { + const lines = str.split("\n"); + // the first line visually binds to the preceding token + if (lines.length) { + const match = lines[lines.length - 1].match(/^\s+/); + if (match) { + return match[0]; + } + } + return ""; +} + +/** + * @param {string} parentTrivia + */ +function getMemberIndentation(parentTrivia) { + const indentation = getLastIndentation(parentTrivia); + const indentCh = indentation.includes("\t") ? "\t" : " "; + return indentation + indentCh; +} + +/** + * @param {import("./interface.js").Interface} def + */ +function autofixAddExposedWindow(def) { + return () => { + if (def.extAttrs.length) { + const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("Exposed=Window,"); + const exposed = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.SimpleExtendedAttribute.parse(tokeniser); + exposed.tokens.separator = tokeniser.consume(","); + const existing = def.extAttrs[0]; + if (!/^\s/.test(existing.tokens.name.trivia)) { + existing.tokens.name.trivia = ` ${existing.tokens.name.trivia}`; + } + def.extAttrs.unshift(exposed); + } else { + autoParenter(def).extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse( + new _tokeniser_js__WEBPACK_IMPORTED_MODULE_5__.Tokeniser("[Exposed=Window]") + ); + const trivia = def.tokens.base.trivia; + def.extAttrs.tokens.open.trivia = trivia; + def.tokens.base.trivia = `\n${getLastIndentation(trivia)}`; + } + }; +} + +/** + * Get the first syntax token for the given IDL object. + * @param {*} data + */ +function getFirstToken(data) { + if (data.extAttrs.length) { + return data.extAttrs.tokens.open; + } + if (data.type === "operation" && !data.special) { + return getFirstToken(data.idlType); + } + const tokens = Object.values(data.tokens).sort((x, y) => x.index - y.index); + return tokens[0]; +} + +/** + * @template T + * @param {T[]} array + * @param {(item: T) => boolean} predicate + */ +function findLastIndex(array, predicate) { + const index = array.slice().reverse().findIndex(predicate); + if (index === -1) { + return index; + } + return array.length - index - 1; +} + +/** + * Returns a proxy that auto-assign `parent` field. + * @template {Record<string | symbol, any>} T + * @param {T} data + * @param {*} [parent] The object that will be assigned to `parent`. + * If absent, it will be `data` by default. + * @return {T} + */ +function autoParenter(data, parent) { + if (!parent) { + // Defaults to `data` unless specified otherwise. + parent = data; + } + if (!data) { + // This allows `autoParenter(undefined)` which again allows + // `autoParenter(parse())` where the function may return nothing. + return data; + } + const proxy = new Proxy(data, { + get(target, p) { + const value = target[p]; + if (Array.isArray(value) && p !== "source") { + // Wraps the array so that any added items will also automatically + // get their `parent` values. + return autoParenter(value, target); + } + return value; + }, + set(target, p, value) { + // @ts-ignore https://github.com/microsoft/TypeScript/issues/47357 + target[p] = value; + if (!value) { + return true; + } else if (Array.isArray(value)) { + // Assigning an array will add `parent` to its items. + for (const item of value) { + if (typeof item.parent !== "undefined") { + item.parent = parent; + } + } + } else if (typeof value.parent !== "undefined") { + value.parent = parent; + } + return true; + }, + }); + return proxy; +} + + +/***/ }), +/* 5 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Type": () => (/* binding */ Type) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3); +/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(8); + + + + + + + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} typeName + */ +function generic_type(tokeniser, typeName) { + const base = tokeniser.consume( + "FrozenArray", + "ObservableArray", + "Promise", + "sequence", + "record" + ); + if (!base) { + return; + } + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( + new Type({ source: tokeniser.source, tokens: { base } }) + ); + ret.tokens.open = + tokeniser.consume("<") || + tokeniser.error(`No opening bracket after ${base.value}`); + switch (base.value) { + case "Promise": { + if (tokeniser.probe("[")) + tokeniser.error("Promise type cannot have extended attribute"); + const subtype = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser, typeName) || + tokeniser.error("Missing Promise subtype"); + ret.subtype.push(subtype); + break; + } + case "sequence": + case "FrozenArray": + case "ObservableArray": { + const subtype = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, typeName) || + tokeniser.error(`Missing ${base.value} subtype`); + ret.subtype.push(subtype); + break; + } + case "record": { + if (tokeniser.probe("[")) + tokeniser.error("Record key cannot have extended attribute"); + const keyType = + tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes) || + tokeniser.error(`Record key must be one of: ${_tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes.join(", ")}`); + const keyIdlType = new Type({ + source: tokeniser.source, + tokens: { base: keyType }, + }); + keyIdlType.tokens.separator = + tokeniser.consume(",") || + tokeniser.error("Missing comma after record key type"); + keyIdlType.type = typeName; + const valueType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, typeName) || + tokeniser.error("Error parsing generic type record"); + ret.subtype.push(keyIdlType, valueType); + break; + } + } + if (!ret.idlType) tokeniser.error(`Error parsing generic type ${base.value}`); + ret.tokens.close = + tokeniser.consume(">") || + tokeniser.error(`Missing closing bracket after ${base.value}`); + return ret.this; +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function type_suffix(tokeniser, obj) { + const nullable = tokeniser.consume("?"); + if (nullable) { + obj.tokens.nullable = nullable; + } + if (tokeniser.probe("?")) tokeniser.error("Can't nullable more than once"); +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} typeName + */ +function single_type(tokeniser, typeName) { + let ret = generic_type(tokeniser, typeName) || (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.primitive_type)(tokeniser); + if (!ret) { + const base = + tokeniser.consumeKind("identifier") || + tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes, ..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.typeNameKeywords); + if (!base) { + return; + } + ret = new Type({ source: tokeniser.source, tokens: { base } }); + if (tokeniser.probe("<")) + tokeniser.error(`Unsupported generic type ${base.value}`); + } + if (ret.generic === "Promise" && tokeniser.probe("?")) { + tokeniser.error("Promise type cannot be nullable"); + } + ret.type = typeName || null; + type_suffix(tokeniser, ret); + if (ret.nullable && ret.idlType === "any") + tokeniser.error("Type `any` cannot be made nullable"); + return ret; +} + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} type + */ +function union_type(tokeniser, type) { + const tokens = {}; + tokens.open = tokeniser.consume("("); + if (!tokens.open) return; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Type({ source: tokeniser.source, tokens })); + ret.type = type || null; + while (true) { + const typ = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser) || + tokeniser.error("No type after open parenthesis or 'or' in union type"); + if (typ.idlType === "any") + tokeniser.error("Type `any` cannot be included in a union type"); + if (typ.generic === "Promise") + tokeniser.error("Type `Promise` cannot be included in a union type"); + ret.subtype.push(typ); + const or = tokeniser.consume("or"); + if (or) { + typ.tokens.separator = or; + } else break; + } + if (ret.idlType.length < 2) { + tokeniser.error( + "At least two types are expected in a union type but found less" + ); + } + tokens.close = + tokeniser.consume(")") || tokeniser.error("Unterminated union type"); + type_suffix(tokeniser, ret); + return ret.this; +} + +class Type extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} typeName + */ + static parse(tokeniser, typeName) { + return single_type(tokeniser, typeName) || union_type(tokeniser, typeName); + } + + constructor({ source, tokens }) { + super({ source, tokens }); + Object.defineProperty(this, "subtype", { value: [], writable: true }); + this.extAttrs = new _extended_attributes_js__WEBPACK_IMPORTED_MODULE_5__.ExtendedAttributes({ source, tokens: {} }); + } + + get generic() { + if (this.subtype.length && this.tokens.base) { + return this.tokens.base.value; + } + return ""; + } + get nullable() { + return Boolean(this.tokens.nullable); + } + get union() { + return Boolean(this.subtype.length) && !this.tokens.base; + } + get idlType() { + if (this.subtype.length) { + return this.subtype; + } + // Adding prefixes/postfixes for "unrestricted float", etc. + const name = [this.tokens.prefix, this.tokens.base, this.tokens.postfix] + .filter((t) => t) + .map((t) => t.value) + .join(" "); + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(name); + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + + if (this.idlType === "void") { + const message = `\`void\` is now replaced by \`undefined\`. Refer to the \ +[relevant GitHub issue](https://github.com/whatwg/webidl/issues/60) \ +for more information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)(this.tokens.base, this, "replace-void", message, { + autofix: replaceVoid(this), + }); + } + + /* + * If a union is nullable, its subunions cannot include a dictionary + * If not, subunions may include dictionaries if each union is not nullable + */ + const typedef = !this.union && defs.unique.get(this.idlType); + const target = this.union + ? this + : typedef && typedef.type === "typedef" + ? typedef.idlType + : undefined; + if (target && this.nullable) { + // do not allow any dictionary + const { reference } = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_4__.idlTypeIncludesDictionary)(target, defs) || {}; + if (reference) { + const targetToken = (this.union ? reference : this).tokens.base; + const message = "Nullable union cannot include a dictionary type."; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)( + targetToken, + this, + "no-nullable-union-dict", + message + ); + } + } else { + // allow some dictionary + for (const subtype of this.subtype) { + yield* subtype.validate(defs); + } + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const type_body = () => { + if (this.union || this.generic) { + return w.ts.wrap([ + w.token(this.tokens.base, w.ts.generic), + w.token(this.tokens.open), + ...this.subtype.map((t) => t.write(w)), + w.token(this.tokens.close), + ]); + } + const firstToken = this.tokens.prefix || this.tokens.base; + const prefix = this.tokens.prefix + ? [this.tokens.prefix.value, w.ts.trivia(this.tokens.base.trivia)] + : []; + const ref = w.reference( + w.ts.wrap([ + ...prefix, + this.tokens.base.value, + w.token(this.tokens.postfix), + ]), + { + unescaped: /** @type {string} (because it's not union) */ ( + this.idlType + ), + context: this, + } + ); + return w.ts.wrap([w.ts.trivia(firstToken.trivia), ref]); + }; + return w.ts.wrap([ + this.extAttrs.write(w), + type_body(), + w.token(this.tokens.nullable), + w.token(this.tokens.separator), + ]); + } +} + +/** + * @param {Type} type + */ +function replaceVoid(type) { + return () => { + type.tokens.base.value = "undefined"; + }; +} + + +/***/ }), +/* 6 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Base": () => (/* binding */ Base) +/* harmony export */ }); +class Base { + /** + * @param {object} initializer + * @param {Base["source"]} initializer.source + * @param {Base["tokens"]} initializer.tokens + */ + constructor({ source, tokens }) { + Object.defineProperties(this, { + source: { value: source }, + tokens: { value: tokens, writable: true }, + parent: { value: null, writable: true }, + this: { value: this }, // useful when escaping from proxy + }); + } + + toJSON() { + const json = { type: undefined, name: undefined, inheritance: undefined }; + let proto = this; + while (proto !== Object.prototype) { + const descMap = Object.getOwnPropertyDescriptors(proto); + for (const [key, value] of Object.entries(descMap)) { + if (value.enumerable || value.get) { + // @ts-ignore - allow indexing here + json[key] = this[key]; + } + } + proto = Object.getPrototypeOf(proto); + } + return json; + } +} + + +/***/ }), +/* 7 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "dictionaryIncludesRequiredField": () => (/* binding */ dictionaryIncludesRequiredField), +/* harmony export */ "idlTypeIncludesDictionary": () => (/* binding */ idlTypeIncludesDictionary) +/* harmony export */ }); +/** + * @typedef {import("../productions/dictionary.js").Dictionary} Dictionary + * + * @param {*} idlType + * @param {import("../validator.js").Definitions} defs + * @param {object} [options] + * @param {boolean} [options.useNullableInner] use when the input idlType is nullable and you want to use its inner type + * @return {{ reference: *, dictionary: Dictionary }} the type reference that ultimately includes dictionary. + */ +function idlTypeIncludesDictionary( + idlType, + defs, + { useNullableInner } = {} +) { + if (!idlType.union) { + const def = defs.unique.get(idlType.idlType); + if (!def) { + return; + } + if (def.type === "typedef") { + const { typedefIncludesDictionary } = defs.cache; + if (typedefIncludesDictionary.has(def)) { + // Note that this also halts when it met indeterminate state + // to prevent infinite recursion + return typedefIncludesDictionary.get(def); + } + defs.cache.typedefIncludesDictionary.set(def, undefined); // indeterminate state + const result = idlTypeIncludesDictionary(def.idlType, defs); + defs.cache.typedefIncludesDictionary.set(def, result); + if (result) { + return { + reference: idlType, + dictionary: result.dictionary, + }; + } + } + if (def.type === "dictionary" && (useNullableInner || !idlType.nullable)) { + return { + reference: idlType, + dictionary: def, + }; + } + } + for (const subtype of idlType.subtype) { + const result = idlTypeIncludesDictionary(subtype, defs); + if (result) { + if (subtype.union) { + return result; + } + return { + reference: subtype, + dictionary: result.dictionary, + }; + } + } +} + +/** + * @param {*} dict dictionary type + * @param {import("../validator.js").Definitions} defs + * @return {boolean} + */ +function dictionaryIncludesRequiredField(dict, defs) { + if (defs.cache.dictionaryIncludesRequiredField.has(dict)) { + return defs.cache.dictionaryIncludesRequiredField.get(dict); + } + // Set cached result to indeterminate to short-circuit circular definitions. + // The final result will be updated to true or false. + defs.cache.dictionaryIncludesRequiredField.set(dict, undefined); + let result = dict.members.some((field) => field.required); + if (!result && dict.inheritance) { + const superdict = defs.unique.get(dict.inheritance); + if (!superdict) { + // Assume required members in the supertype if it is unknown. + result = true; + } else if (dictionaryIncludesRequiredField(superdict, defs)) { + result = true; + } + } + defs.cache.dictionaryIncludesRequiredField.set(dict, result); + return result; +} + + +/***/ }), +/* 8 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "ExtendedAttributeParameters": () => (/* binding */ ExtendedAttributeParameters), +/* harmony export */ "ExtendedAttributes": () => (/* binding */ ExtendedAttributes), +/* harmony export */ "SimpleExtendedAttribute": () => (/* binding */ SimpleExtendedAttribute) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _array_base_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(9); +/* harmony import */ var _token_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(10); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(3); + + + + + + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} tokenName + */ +function tokens(tokeniser, tokenName) { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.list)(tokeniser, { + parser: _token_js__WEBPACK_IMPORTED_MODULE_2__.WrappedToken.parser(tokeniser, tokenName), + listName: tokenName + " list", + }); +} + +const extAttrValueSyntax = ["identifier", "decimal", "integer", "string"]; + +const shouldBeLegacyPrefixed = [ + "NoInterfaceObject", + "LenientSetter", + "LenientThis", + "TreatNonObjectAsNull", + "Unforgeable", +]; + +const renamedLegacies = new Map([ + .../** @type {[string, string][]} */ ( + shouldBeLegacyPrefixed.map((name) => [name, `Legacy${name}`]) + ), + ["NamedConstructor", "LegacyFactoryFunction"], + ["OverrideBuiltins", "LegacyOverrideBuiltIns"], + ["TreatNullAs", "LegacyNullToEmptyString"], +]); + +/** + * This will allow a set of extended attribute values to be parsed. + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function extAttrListItems(tokeniser) { + for (const syntax of extAttrValueSyntax) { + const toks = tokens(tokeniser, syntax); + if (toks.length) { + return toks; + } + } + tokeniser.error( + `Expected identifiers, strings, decimals, or integers but none found` + ); +} + +class ExtendedAttributeParameters extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const tokens = { assign: tokeniser.consume("=") }; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)( + new ExtendedAttributeParameters({ source: tokeniser.source, tokens }) + ); + ret.list = []; + if (tokens.assign) { + tokens.asterisk = tokeniser.consume("*"); + if (tokens.asterisk) { + return ret.this; + } + tokens.secondaryName = tokeniser.consumeKind(...extAttrValueSyntax); + } + tokens.open = tokeniser.consume("("); + if (tokens.open) { + ret.list = ret.rhsIsList + ? // [Exposed=(Window,Worker)] + extAttrListItems(tokeniser) + : // [LegacyFactoryFunction=Audio(DOMString src)] or [Constructor(DOMString str)] + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.argument_list)(tokeniser); + tokens.close = + tokeniser.consume(")") || + tokeniser.error("Unexpected token in extended attribute argument list"); + } else if (tokens.assign && !tokens.secondaryName) { + tokeniser.error("No right hand side to extended attribute assignment"); + } + return ret.this; + } + + get rhsIsList() { + return ( + this.tokens.assign && !this.tokens.asterisk && !this.tokens.secondaryName + ); + } + + get rhsType() { + if (this.rhsIsList) { + return this.list[0].tokens.value.type + "-list"; + } + if (this.tokens.asterisk) { + return "*"; + } + if (this.tokens.secondaryName) { + return this.tokens.secondaryName.type; + } + return null; + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { rhsType } = this; + return w.ts.wrap([ + w.token(this.tokens.assign), + w.token(this.tokens.asterisk), + w.reference_token(this.tokens.secondaryName, this.parent), + w.token(this.tokens.open), + ...this.list.map((p) => { + return rhsType === "identifier-list" + ? w.identifier(p, this.parent) + : p.write(w); + }), + w.token(this.tokens.close), + ]); + } +} + +class SimpleExtendedAttribute extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const name = tokeniser.consumeKind("identifier"); + if (name) { + return new SimpleExtendedAttribute({ + source: tokeniser.source, + tokens: { name }, + params: ExtendedAttributeParameters.parse(tokeniser), + }); + } + } + + constructor({ source, tokens, params }) { + super({ source, tokens }); + params.parent = this; + Object.defineProperty(this, "params", { value: params }); + } + + get type() { + return "extended-attribute"; + } + get name() { + return this.tokens.name.value; + } + get rhs() { + const { rhsType: type, tokens, list } = this.params; + if (!type) { + return null; + } + const value = this.params.rhsIsList + ? list + : this.params.tokens.secondaryName + ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(tokens.secondaryName.value) + : null; + return { type, value }; + } + get arguments() { + const { rhsIsList, list } = this.params; + if (!list || rhsIsList) { + return []; + } + return list; + } + + *validate(defs) { + const { name } = this; + if (name === "LegacyNoInterfaceObject") { + const message = `\`[LegacyNoInterfaceObject]\` extended attribute is an \ +undesirable feature that may be removed from Web IDL in the future. Refer to the \ +[relevant upstream PR](https://github.com/whatwg/webidl/pull/609) for more \ +information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_4__.validationError)( + this.tokens.name, + this, + "no-nointerfaceobject", + message, + { level: "warning" } + ); + } else if (renamedLegacies.has(name)) { + const message = `\`[${name}]\` extended attribute is a legacy feature \ +that is now renamed to \`[${renamedLegacies.get(name)}]\`. Refer to the \ +[relevant upstream PR](https://github.com/whatwg/webidl/pull/870) for more \ +information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_4__.validationError)(this.tokens.name, this, "renamed-legacy", message, { + level: "warning", + autofix: renameLegacyExtendedAttribute(this), + }); + } + for (const arg of this.arguments) { + yield* arg.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.wrap([ + w.ts.trivia(this.tokens.name.trivia), + w.ts.extendedAttribute( + w.ts.wrap([ + w.ts.extendedAttributeReference(this.name), + this.params.write(w), + ]) + ), + w.token(this.tokens.separator), + ]); + } +} + +/** + * @param {SimpleExtendedAttribute} extAttr + */ +function renameLegacyExtendedAttribute(extAttr) { + return () => { + const { name } = extAttr; + extAttr.tokens.name.value = renamedLegacies.get(name); + if (name === "TreatNullAs") { + extAttr.params.tokens = {}; + } + }; +} + +// Note: we parse something simpler than the official syntax. It's all that ever +// seems to be used +class ExtendedAttributes extends _array_base_js__WEBPACK_IMPORTED_MODULE_1__.ArrayBase { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const tokens = {}; + tokens.open = tokeniser.consume("["); + const ret = new ExtendedAttributes({ source: tokeniser.source, tokens }); + if (!tokens.open) return ret; + ret.push( + ...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.list)(tokeniser, { + parser: SimpleExtendedAttribute.parse, + listName: "extended attribute", + }) + ); + tokens.close = + tokeniser.consume("]") || + tokeniser.error( + "Expected a closing token for the extended attribute list" + ); + if (!ret.length) { + tokeniser.unconsume(tokens.close.index); + tokeniser.error("An extended attribute list must not be empty"); + } + if (tokeniser.probe("[")) { + tokeniser.error( + "Illegal double extended attribute lists, consider merging them" + ); + } + return ret; + } + + *validate(defs) { + for (const extAttr of this) { + yield* extAttr.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + if (!this.length) return ""; + return w.ts.wrap([ + w.token(this.tokens.open), + ...this.map((ea) => ea.write(w)), + w.token(this.tokens.close), + ]); + } +} + + +/***/ }), +/* 9 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "ArrayBase": () => (/* binding */ ArrayBase) +/* harmony export */ }); +class ArrayBase extends Array { + constructor({ source, tokens }) { + super(); + Object.defineProperties(this, { + source: { value: source }, + tokens: { value: tokens }, + parent: { value: null, writable: true }, + }); + } +} + + +/***/ }), +/* 10 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Eof": () => (/* binding */ Eof), +/* harmony export */ "WrappedToken": () => (/* binding */ WrappedToken) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class WrappedToken extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {string} type + */ + static parser(tokeniser, type) { + return () => { + const value = tokeniser.consumeKind(type); + if (value) { + return new WrappedToken({ + source: tokeniser.source, + tokens: { value }, + }); + } + }; + } + + get value() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.value.value); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.wrap([ + w.token(this.tokens.value), + w.token(this.tokens.separator), + ]); + } +} + +class Eof extends WrappedToken { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const value = tokeniser.consumeKind("eof"); + if (value) { + return new Eof({ source: tokeniser.source, tokens: { value } }); + } + } + + get type() { + return "eof"; + } +} + + +/***/ }), +/* 11 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Argument": () => (/* binding */ Argument) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _default_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(12); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(2); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(3); +/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(7); + + + + + + + + +class Argument extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const start_position = tokeniser.position; + /** @type {Base["tokens"]} */ + const tokens = {}; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)( + new Argument({ source: tokeniser.source, tokens }) + ); + ret.extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser); + tokens.optional = tokeniser.consume("optional"); + ret.idlType = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.type_with_extended_attributes)(tokeniser, "argument-type"); + if (!ret.idlType) { + return tokeniser.unconsume(start_position); + } + if (!tokens.optional) { + tokens.variadic = tokeniser.consume("..."); + } + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_4__.argumentNameKeywords); + if (!tokens.name) { + return tokeniser.unconsume(start_position); + } + ret.default = tokens.optional ? _default_js__WEBPACK_IMPORTED_MODULE_1__.Default.parse(tokeniser) : null; + return ret.this; + } + + get type() { + return "argument"; + } + get optional() { + return !!this.tokens.optional; + } + get variadic() { + return !!this.tokens.variadic; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(this.tokens.name.value); + } + + /** + * @param {import("../validator.js").Definitions} defs + */ + *validate(defs) { + yield* this.extAttrs.validate(defs); + yield* this.idlType.validate(defs); + const result = (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__.idlTypeIncludesDictionary)(this.idlType, defs, { + useNullableInner: true, + }); + if (result) { + if (this.idlType.nullable) { + const message = `Dictionary arguments cannot be nullable.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)( + this.tokens.name, + this, + "no-nullable-dict-arg", + message + ); + } else if (!this.optional) { + if ( + this.parent && + !(0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_6__.dictionaryIncludesRequiredField)(result.dictionary, defs) && + isLastRequiredArgument(this) + ) { + const message = `Dictionary argument must be optional if it has no required fields`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)( + this.tokens.name, + this, + "dict-arg-optional", + message, + { + autofix: autofixDictionaryArgumentOptionality(this), + } + ); + } + } else if (!this.default) { + const message = `Optional dictionary arguments must have a default value of \`{}\`.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_5__.validationError)( + this.tokens.name, + this, + "dict-arg-default", + message, + { + autofix: autofixOptionalDictionaryDefaultValue(this), + } + ); + } + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.optional), + w.ts.type(this.idlType.write(w)), + w.token(this.tokens.variadic), + w.name_token(this.tokens.name, { data: this }), + this.default ? this.default.write(w) : "", + w.token(this.tokens.separator), + ]); + } +} + +/** + * @param {Argument} arg + */ +function isLastRequiredArgument(arg) { + const list = arg.parent.arguments || arg.parent.list; + const index = list.indexOf(arg); + const requiredExists = list.slice(index + 1).some((a) => !a.optional); + return !requiredExists; +} + +/** + * @param {Argument} arg + */ +function autofixDictionaryArgumentOptionality(arg) { + return () => { + const firstToken = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.getFirstToken)(arg.idlType); + arg.tokens.optional = { + ...firstToken, + type: "optional", + value: "optional", + }; + firstToken.trivia = " "; + autofixOptionalDictionaryDefaultValue(arg)(); + }; +} + +/** + * @param {Argument} arg + */ +function autofixOptionalDictionaryDefaultValue(arg) { + return () => { + arg.default = _default_js__WEBPACK_IMPORTED_MODULE_1__.Default.parse(new _tokeniser_js__WEBPACK_IMPORTED_MODULE_4__.Tokeniser(" = {}")); + }; +} + + +/***/ }), +/* 12 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Default": () => (/* binding */ Default) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class Default extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const assign = tokeniser.consume("="); + if (!assign) { + return null; + } + const def = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_value)(tokeniser) || + tokeniser.consumeKind("string") || + tokeniser.consume("null", "[", "{") || + tokeniser.error("No value for default"); + const expression = [def]; + if (def.value === "[") { + const close = + tokeniser.consume("]") || + tokeniser.error("Default sequence value must be empty"); + expression.push(close); + } else if (def.value === "{") { + const close = + tokeniser.consume("}") || + tokeniser.error("Default dictionary value must be empty"); + expression.push(close); + } + return new Default({ + source: tokeniser.source, + tokens: { assign }, + expression, + }); + } + + constructor({ source, tokens, expression }) { + super({ source, tokens }); + expression.parent = this; + Object.defineProperty(this, "expression", { value: expression }); + } + + get type() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).type; + } + get value() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).value; + } + get negative() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_data)(this.expression[0]).negative; + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.wrap([ + w.token(this.tokens.assign), + ...this.expression.map((t) => w.token(t)), + ]); + } +} + + +/***/ }), +/* 13 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Operation": () => (/* binding */ Operation) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(3); + + + + +class Operation extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @typedef {import("../tokeniser.js").Token} Token + * + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} [options] + * @param {Token} [options.special] + * @param {Token} [options.regular] + */ + static parse(tokeniser, { special, regular } = {}) { + const tokens = { special }; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( + new Operation({ source: tokeniser.source, tokens }) + ); + if (special && special.value === "stringifier") { + tokens.termination = tokeniser.consume(";"); + if (tokens.termination) { + ret.arguments = []; + return ret; + } + } + if (!special && !regular) { + tokens.special = tokeniser.consume("getter", "setter", "deleter"); + } + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser) || tokeniser.error("Missing return type"); + tokens.name = + tokeniser.consumeKind("identifier") || tokeniser.consume("includes"); + tokens.open = + tokeniser.consume("(") || tokeniser.error("Invalid operation"); + ret.arguments = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser); + tokens.close = + tokeniser.consume(")") || tokeniser.error("Unterminated operation"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated operation, expected `;`"); + return ret.this; + } + + get type() { + return "operation"; + } + get name() { + const { name } = this.tokens; + if (!name) { + return ""; + } + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(name.value); + } + get special() { + if (!this.tokens.special) { + return ""; + } + return this.tokens.special.value; + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + if (!this.name && ["", "static"].includes(this.special)) { + const message = `Regular or static operations must have both a return type and an identifier.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_2__.validationError)(this.tokens.open, this, "incomplete-op", message); + } + if (this.idlType) { + yield* this.idlType.validate(defs); + } + for (const argument of this.arguments) { + yield* argument.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + const body = this.idlType + ? [ + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this, parent }), + w.token(this.tokens.open), + w.ts.wrap(this.arguments.map((arg) => arg.write(w))), + w.token(this.tokens.close), + ] + : []; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + this.tokens.name + ? w.token(this.tokens.special) + : w.token(this.tokens.special, w.ts.nameless, { data: this, parent }), + ...body, + w.token(this.tokens.termination), + ]), + { data: this, parent } + ); + } +} + + +/***/ }), +/* 14 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Attribute": () => (/* binding */ Attribute) +/* harmony export */ }); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); +/* harmony import */ var _validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(7); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4); + + + + + +class Attribute extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} [options] + * @param {import("../tokeniser.js").Token} [options.special] + * @param {boolean} [options.noInherit] + * @param {boolean} [options.readonly] + */ + static parse( + tokeniser, + { special, noInherit = false, readonly = false } = {} + ) { + const start_position = tokeniser.position; + const tokens = { special }; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)( + new Attribute({ source: tokeniser.source, tokens }) + ); + if (!special && !noInherit) { + tokens.special = tokeniser.consume("inherit"); + } + if (ret.special === "inherit" && tokeniser.probe("readonly")) { + tokeniser.error("Inherited attributes cannot be read-only"); + } + tokens.readonly = tokeniser.consume("readonly"); + if (readonly && !tokens.readonly && tokeniser.probe("attribute")) { + tokeniser.error("Attributes must be readonly in this context"); + } + tokens.base = tokeniser.consume("attribute"); + if (!tokens.base) { + tokeniser.unconsume(start_position); + return; + } + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.type_with_extended_attributes)(tokeniser, "attribute-type") || + tokeniser.error("Attribute lacks a type"); + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.consume("async", "required") || + tokeniser.error("Attribute lacks a name"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated attribute, expected `;`"); + return ret.this; + } + + get type() { + return "attribute"; + } + get special() { + if (!this.tokens.special) { + return ""; + } + return this.tokens.special.value; + } + get readonly() { + return !!this.tokens.readonly; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(this.tokens.name.value); + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + yield* this.idlType.validate(defs); + + switch (this.idlType.generic) { + case "sequence": + case "record": { + const message = `Attributes cannot accept ${this.idlType.generic} types.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( + this.tokens.name, + this, + "attr-invalid-type", + message + ); + break; + } + default: { + const { reference } = + (0,_validators_helpers_js__WEBPACK_IMPORTED_MODULE_1__.idlTypeIncludesDictionary)(this.idlType, defs) || {}; + if (reference) { + const targetToken = (this.idlType.union ? reference : this.idlType) + .tokens.base; + const message = "Attributes cannot accept dictionary types."; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( + targetToken, + this, + "attr-invalid-type", + message + ); + } + } + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.special), + w.token(this.tokens.readonly), + w.token(this.tokens.base), + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this, parent }), + w.token(this.tokens.termination), + ]), + { data: this, parent } + ); + } +} + + +/***/ }), +/* 15 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Enum": () => (/* binding */ Enum), +/* harmony export */ "EnumValue": () => (/* binding */ EnumValue) +/* harmony export */ }); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4); +/* harmony import */ var _token_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(10); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6); + + + + +class EnumValue extends _token_js__WEBPACK_IMPORTED_MODULE_1__.WrappedToken { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const value = tokeniser.consumeKind("string"); + if (value) { + return new EnumValue({ source: tokeniser.source, tokens: { value } }); + } + } + + get type() { + return "enum-value"; + } + get value() { + return super.value.slice(1, -1); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.wrap([ + w.ts.trivia(this.tokens.value.trivia), + w.ts.definition( + w.ts.wrap(['"', w.ts.name(this.value, { data: this, parent }), '"']), + { data: this, parent } + ), + w.token(this.tokens.separator), + ]); + } +} + +class Enum extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + /** @type {Base["tokens"]} */ + const tokens = {}; + tokens.base = tokeniser.consume("enum"); + if (!tokens.base) { + return; + } + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("No name for enum"); + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.autoParenter)(new Enum({ source: tokeniser.source, tokens })); + tokeniser.current = ret.this; + tokens.open = tokeniser.consume("{") || tokeniser.error("Bodyless enum"); + ret.values = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.list)(tokeniser, { + parser: EnumValue.parse, + allowDangler: true, + listName: "enumeration", + }); + if (tokeniser.probeKind("string")) { + tokeniser.error("No comma between enum values"); + } + tokens.close = + tokeniser.consume("}") || tokeniser.error("Unexpected value in enum"); + if (!ret.values.length) { + tokeniser.error("No value in enum"); + } + tokens.termination = + tokeniser.consume(";") || tokeniser.error("No semicolon after enum"); + return ret.this; + } + + get type() { + return "enum"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_0__.unescape)(this.tokens.name.value); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base), + w.name_token(this.tokens.name, { data: this }), + w.token(this.tokens.open), + w.ts.wrap(this.values.map((v) => v.write(w))), + w.token(this.tokens.close), + w.token(this.tokens.termination), + ]), + { data: this } + ); + } +} + + +/***/ }), +/* 16 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Includes": () => (/* binding */ Includes) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class Includes extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const target = tokeniser.consumeKind("identifier"); + if (!target) { + return; + } + const tokens = { target }; + tokens.includes = tokeniser.consume("includes"); + if (!tokens.includes) { + tokeniser.unconsume(target.index); + return; + } + tokens.mixin = + tokeniser.consumeKind("identifier") || + tokeniser.error("Incomplete includes statement"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("No terminating ; for includes statement"); + return new Includes({ source: tokeniser.source, tokens }); + } + + get type() { + return "includes"; + } + get target() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.target.value); + } + get includes() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.mixin.value); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.reference_token(this.tokens.target, this), + w.token(this.tokens.includes), + w.reference_token(this.tokens.mixin, this), + w.token(this.tokens.termination), + ]), + { data: this } + ); + } +} + + +/***/ }), +/* 17 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Typedef": () => (/* binding */ Typedef) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class Typedef extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + /** @type {Base["tokens"]} */ + const tokens = {}; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Typedef({ source: tokeniser.source, tokens })); + tokens.base = tokeniser.consume("typedef"); + if (!tokens.base) { + return; + } + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "typedef-type") || + tokeniser.error("Typedef lacks a type"); + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("Typedef lacks a name"); + tokeniser.current = ret.this; + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated typedef, expected `;`"); + return ret.this; + } + + get type() { + return "typedef"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value); + } + + *validate(defs) { + yield* this.idlType.validate(defs); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base), + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this }), + w.token(this.tokens.termination), + ]), + { data: this } + ); + } +} + + +/***/ }), +/* 18 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "CallbackFunction": () => (/* binding */ CallbackFunction) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class CallbackFunction extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser, base) { + const tokens = { base }; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( + new CallbackFunction({ source: tokeniser.source, tokens }) + ); + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("Callback lacks a name"); + tokeniser.current = ret.this; + tokens.assign = + tokeniser.consume("=") || tokeniser.error("Callback lacks an assignment"); + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser) || tokeniser.error("Callback lacks a return type"); + tokens.open = + tokeniser.consume("(") || + tokeniser.error("Callback lacks parentheses for arguments"); + ret.arguments = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser); + tokens.close = + tokeniser.consume(")") || tokeniser.error("Unterminated callback"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated callback, expected `;`"); + return ret.this; + } + + get type() { + return "callback"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value); + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + yield* this.idlType.validate(defs); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base), + w.name_token(this.tokens.name, { data: this }), + w.token(this.tokens.assign), + w.ts.type(this.idlType.write(w)), + w.token(this.tokens.open), + ...this.arguments.map((arg) => arg.write(w)), + w.token(this.tokens.close), + w.token(this.tokens.termination), + ]), + { data: this } + ); + } +} + + +/***/ }), +/* 19 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Interface": () => (/* binding */ Interface) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13); +/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(21); +/* harmony import */ var _iterable_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(22); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(4); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(3); +/* harmony import */ var _validators_interface_js__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(23); +/* harmony import */ var _constructor_js__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(24); +/* harmony import */ var _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(2); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(8); + + + + + + + + + + + + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function static_member(tokeniser) { + const special = tokeniser.consume("static"); + if (!special) return; + const member = + _attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse(tokeniser, { special }) || + _operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse(tokeniser, { special }) || + tokeniser.error("No body in static member"); + return member; +} + +class Interface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser, base, { partial = null } = {}) { + const tokens = { partial, base }; + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new Interface({ source: tokeniser.source, tokens }), + { + inheritable: !partial, + allowedMembers: [ + [_constant_js__WEBPACK_IMPORTED_MODULE_3__.Constant.parse], + [_constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse], + [static_member], + [_helpers_js__WEBPACK_IMPORTED_MODULE_5__.stringifier], + [_iterable_js__WEBPACK_IMPORTED_MODULE_4__.IterableLike.parse], + [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse], + [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse], + ], + } + ); + } + + get type() { + return "interface"; + } + + *validate(defs) { + yield* this.extAttrs.validate(defs); + if ( + !this.partial && + this.extAttrs.every((extAttr) => extAttr.name !== "Exposed") + ) { + const message = `Interfaces must have \`[Exposed]\` extended attribute. \ +To fix, add, for example, \`[Exposed=Window]\`. Please also consider carefully \ +if your interface should also be exposed in a Worker scope. Refer to the \ +[WebIDL spec section on Exposed](https://heycam.github.io/webidl/#Exposed) \ +for more information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)( + this.tokens.name, + this, + "require-exposed", + message, + { + autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autofixAddExposedWindow)(this), + } + ); + } + const oldConstructors = this.extAttrs.filter( + (extAttr) => extAttr.name === "Constructor" + ); + for (const constructor of oldConstructors) { + const message = `Constructors should now be represented as a \`constructor()\` operation on the interface \ +instead of \`[Constructor]\` extended attribute. Refer to the \ +[WebIDL spec section on constructor operations](https://heycam.github.io/webidl/#idl-constructors) \ +for more information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)( + constructor.tokens.name, + this, + "constructor-member", + message, + { + autofix: autofixConstructor(this, constructor), + } + ); + } + + const isGlobal = this.extAttrs.some((extAttr) => extAttr.name === "Global"); + if (isGlobal) { + const factoryFunctions = this.extAttrs.filter( + (extAttr) => extAttr.name === "LegacyFactoryFunction" + ); + for (const named of factoryFunctions) { + const message = `Interfaces marked as \`[Global]\` cannot have factory functions.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)( + named.tokens.name, + this, + "no-constructible-global", + message + ); + } + + const constructors = this.members.filter( + (member) => member.type === "constructor" + ); + for (const named of constructors) { + const message = `Interfaces marked as \`[Global]\` cannot have constructors.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_6__.validationError)( + named.tokens.base, + this, + "no-constructible-global", + message + ); + } + } + + yield* super.validate(defs); + if (!this.partial) { + yield* (0,_validators_interface_js__WEBPACK_IMPORTED_MODULE_7__.checkInterfaceMemberDuplication)(defs, this); + } + } +} + +function autofixConstructor(interfaceDef, constructorExtAttr) { + interfaceDef = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autoParenter)(interfaceDef); + return () => { + const indentation = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)( + interfaceDef.extAttrs.tokens.open.trivia + ); + const memberIndent = interfaceDef.members.length + ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getLastIndentation)((0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getFirstToken)(interfaceDef.members[0]).trivia) + : (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.getMemberIndentation)(indentation); + const constructorOp = _constructor_js__WEBPACK_IMPORTED_MODULE_8__.Constructor.parse( + new _tokeniser_js__WEBPACK_IMPORTED_MODULE_9__.Tokeniser(`\n${memberIndent}constructor();`) + ); + constructorOp.extAttrs = new _extended_attributes_js__WEBPACK_IMPORTED_MODULE_10__.ExtendedAttributes({ + source: interfaceDef.source, + tokens: {}, + }); + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.autoParenter)(constructorOp).arguments = constructorExtAttr.arguments; + + const existingIndex = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_5__.findLastIndex)( + interfaceDef.members, + (m) => m.type === "constructor" + ); + interfaceDef.members.splice(existingIndex + 1, 0, constructorOp); + + const { close } = interfaceDef.tokens; + if (!close.trivia.includes("\n")) { + close.trivia += `\n${indentation}`; + } + + const { extAttrs } = interfaceDef; + const index = extAttrs.indexOf(constructorExtAttr); + const removed = extAttrs.splice(index, 1); + if (!extAttrs.length) { + extAttrs.tokens.open = extAttrs.tokens.close = undefined; + } else if (extAttrs.length === index) { + extAttrs[index - 1].tokens.separator = undefined; + } else if (!extAttrs[index].tokens.name.trivia.trim()) { + extAttrs[index].tokens.name.trivia = removed[0].tokens.name.trivia; + } + }; +} + + +/***/ }), +/* 20 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Container": () => (/* binding */ Container) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(8); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); + + + + +/** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ +function inheritance(tokeniser) { + const colon = tokeniser.consume(":"); + if (!colon) { + return {}; + } + const inheritance = + tokeniser.consumeKind("identifier") || + tokeniser.error("Inheritance lacks a type"); + return { colon, inheritance }; +} + +class Container extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {*} instance TODO: This should be {T extends Container}, but see https://github.com/microsoft/TypeScript/issues/4628 + * @param {*} args + */ + static parse(tokeniser, instance, { inheritable, allowedMembers }) { + const { tokens, type } = instance; + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error(`Missing name in ${type}`); + tokeniser.current = instance; + instance = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(instance); + if (inheritable) { + Object.assign(tokens, inheritance(tokeniser)); + } + tokens.open = tokeniser.consume("{") || tokeniser.error(`Bodyless ${type}`); + instance.members = []; + while (true) { + tokens.close = tokeniser.consume("}"); + if (tokens.close) { + tokens.termination = + tokeniser.consume(";") || + tokeniser.error(`Missing semicolon after ${type}`); + return instance.this; + } + const ea = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_1__.ExtendedAttributes.parse(tokeniser); + let mem; + for (const [parser, ...args] of allowedMembers) { + mem = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(parser(tokeniser, ...args)); + if (mem) { + break; + } + } + if (!mem) { + tokeniser.error("Unknown member"); + } + mem.extAttrs = ea; + instance.members.push(mem.this); + } + } + + get partial() { + return !!this.tokens.partial; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.name.value); + } + get inheritance() { + if (!this.tokens.inheritance) { + return null; + } + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.inheritance.value); + } + + *validate(defs) { + for (const member of this.members) { + if (member.validate) { + yield* member.validate(defs); + } + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const inheritance = () => { + if (!this.tokens.inheritance) { + return ""; + } + return w.ts.wrap([ + w.token(this.tokens.colon), + w.ts.trivia(this.tokens.inheritance.trivia), + w.ts.inheritance( + w.reference(this.tokens.inheritance.value, { context: this }) + ), + ]); + }; + + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.callback), + w.token(this.tokens.partial), + w.token(this.tokens.base), + w.token(this.tokens.mixin), + w.name_token(this.tokens.name, { data: this }), + inheritance(), + w.token(this.tokens.open), + w.ts.wrap(this.members.map((m) => m.write(w))), + w.token(this.tokens.close), + w.token(this.tokens.termination), + ]), + { data: this } + ); + } +} + + +/***/ }), +/* 21 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Constant": () => (/* binding */ Constant) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _type_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(5); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); + + + + +class Constant extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + /** @type {Base["tokens"]} */ + const tokens = {}; + tokens.base = tokeniser.consume("const"); + if (!tokens.base) { + return; + } + let idlType = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.primitive_type)(tokeniser); + if (!idlType) { + const base = + tokeniser.consumeKind("identifier") || + tokeniser.error("Const lacks a type"); + idlType = new _type_js__WEBPACK_IMPORTED_MODULE_1__.Type({ source: tokeniser.source, tokens: { base } }); + } + if (tokeniser.probe("?")) { + tokeniser.error("Unexpected nullable constant type"); + } + idlType.type = "const-type"; + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("Const lacks a name"); + tokens.assign = + tokeniser.consume("=") || tokeniser.error("Const lacks value assignment"); + tokens.value = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.const_value)(tokeniser) || tokeniser.error("Const lacks a value"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated const, expected `;`"); + const ret = new Constant({ source: tokeniser.source, tokens }); + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(ret).idlType = idlType; + return ret; + } + + get type() { + return "const"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.unescape)(this.tokens.name.value); + } + get value() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.const_data)(this.tokens.value); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base), + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this, parent }), + w.token(this.tokens.assign), + w.token(this.tokens.value), + w.token(this.tokens.termination), + ]), + { data: this, parent } + ); + } +} + + +/***/ }), +/* 22 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "IterableLike": () => (/* binding */ IterableLike) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class IterableLike extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const start_position = tokeniser.position; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)( + new IterableLike({ source: tokeniser.source, tokens: {} }) + ); + const { tokens } = ret; + tokens.readonly = tokeniser.consume("readonly"); + if (!tokens.readonly) { + tokens.async = tokeniser.consume("async"); + } + tokens.base = tokens.readonly + ? tokeniser.consume("maplike", "setlike") + : tokens.async + ? tokeniser.consume("iterable") + : tokeniser.consume("iterable", "maplike", "setlike"); + if (!tokens.base) { + tokeniser.unconsume(start_position); + return; + } + + const { type } = ret; + const secondTypeRequired = type === "maplike"; + const secondTypeAllowed = secondTypeRequired || type === "iterable"; + const argumentAllowed = ret.async && type === "iterable"; + + tokens.open = + tokeniser.consume("<") || + tokeniser.error(`Missing less-than sign \`<\` in ${type} declaration`); + const first = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser) || + tokeniser.error(`Missing a type argument in ${type} declaration`); + ret.idlType = [first]; + ret.arguments = []; + + if (secondTypeAllowed) { + first.tokens.separator = tokeniser.consume(","); + if (first.tokens.separator) { + ret.idlType.push((0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser)); + } else if (secondTypeRequired) { + tokeniser.error(`Missing second type argument in ${type} declaration`); + } + } + + tokens.close = + tokeniser.consume(">") || + tokeniser.error(`Missing greater-than sign \`>\` in ${type} declaration`); + + if (tokeniser.probe("(")) { + if (argumentAllowed) { + tokens.argsOpen = tokeniser.consume("("); + ret.arguments.push(...(0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser)); + tokens.argsClose = + tokeniser.consume(")") || + tokeniser.error("Unterminated async iterable argument list"); + } else { + tokeniser.error(`Arguments are only allowed for \`async iterable\``); + } + } + + tokens.termination = + tokeniser.consume(";") || + tokeniser.error(`Missing semicolon after ${type} declaration`); + + return ret.this; + } + + get type() { + return this.tokens.base.value; + } + get readonly() { + return !!this.tokens.readonly; + } + get async() { + return !!this.tokens.async; + } + + *validate(defs) { + for (const type of this.idlType) { + yield* type.validate(defs); + } + for (const argument of this.arguments) { + yield* argument.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.readonly), + w.token(this.tokens.async), + w.token(this.tokens.base, w.ts.generic), + w.token(this.tokens.open), + w.ts.wrap(this.idlType.map((t) => t.write(w))), + w.token(this.tokens.close), + w.token(this.tokens.argsOpen), + w.ts.wrap(this.arguments.map((arg) => arg.write(w))), + w.token(this.tokens.argsClose), + w.token(this.tokens.termination), + ]), + { data: this, parent: this.parent } + ); + } +} + + +/***/ }), +/* 23 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "checkInterfaceMemberDuplication": () => (/* binding */ checkInterfaceMemberDuplication) +/* harmony export */ }); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); + + +/** + * @param {import("../validator.js").Definitions} defs + * @param {import("../productions/container.js").Container} i + */ +function* checkInterfaceMemberDuplication(defs, i) { + const opNames = groupOperationNames(i); + const partials = defs.partials.get(i.name) || []; + const mixins = defs.mixinMap.get(i.name) || []; + for (const ext of [...partials, ...mixins]) { + const additions = getOperations(ext); + const statics = additions.filter((a) => a.special === "static"); + const nonstatics = additions.filter((a) => a.special !== "static"); + yield* checkAdditions(statics, opNames.statics, ext, i); + yield* checkAdditions(nonstatics, opNames.nonstatics, ext, i); + statics.forEach((op) => opNames.statics.add(op.name)); + nonstatics.forEach((op) => opNames.nonstatics.add(op.name)); + } + + /** + * @param {import("../productions/operation.js").Operation[]} additions + * @param {Set<string>} existings + * @param {import("../productions/container.js").Container} ext + * @param {import("../productions/container.js").Container} base + */ + function* checkAdditions(additions, existings, ext, base) { + for (const addition of additions) { + const { name } = addition; + if (name && existings.has(name)) { + const isStatic = addition.special === "static" ? "static " : ""; + const message = `The ${isStatic}operation "${name}" has already been defined for the base interface "${base.name}" either in itself or in a mixin`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)( + addition.tokens.name, + ext, + "no-cross-overload", + message + ); + } + } + } + + /** + * @param {import("../productions/container.js").Container} i + * @returns {import("../productions/operation.js").Operation[]} + */ + function getOperations(i) { + return i.members.filter(({ type }) => type === "operation"); + } + + /** + * @param {import("../productions/container.js").Container} i + */ + function groupOperationNames(i) { + const ops = getOperations(i); + return { + statics: new Set( + ops.filter((op) => op.special === "static").map((op) => op.name) + ), + nonstatics: new Set( + ops.filter((op) => op.special !== "static").map((op) => op.name) + ), + }; + } +} + + +/***/ }), +/* 24 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Constructor": () => (/* binding */ Constructor) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); + + + +class Constructor extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + const base = tokeniser.consume("constructor"); + if (!base) { + return; + } + /** @type {Base["tokens"]} */ + const tokens = { base }; + tokens.open = + tokeniser.consume("(") || + tokeniser.error("No argument list in constructor"); + const args = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.argument_list)(tokeniser); + tokens.close = + tokeniser.consume(")") || tokeniser.error("Unterminated constructor"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("No semicolon after constructor"); + const ret = new Constructor({ source: tokeniser.source, tokens }); + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(ret).arguments = args; + return ret; + } + + get type() { + return "constructor"; + } + + *validate(defs) { + for (const argument of this.arguments) { + yield* argument.validate(defs); + } + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.base, w.ts.nameless, { data: this, parent }), + w.token(this.tokens.open), + w.ts.wrap(this.arguments.map((arg) => arg.write(w))), + w.token(this.tokens.close), + w.token(this.tokens.termination), + ]), + { data: this, parent } + ); + } +} + + +/***/ }), +/* 25 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Mixin": () => (/* binding */ Mixin) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(21); +/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(14); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(13); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4); + + + + + + +class Mixin extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @typedef {import("../tokeniser.js").Token} Token + * + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {Token} base + * @param {object} [options] + * @param {Token} [options.partial] + */ + static parse(tokeniser, base, { partial } = {}) { + const tokens = { partial, base }; + tokens.mixin = tokeniser.consume("mixin"); + if (!tokens.mixin) { + return; + } + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new Mixin({ source: tokeniser.source, tokens }), + { + allowedMembers: [ + [_constant_js__WEBPACK_IMPORTED_MODULE_1__.Constant.parse], + [_helpers_js__WEBPACK_IMPORTED_MODULE_4__.stringifier], + [_attribute_js__WEBPACK_IMPORTED_MODULE_2__.Attribute.parse, { noInherit: true }], + [_operation_js__WEBPACK_IMPORTED_MODULE_3__.Operation.parse, { regular: true }], + ], + } + ); + } + + get type() { + return "interface mixin"; + } +} + + +/***/ }), +/* 26 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Dictionary": () => (/* binding */ Dictionary) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _field_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(27); + + + +class Dictionary extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} [options] + * @param {import("../tokeniser.js").Token} [options.partial] + */ + static parse(tokeniser, { partial } = {}) { + const tokens = { partial }; + tokens.base = tokeniser.consume("dictionary"); + if (!tokens.base) { + return; + } + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new Dictionary({ source: tokeniser.source, tokens }), + { + inheritable: !partial, + allowedMembers: [[_field_js__WEBPACK_IMPORTED_MODULE_1__.Field.parse]], + } + ); + } + + get type() { + return "dictionary"; + } +} + + +/***/ }), +/* 27 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Field": () => (/* binding */ Field) +/* harmony export */ }); +/* harmony import */ var _base_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4); +/* harmony import */ var _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(8); +/* harmony import */ var _default_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(12); + + + + + +class Field extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser) { + /** @type {Base["tokens"]} */ + const tokens = {}; + const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.autoParenter)(new Field({ source: tokeniser.source, tokens })); + ret.extAttrs = _extended_attributes_js__WEBPACK_IMPORTED_MODULE_2__.ExtendedAttributes.parse(tokeniser); + tokens.required = tokeniser.consume("required"); + ret.idlType = + (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "dictionary-type") || + tokeniser.error("Dictionary member lacks a type"); + tokens.name = + tokeniser.consumeKind("identifier") || + tokeniser.error("Dictionary member lacks a name"); + ret.default = _default_js__WEBPACK_IMPORTED_MODULE_3__.Default.parse(tokeniser); + if (tokens.required && ret.default) + tokeniser.error("Required member must not have a default"); + tokens.termination = + tokeniser.consume(";") || + tokeniser.error("Unterminated dictionary member, expected `;`"); + return ret.this; + } + + get type() { + return "field"; + } + get name() { + return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.name.value); + } + get required() { + return !!this.tokens.required; + } + + *validate(defs) { + yield* this.idlType.validate(defs); + } + + /** @param {import("../writer.js").Writer} w */ + write(w) { + const { parent } = this; + return w.ts.definition( + w.ts.wrap([ + this.extAttrs.write(w), + w.token(this.tokens.required), + w.ts.type(this.idlType.write(w)), + w.name_token(this.tokens.name, { data: this, parent }), + this.default ? this.default.write(w) : "", + w.token(this.tokens.termination), + ]), + { data: this, parent } + ); + } +} + + +/***/ }), +/* 28 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Namespace": () => (/* binding */ Namespace) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _attribute_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(14); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(13); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3); +/* harmony import */ var _helpers_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(4); +/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(21); + + + + + + + +class Namespace extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + * @param {object} [options] + * @param {import("../tokeniser.js").Token} [options.partial] + */ + static parse(tokeniser, { partial } = {}) { + const tokens = { partial }; + tokens.base = tokeniser.consume("namespace"); + if (!tokens.base) { + return; + } + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new Namespace({ source: tokeniser.source, tokens }), + { + allowedMembers: [ + [_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse, { noInherit: true, readonly: true }], + [_constant_js__WEBPACK_IMPORTED_MODULE_5__.Constant.parse], + [_operation_js__WEBPACK_IMPORTED_MODULE_2__.Operation.parse, { regular: true }], + ], + } + ); + } + + get type() { + return "namespace"; + } + + *validate(defs) { + if ( + !this.partial && + this.extAttrs.every((extAttr) => extAttr.name !== "Exposed") + ) { + const message = `Namespaces must have [Exposed] extended attribute. \ +To fix, add, for example, [Exposed=Window]. Please also consider carefully \ +if your namespace should also be exposed in a Worker scope. Refer to the \ +[WebIDL spec section on Exposed](https://heycam.github.io/webidl/#Exposed) \ +for more information.`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_3__.validationError)( + this.tokens.name, + this, + "require-exposed", + message, + { + autofix: (0,_helpers_js__WEBPACK_IMPORTED_MODULE_4__.autofixAddExposedWindow)(this), + } + ); + } + yield* super.validate(defs); + } +} + + +/***/ }), +/* 29 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "CallbackInterface": () => (/* binding */ CallbackInterface) +/* harmony export */ }); +/* harmony import */ var _container_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(20); +/* harmony import */ var _operation_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13); +/* harmony import */ var _constant_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(21); + + + + +class CallbackInterface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container { + /** + * @param {import("../tokeniser.js").Tokeniser} tokeniser + */ + static parse(tokeniser, callback, { partial = null } = {}) { + const tokens = { callback }; + tokens.base = tokeniser.consume("interface"); + if (!tokens.base) { + return; + } + return _container_js__WEBPACK_IMPORTED_MODULE_0__.Container.parse( + tokeniser, + new CallbackInterface({ source: tokeniser.source, tokens }), + { + inheritable: !partial, + allowedMembers: [ + [_constant_js__WEBPACK_IMPORTED_MODULE_2__.Constant.parse], + [_operation_js__WEBPACK_IMPORTED_MODULE_1__.Operation.parse, { regular: true }], + ], + } + ); + } + + get type() { + return "callback interface"; + } +} + + +/***/ }), +/* 30 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "Writer": () => (/* binding */ Writer), +/* harmony export */ "write": () => (/* binding */ write) +/* harmony export */ }); +function noop(arg) { + return arg; +} + +const templates = { + wrap: (items) => items.join(""), + trivia: noop, + name: noop, + reference: noop, + type: noop, + generic: noop, + nameless: noop, + inheritance: noop, + definition: noop, + extendedAttribute: noop, + extendedAttributeReference: noop, +}; + +class Writer { + constructor(ts) { + this.ts = Object.assign({}, templates, ts); + } + + /** + * @param {string} raw + * @param {object} options + * @param {string} [options.unescaped] + * @param {import("./productions/base.js").Base} [options.context] + * @returns + */ + reference(raw, { unescaped, context }) { + if (!unescaped) { + unescaped = raw.startsWith("_") ? raw.slice(1) : raw; + } + return this.ts.reference(raw, unescaped, context); + } + + /** + * @param {import("./tokeniser.js").Token} t + * @param {Function} wrapper + * @param {...any} args + * @returns + */ + token(t, wrapper = noop, ...args) { + if (!t) { + return ""; + } + const value = wrapper(t.value, ...args); + return this.ts.wrap([this.ts.trivia(t.trivia), value]); + } + + reference_token(t, context) { + return this.token(t, this.reference.bind(this), { context }); + } + + name_token(t, arg) { + return this.token(t, this.ts.name, arg); + } + + identifier(id, context) { + return this.ts.wrap([ + this.reference_token(id.tokens.value, context), + this.token(id.tokens.separator), + ]); + } +} + +function write(ast, { templates: ts = templates } = {}) { + ts = Object.assign({}, templates, ts); + + const w = new Writer(ts); + + return ts.wrap(ast.map((it) => it.write(w))); +} + + +/***/ }), +/* 31 */ +/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => { + +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "validate": () => (/* binding */ validate) +/* harmony export */ }); +/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3); + + +function getMixinMap(all, unique) { + const map = new Map(); + const includes = all.filter((def) => def.type === "includes"); + for (const include of includes) { + const mixin = unique.get(include.includes); + if (!mixin) { + continue; + } + const array = map.get(include.target); + if (array) { + array.push(mixin); + } else { + map.set(include.target, [mixin]); + } + } + return map; +} + +/** + * @typedef {ReturnType<typeof groupDefinitions>} Definitions + */ +function groupDefinitions(all) { + const unique = new Map(); + const duplicates = new Set(); + const partials = new Map(); + for (const def of all) { + if (def.partial) { + const array = partials.get(def.name); + if (array) { + array.push(def); + } else { + partials.set(def.name, [def]); + } + continue; + } + if (!def.name) { + continue; + } + if (!unique.has(def.name)) { + unique.set(def.name, def); + } else { + duplicates.add(def); + } + } + return { + all, + unique, + partials, + duplicates, + mixinMap: getMixinMap(all, unique), + cache: { + typedefIncludesDictionary: new WeakMap(), + dictionaryIncludesRequiredField: new WeakMap(), + }, + }; +} + +function* checkDuplicatedNames({ unique, duplicates }) { + for (const dup of duplicates) { + const { name } = dup; + const message = `The name "${name}" of type "${ + unique.get(name).type + }" was already seen`; + yield (0,_error_js__WEBPACK_IMPORTED_MODULE_0__.validationError)(dup.tokens.name, dup, "no-duplicate", message); + } +} + +function* validateIterable(ast) { + const defs = groupDefinitions(ast); + for (const def of defs.all) { + if (def.validate) { + yield* def.validate(defs); + } + } + yield* checkDuplicatedNames(defs); +} + +// Remove this once all of our support targets expose `.flat()` by default +function flatten(array) { + if (array.flat) { + return array.flat(); + } + return [].concat(...array); +} + +/** + * @param {import("./productions/base.js").Base[]} ast + * @return {import("./error.js").WebIDLErrorData[]} validation errors + */ +function validate(ast) { + return [...validateIterable(flatten(ast))]; +} + + +/***/ }) +/******/ ]); +/************************************************************************/ +/******/ // The module cache +/******/ var __webpack_module_cache__ = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ // Check if module is in cache +/******/ var cachedModule = __webpack_module_cache__[moduleId]; +/******/ if (cachedModule !== undefined) { +/******/ return cachedModule.exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = __webpack_module_cache__[moduleId] = { +/******/ // no module.id needed +/******/ // no module.loaded needed +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__); +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/************************************************************************/ +/******/ /* webpack/runtime/define property getters */ +/******/ (() => { +/******/ // define getter functions for harmony exports +/******/ __webpack_require__.d = (exports, definition) => { +/******/ for(var key in definition) { +/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) { +/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] }); +/******/ } +/******/ } +/******/ }; +/******/ })(); +/******/ +/******/ /* webpack/runtime/hasOwnProperty shorthand */ +/******/ (() => { +/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop)) +/******/ })(); +/******/ +/******/ /* webpack/runtime/make namespace object */ +/******/ (() => { +/******/ // define __esModule on exports +/******/ __webpack_require__.r = (exports) => { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ })(); +/******/ +/************************************************************************/ +var __webpack_exports__ = {}; +// This entry need to be wrapped in an IIFE because it need to be isolated against other modules in the chunk. +(() => { +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "WebIDLParseError": () => (/* reexport safe */ _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__.WebIDLParseError), +/* harmony export */ "parse": () => (/* reexport safe */ _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__.parse), +/* harmony export */ "validate": () => (/* reexport safe */ _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__.validate), +/* harmony export */ "write": () => (/* reexport safe */ _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__.write) +/* harmony export */ }); +/* harmony import */ var _lib_webidl2_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1); +/* harmony import */ var _lib_writer_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(30); +/* harmony import */ var _lib_validator_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(31); +/* harmony import */ var _lib_tokeniser_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(2); + + + + + +})(); + +/******/ return __webpack_exports__; +/******/ })() +; +}); +//# sourceMappingURL=webidl2.js.map
\ No newline at end of file diff --git a/testing/web-platform/tests/resources/webidl2/lib/webidl2.js.headers b/testing/web-platform/tests/resources/webidl2/lib/webidl2.js.headers new file mode 100644 index 0000000000..6805c323df --- /dev/null +++ b/testing/web-platform/tests/resources/webidl2/lib/webidl2.js.headers @@ -0,0 +1 @@ +Content-Type: text/javascript; charset=utf-8 |