diff --git a/test/common/wpt/worker.js b/test/common/wpt/worker.js
index 468e950ac10cb1..47e119c22dcd84 100644
--- a/test/common/wpt/worker.js
+++ b/test/common/wpt/worker.js
@@ -8,7 +8,8 @@ const resource = new ResourceLoader(workerData.wptPath);
global.self = global;
global.GLOBAL = {
- isWindow() { return false; }
+ isWindow() { return false; },
+ isShadowRealm() { return false; }
};
global.require = require;
diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md
index 927a8a6f80fb58..e7b6b61c78cb0a 100644
--- a/test/fixtures/wpt/README.md
+++ b/test/fixtures/wpt/README.md
@@ -22,9 +22,9 @@ Last update:
- html/webappapis/timers: https://github.com/web-platform-tests/wpt/tree/5873f2d8f1/html/webappapis/timers
- interfaces: https://github.com/web-platform-tests/wpt/tree/fc086c82d5/interfaces
- performance-timeline: https://github.com/web-platform-tests/wpt/tree/17ebc3aea0/performance-timeline
-- resources: https://github.com/web-platform-tests/wpt/tree/fbee645164/resources
+- resources: https://github.com/web-platform-tests/wpt/tree/c5b428f15a/resources
- streams: https://github.com/web-platform-tests/wpt/tree/8f60d94439/streams
-- url: https://github.com/web-platform-tests/wpt/tree/77d54aa9e0/url
+- url: https://github.com/web-platform-tests/wpt/tree/0e5b126cd0/url
- user-timing: https://github.com/web-platform-tests/wpt/tree/df24fb604e/user-timing
- wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/1dd414c796/wasm/jsapi
- wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi
diff --git a/test/fixtures/wpt/resources/accesskey.js b/test/fixtures/wpt/resources/accesskey.js
new file mode 100644
index 00000000000000..e95c9d21e58d52
--- /dev/null
+++ b/test/fixtures/wpt/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:
+ *
+ *
+ *
+*/
+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/test/fixtures/wpt/resources/blank.html b/test/fixtures/wpt/resources/blank.html
new file mode 100644
index 00000000000000..edeaa45bb62078
--- /dev/null
+++ b/test/fixtures/wpt/resources/blank.html
@@ -0,0 +1,16 @@
+
+
+
+
+ Blank Page
+
+
+
+
+
diff --git a/test/fixtures/wpt/resources/channel.sub.js b/test/fixtures/wpt/resources/channel.sub.js
new file mode 100644
index 00000000000000..7385a65f6e4abb
--- /dev/null
+++ b/test/fixtures/wpt/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 explictly 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
+ * `_.
+ *
+ * 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/test/fixtures/wpt/resources/check-layout-th.js b/test/fixtures/wpt/resources/check-layout-th.js
index a507a8dfd7f197..9cd8abc938d9fb 100644
--- a/test/fixtures/wpt/resources/check-layout-th.js
+++ b/test/fixtures/wpt/resources/check-layout-th.js
@@ -20,7 +20,7 @@ function checkAttribute(output, node, attribute)
function assert_tolerance(actual, expected, message)
{
- if (isNaN(expected) || Math.abs(actual - expected) >= 1) {
+ if (isNaN(expected) || isNaN(actual) || Math.abs(actual - expected) >= 1) {
assert_equals(actual, Number(expected), message);
}
}
diff --git a/test/fixtures/wpt/resources/check-layout.js b/test/fixtures/wpt/resources/check-layout.js
new file mode 100644
index 00000000000000..8634481497d701
--- /dev/null
+++ b/test/fixtures/wpt/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/test/fixtures/wpt/resources/idlharness-shadowrealm.js b/test/fixtures/wpt/resources/idlharness-shadowrealm.js
new file mode 100644
index 00000000000000..631278db22d8fb
--- /dev/null
+++ b/test/fixtures/wpt/resources/idlharness-shadowrealm.js
@@ -0,0 +1,81 @@
+// 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) {
+ const script_urls = [
+ "/resources/testharness.js",
+ "/resources/WebIDLParser.js",
+ "/resources/idlharness.js",
+ ];
+ 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; },
+ };
+ `);
+
+ const ss = await Promise.all(script_urls.map(url => fetch_text(url)));
+ for (const s of ss) {
+ realm.evaluate(s);
+ }
+ const specs = await Promise.all(srcs.concat(deps).map(spec => {
+ return fetch_text("/interfaces/" + spec + ".idl");
+ }));
+ const idls = JSON.stringify(specs);
+
+ const results = JSON.parse(await new Promise(
+ realm.evaluate(`(resolve,reject) => {
+ const idls = ${idls};
+ add_completion_callback(function (tests, harness_status, asserts_run) {
+ resolve(JSON.stringify(tests));
+ });
+
+ // Without the wrapping test, testharness.js will think it's done after it has run
+ // the first idlharness test.
+ test(() => {
+ 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();
+ }, "setup");
+ }`)
+ ));
+
+ // We ran the tests in the ShadowRealm and gathered the results. Now treat them as if
+ // we'd run them directly here, so we can see them.
+ for (const {name, status, message} of results) {
+ // TODO: make this an API in testharness.js - needs RFC?
+ promise_test(t => {t.set_status(status, message); t.phase = t.phases.HAS_RESULT; t.done()}, name);
+ }
+ }, "outer setup");
+}
+// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker:
diff --git a/test/fixtures/wpt/resources/idlharness.js b/test/fixtures/wpt/resources/idlharness.js
index d81693d2a2226d..b5eed06ce3e138 100644
--- a/test/fixtures/wpt/resources/idlharness.js
+++ b/test/fixtures/wpt/resources/idlharness.js
@@ -508,7 +508,7 @@ IdlArray.prototype.is_json_type = function(type)
{
/**
* Checks whether type is a JSON type as per
- * https://heycam.github.io/webidl/#dfn-json-types
+ * https://webidl.spec.whatwg.org/#dfn-json-types
*/
var idlType = type.idlType;
@@ -637,11 +637,17 @@ function exposure_set(object, default_set) {
if (exposed && exposed.length) {
const { rhs } = exposed[0];
// Could be a list or a string.
- const set = rhs.type === "identifier-list" ?
+ 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");
@@ -652,6 +658,9 @@ function exposure_set(object, default_set) {
}
function exposed_in(globals) {
+ if (globals === "*") {
+ return true;
+ }
if ('Window' in self) {
return globals.has("Window");
}
@@ -667,6 +676,10 @@ function exposed_in(globals) {
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");
}
@@ -804,6 +817,13 @@ IdlArray.prototype.merge_partials = function()
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(
@@ -942,7 +962,7 @@ IdlArray.prototype.assert_type_is = function(value, type)
return;
}
- if (type.generic === "sequence")
+ if (type.generic === "sequence" || type.generic == "ObservableArray")
{
assert_true(Array.isArray(value), "should be an Array");
if (!value.length)
@@ -1255,7 +1275,7 @@ IdlInterface.prototype.is_global = function()
/**
* Value of the LegacyNamespace extended attribute, if any.
*
- * https://heycam.github.io/webidl/#LegacyNamespace
+ * https://webidl.spec.whatwg.org/#LegacyNamespace
*/
IdlInterface.prototype.get_legacy_namespace = function()
{
@@ -1299,7 +1319,7 @@ IdlInterface.prototype.get_interface_object = function() {
};
IdlInterface.prototype.get_qualified_name = function() {
- // https://heycam.github.io/webidl/#qualified-name
+ // https://webidl.spec.whatwg.org/#qualified-name
var legacyNamespace = this.get_legacy_namespace();
if (legacyNamespace) {
return legacyNamespace + "." + this.name;
@@ -1320,7 +1340,7 @@ IdlInterface.prototype.has_default_to_json_regular_operation = function() {
};
/**
- * Implementation of https://heycam.github.io/webidl/#create-an-inheritance-stack
+ * 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
@@ -1358,7 +1378,7 @@ IdlInterface.prototype.get_reverse_inheritance_stack = function() {
/**
* Implementation of
- * https://heycam.github.io/webidl/#default-tojson-operation
+ * https://webidl.spec.whatwg.org/#default-tojson-operation
* for testing purposes.
*
* Collects the IDL types of the attributes that meet the criteria
@@ -1524,7 +1544,7 @@ IdlInterface.prototype.test_self = function()
if (this.should_have_interface_object() && !this.is_callback()) {
subsetTestByKey(this.name, test, function() {
// This function tests WebIDL as of 2014-10-25.
- // https://heycam.github.io/webidl/#es-interface-call
+ // https://webidl.spec.whatwg.org/#es-interface-call
this.assert_interface_object_exists();
@@ -1549,7 +1569,7 @@ IdlInterface.prototype.test_self = function()
if (this.should_have_interface_object()) {
subsetTestByKey(this.name, test, function() {
// This function tests WebIDL as of 2015-11-17.
- // https://heycam.github.io/webidl/#interface-object
+ // https://webidl.spec.whatwg.org/#interface-object
this.assert_interface_object_exists();
@@ -1580,7 +1600,7 @@ IdlInterface.prototype.test_self = function()
if (this.is_callback()) {
throw new IdlHarnessError("Invalid IDL: LegacyWindowAlias extended attribute on non-interface " + this.name);
}
- if (!this.exposureSet.has("Window")) {
+ 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,
@@ -1741,7 +1761,7 @@ IdlInterface.prototype.test_self = function()
subsetTestByKey(this.name, test, function()
{
// This function tests WebIDL as of 2015-01-21.
- // https://heycam.github.io/webidl/#interface-object
+ // https://webidl.spec.whatwg.org/#interface-object
if (!this.should_have_interface_object()) {
return;
@@ -1860,7 +1880,7 @@ IdlInterface.prototype.test_self = function()
// 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://heycam.github.io/webidl/#interface-prototype-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);
}
@@ -2237,7 +2257,7 @@ IdlInterface.prototype.test_member_operation = function(member)
a_test.step(function()
{
// This function tests WebIDL as of 2015-12-29.
- // https://heycam.github.io/webidl/#es-operations
+ // https://webidl.spec.whatwg.org/#es-operations
if (!this.should_have_interface_object()) {
a_test.done();
@@ -2666,7 +2686,7 @@ IdlInterface.prototype.test_primary_interface_of = function(desc, obj, exception
// attribute must execute the same algorithm as is defined for the
// [[SetPrototypeOf]] internal method of an immutable prototype exotic
// object."
- // https://heycam.github.io/webidl/#platform-object-setprototypeof
+ // https://webidl.spec.whatwg.org/#platform-object-setprototypeof
if (this.is_global())
{
this.test_immutable_prototype("global platform object", obj);
@@ -3332,9 +3352,9 @@ IdlNamespace.prototype.test = function ()
* idl_test is a promise_test wrapper that handles the fetching of the IDL,
* avoiding repetitive boilerplate.
*
- * @param {String|String[]} srcs Spec name(s) for source idl files (fetched from
+ * @param {String[]} srcs Spec name(s) for source idl files (fetched from
* /interfaces/{name}.idl).
- * @param {String|String[]} deps Spec name(s) for dependency idl files (fetched
+ * @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).
diff --git a/test/fixtures/wpt/resources/sriharness.js b/test/fixtures/wpt/resources/sriharness.js
index d57a1b38465d64..943d677224f2f2 100644
--- a/test/fixtures/wpt/resources/sriharness.js
+++ b/test/fixtures/wpt/resources/sriharness.js
@@ -1,17 +1,30 @@
-var SRIScriptTest = function(pass, name, src, integrityValue, crossoriginValue, nonce) {
+// `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;
- e.setAttribute("integrity", this.integrityValue);
+ if (this.integrityValue) {
+ e.setAttribute("integrity", this.integrityValue);
+ }
if(this.crossoriginValue) {
e.setAttribute("crossorigin", this.crossoriginValue);
}
@@ -30,6 +43,12 @@ SRIScriptTest.prototype.execute = function() {
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) {
diff --git a/test/fixtures/wpt/resources/testdriver-actions.js b/test/fixtures/wpt/resources/testdriver-actions.js
index 4dafa0c018b101..3e5ba74b4cab35 100644
--- a/test/fixtures/wpt/resources/testdriver-actions.js
+++ b/test/fixtures/wpt/resources/testdriver-actions.js
@@ -2,9 +2,38 @@
let sourceNameIdx = 0;
/**
+ * @class
* Builder for creating a sequence of actions
- * The default tick duration is set to 16ms, which is one frame time based on
- * 60Hz display.
+ *
+ *
+ * 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],
diff --git a/test/fixtures/wpt/resources/testdriver.js b/test/fixtures/wpt/resources/testdriver.js
index f2df26cda1ccdf..0737e64a50b313 100644
--- a/test/fixtures/wpt/resources/testdriver.js
+++ b/test/fixtures/wpt/resources/testdriver.js
@@ -46,7 +46,7 @@
/**
- * @namespace
+ * @namespace {test_driver}
*/
window.test_driver = {
/**
@@ -78,9 +78,17 @@
* Trigger user interaction in order to grant additional privileges to
* a provided function.
*
- * https://html.spec.whatwg.org/#triggered-by-user-activation
+ * See `triggered by user activation
+ * `_.
*
- * @param {String} intent - a description of the action which much be
+ * @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
@@ -118,9 +126,21 @@
/**
* Triggers a user-initiated click
*
- * This matches the behaviour of the {@link
- * https://w3c.github.io/webdriver/#element-click|WebDriver
- * Element Click command}.
+ * If ``element`` isn't inside the
+ * viewport, it will be scrolled into view before the click
+ * occurs.
+ *
+ * If ``element`` is from a different browsing context, the
+ * command will be run in that context.
+ *
+ * Matches the behaviour of the `Element Click
+ * `_
+ * WebDriver command.
+ *
+ * **Note:** If the element to be clicked does not have a
+ * unique ID, the document must not have any DOM mutations
+ * made between the function being called and the promise
+ * settling.
*
* @param {Element} element - element to be clicked
* @returns {Promise} fulfilled after click occurs, or rejected in
@@ -149,9 +169,9 @@
/**
* Deletes all cookies.
*
- * This matches the behaviour of the {@link
- * https://w3c.github.io/webdriver/#delete-all-cookies|WebDriver
- * Delete All Cookies command}.
+ * Matches the behaviour of the `Delete All Cookies
+ * `_
+ * WebDriver command.
*
* @param {WindowProxy} context - Browsing context in which
* to run the call, or null for the current
@@ -165,11 +185,34 @@
},
/**
- * Send keys to an element
+ * 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.
+ *
+ * To send special keys, send the respective key's codepoint,
+ * as defined by `WebDriver
+ * `_. For
+ * example, the "tab" key is represented as "``\uE004``".
*
- * This matches the behaviour of the {@link
- * https://w3c.github.io/webdriver/#element-send-keys|WebDriver
- * Send Keys command}.
+ * **Note:** these special-key codepoints are not necessarily
+ * what you would expect. For example, Esc is the
+ * invalid Unicode character ``\uE00C``, not the ``\u001B`` Escape
+ * character from ASCII.
+ *
+ * This matches the behaviour of the
+ * `Send Keys
+ * `_
+ * WebDriver command.
+ *
+ * **Note:** If the element to be clicked does not have a
+ * unique ID, the document must not have any DOM mutations
+ * made between the function being called and the promise
+ * settling.
*
* @param {Element} element - element to send keys to
* @param {String} keys - keys to send to the element
@@ -196,9 +239,8 @@
* Freeze the current page
*
* The freeze function transitions the page from the HIDDEN state to
- * the FROZEN state as described in {@link
- * https://github.com/WICG/page-lifecycle/blob/master/README.md|Lifecycle API
- * for Web Pages}
+ * the FROZEN state as described in `Lifecycle API for Web Pages
+ * `_.
*
* @param {WindowProxy} context - Browsing context in which
* to run the call, or null for the current
@@ -211,27 +253,76 @@
return window.test_driver_internal.freeze();
},
+ /**
+ * Minimizes the browser window.
+ *
+ * Matches the the behaviour of the `Minimize
+ * `_
+ * WebDriver command
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled with the previous {@link
+ * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect}
+ * value, after the window is minimized.
+ */
+ minimize_window: function(context=null) {
+ return window.test_driver_internal.minimize_window(context);
+ },
+
+ /**
+ * Restore the window from minimized/maximized state to a given rect.
+ *
+ * Matches the behaviour of the `Set Window Rect
+ * `_
+ * WebDriver command
+ *
+ * @param {Object} rect - A {@link
+ * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect}
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the window is restored to the given rect.
+ */
+ set_window_rect: function(rect, context=null) {
+ return window.test_driver_internal.set_window_rect(rect, context);
+ },
+
/**
* Send a sequence of actions
*
- * This function sends a sequence of actions
- * to perform. It is modeled after the behaviour of {@link
- * https://w3c.github.io/webdriver/#actions|WebDriver Actions Command}
- *
- * @param {Array} actions - an array of actions. The format is the same as the actions
- * property of the WebDriver command {@link
- * https://w3c.github.io/webdriver/#perform-actions|Perform
- * Actions} 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
+ * This function sends a sequence of actions to perform.
+ *
+ * Matches the behaviour of the `Actions
+ * `_ feature in
+ * WebDriver.
+ *
+ * Authors are encouraged to use the
+ * :js:class:`test_driver.Actions` builder rather than
+ * invoking this API directly.
+ *
+ * @param {Array} actions - an array of actions. The format is
+ * the same as the actions property
+ * of the `Perform Actions
+ * `_
+ * WebDriver command. Each element is
+ * an object representing an input
+ * source and each input source
+ * itself has an actions property
+ * detailing the behaviour of that
+ * source at each timestep (or
+ * tick). Authors are not expected to
+ * construct the actions sequence by
+ * hand, but to use the builder api
+ * provided in testdriver-actions.js
* @param {WindowProxy} context - Browsing context in which
* to run the call, or null for the current
* browsing context.
*
- * @returns {Promise} fufiled after the actions are performed, or rejected in
+ * @returns {Promise} fulfilled after the actions are performed, or rejected in
* the cases the WebDriver command errors
*/
action_sequence: function(actions, context=null) {
@@ -241,9 +332,12 @@
/**
* Generates a test report on the current page
*
- * The generate_test_report function generates a report (to be observed
- * by ReportingObserver) for testing purposes, as described in
- * {@link https://w3c.github.io/reporting/#generate-test-report-command}
+ * The generate_test_report function generates a report (to be
+ * observed by ReportingObserver) for testing purposes.
+ *
+ * Matches the `Generate Test Report
+ * `_
+ * WebDriver command.
*
* @param {WindowProxy} context - Browsing context in which
* to run the call, or null for the current
@@ -259,21 +353,25 @@
/**
* Sets the state of a permission
*
- * This function simulates a user setting a permission into a particular state as described
- * in {@link https://w3c.github.io/permissions/#set-permission-command}
+ * This function simulates a user setting a permission into a
+ * particular state.
+ *
+ * Matches the `Set Permission
+ * `_
+ * WebDriver command.
+ *
+ * @example
+ * await test_driver.set_permission({ name: "background-fetch" }, "denied");
+ * await test_driver.set_permission({ name: "push", userVisibleOnly: true }, "granted", true);
*
- * @param {Object} descriptor - a [PermissionDescriptor]{@link
- * https://w3c.github.io/permissions/#dictdef-permissiondescriptor}
+ * @param {Object} descriptor - a `PermissionDescriptor
+ * `_
* object
* @param {String} state - the state of the permission
* @param {boolean} one_realm - Optional. Whether the permission applies to only one realm
* @param {WindowProxy} context - Browsing context in which
* to run the call, or null for the current
* browsing context.
- *
- * The above params are used to create a [PermissionSetParameters]{@link
- * https://w3c.github.io/permissions/#dictdef-permissionsetparameters} object
- *
* @returns {Promise} fulfilled after the permission is set, or rejected if setting the
* permission fails
*/
@@ -289,12 +387,15 @@
/**
* Creates a virtual authenticator
*
- * This function creates a virtual authenticator for use with the U2F
- * and WebAuthn APIs as described in {@link
- * https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator}
+ * This function creates a virtual authenticator for use with
+ * the U2F and WebAuthn APIs.
*
- * @param {Object} config - an [Authenticator Configuration]{@link
- * https://w3c.github.io/webauthn/#authenticator-configuration}
+ * Matches the `Add Virtual Authenticator
+ * `_
+ * WebDriver command.
+ *
+ * @param {Object} config - an `Authenticator Configuration
+ * `_
* object
* @param {WindowProxy} context - Browsing context in which
* to run the call, or null for the current
@@ -311,9 +412,12 @@
/**
* Removes a virtual authenticator
*
- * This function removes a virtual authenticator that has been created
- * by add_virtual_authenticator
- * https://w3c.github.io/webauthn/#sctn-automation-remove-virtual-authenticator
+ * This function removes a virtual authenticator that has been
+ * created by :js:func:`add_virtual_authenticator`.
+ *
+ * Matches the `Remove Virtual Authenticator
+ * `_
+ * WebDriver command.
*
* @param {String} authenticator_id - the ID of the authenticator to be
* removed.
@@ -332,11 +436,13 @@
/**
* Adds a credential to a virtual authenticator
*
- * https://w3c.github.io/webauthn/#sctn-automation-add-credential
+ * Matches the `Add Credential
+ * `_
+ * WebDriver command.
*
* @param {String} authenticator_id - the ID of the authenticator
- * @param {Object} credential - A [Credential Parameters]{@link
- * https://w3c.github.io/webauthn/#credential-parameters}
+ * @param {Object} credential - A `Credential Parameters
+ * `_
* object
* @param {WindowProxy} context - Browsing context in which
* to run the call, or null for the current
@@ -356,18 +462,21 @@
* This function retrieves all the credentials (added via the U2F API,
* WebAuthn, or the add_credential function) stored in a virtual
* authenticator
- * https://w3c.github.io/webauthn/#sctn-automation-get-credentials
+ *
+ * Matches the `Get Credentials
+ * `_
+ * WebDriver command.
*
* @param {String} authenticator_id - the ID of the authenticator
* @param {WindowProxy} context - Browsing context in which
* to run the call, or null for the current
* browsing context.
*
- * @returns {Promise} fulfilled after the credentials are returned, or
- * rejected in the cases the WebDriver command
- * errors. Returns an array of [Credential
- * Parameters]{@link
- * https://w3c.github.io/webauthn/#credential-parameters}
+ * @returns {Promise} fulfilled after the credentials are
+ * returned, or rejected in the cases the
+ * WebDriver command errors. Returns an
+ * array of `Credential Parameters
+ * `_
*/
get_credentials: function(authenticator_id, context=null) {
return window.test_driver_internal.get_credentials(authenticator_id, context=null);
@@ -376,7 +485,9 @@
/**
* Remove a credential stored in an authenticator
*
- * https://w3c.github.io/webauthn/#sctn-automation-remove-credential
+ * Matches the `Remove Credential
+ * `_
+ * WebDriver command.
*
* @param {String} authenticator_id - the ID of the authenticator
* @param {String} credential_id - the ID of the credential
@@ -395,7 +506,9 @@
/**
* Removes all the credentials stored in a virtual authenticator
*
- * https://w3c.github.io/webauthn/#sctn-automation-remove-all-credentials
+ * Matches the `Remove All Credentials
+ * `_
+ * WebDriver command.
*
* @param {String} authenticator_id - the ID of the authenticator
* @param {WindowProxy} context - Browsing context in which
@@ -415,7 +528,10 @@
*
* Sets whether requests requiring user verification will succeed or
* fail on a given virtual authenticator
- * https://w3c.github.io/webauthn/#sctn-automation-set-user-verified
+ *
+ * Matches the `Set User Verified
+ * `_
+ * WebDriver command.
*
* @param {String} authenticator_id - the ID of the authenticator
* @param {boolean} uv - the User Verified flag
@@ -431,7 +547,9 @@
* Sets the storage access rule for an origin when embedded
* in a third-party context.
*
- * {@link https://privacycg.github.io/storage-access/#set-storage-access-command}
+ * Matches the `Set Storage Access
+ * `_
+ * WebDriver command.
*
* @param {String} origin - A third-party origin to block or allow.
* May be "*" to indicate all origins.
@@ -455,6 +573,45 @@
const blocked = state === "blocked";
return window.test_driver_internal.set_storage_access(origin, embedding_origin, blocked, context);
},
+
+ /**
+ * Sets the current transaction automation mode for Secure Payment
+ * Confirmation.
+ *
+ * This function places `Secure Payment
+ * Confirmation `_ into
+ * an automated 'autoaccept' or 'autoreject' mode, to allow testing
+ * without user interaction with the transaction UX prompt.
+ *
+ * Matches the `Set SPC Transaction Mode
+ * `_
+ * WebDriver command.
+ *
+ * @example
+ * await test_driver.set_spc_transaction_mode("autoaccept");
+ * test.add_cleanup(() => {
+ * return test_driver.set_spc_transaction_mode("none");
+ * });
+ *
+ * // Assumption: `request` is a PaymentRequest with a secure-payment-confirmation
+ * // payment method.
+ * const response = await request.show();
+ *
+ * @param {String} mode - The `transaction mode
+ * `_
+ * to set. Must be one of "``none``",
+ * "``autoaccept``", or
+ * "``autoreject``".
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the transaction mode has been set,
+ * or rejected if setting the mode fails.
+ */
+ set_spc_transaction_mode: function(mode, context=null) {
+ return window.test_driver_internal.set_spc_transaction_mode(mode, context);
+ },
};
window.test_driver_internal = {
@@ -516,6 +673,14 @@
return Promise.reject(new Error("unimplemented"));
},
+ minimize_window: function(context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ set_window_rect: function(rect, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
action_sequence: function(actions, context=null) {
return Promise.reject(new Error("unimplemented"));
},
@@ -560,5 +725,10 @@
set_storage_access: function(origin, embedding_origin, blocked, context=null) {
return Promise.reject(new Error("unimplemented"));
},
+
+ set_spc_transaction_mode: function(mode, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
};
})();
diff --git a/test/fixtures/wpt/resources/testharness.js b/test/fixtures/wpt/resources/testharness.js
index f85b19fd9bd90c..bfb5cc087775da 100644
--- a/test/fixtures/wpt/resources/testharness.js
+++ b/test/fixtures/wpt/resources/testharness.js
@@ -126,7 +126,7 @@
} catch (e) {}
}
}
- if (supports_post_message(w) && w !== self) {
+ if (w !== self) {
w.postMessage(message_arg, "*");
}
});
@@ -424,6 +424,53 @@
}
};
+ /*
+ * 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.
*
@@ -488,6 +535,15 @@
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();
}
@@ -542,8 +598,23 @@
return test_environment.next_default_test_name();
}
- /*
- * API functions
+ /**
+ * @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)
{
@@ -576,6 +647,17 @@
}
}
+ /**
+ * 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) {
@@ -619,6 +701,19 @@
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;
@@ -678,11 +773,12 @@
* 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 https://github.com/whatwg/html/issues/5319 for a
+ * 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`_ for a
* particular case.
*
* In functions we define here, there is an expectation from the caller
@@ -695,6 +791,16 @@
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))
@@ -707,9 +813,6 @@
/**
* Assert that a Promise is rejected with the right DOMException.
*
- * @param test the test argument passed to promise_test
- * @param {number|string} type. See documentation for assert_throws_dom.
- *
* For the remaining arguments, there are two ways of calling
* promise_rejects_dom:
*
@@ -721,8 +824,22 @@
* 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" &&
@@ -745,6 +862,16 @@
});
}
+ /**
+ * 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))
@@ -755,9 +882,23 @@
}
/**
- * This constructor helper allows DOM events to be handled using Promises,
- * which can make it a lot easier to test a very specific series of events,
+ * 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)
{
@@ -806,15 +947,13 @@
* Returns a Promise that will resolve after the specified event or
* series of events has occurred.
*
- * @param 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.
+ * @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.
*
- * For example,
- *
- * ```js
+ * @example
* const watcher = new EventWatcher(t, div, [ 'animationstart',
* 'animationiteration',
* 'animationend' ]);
@@ -823,7 +962,6 @@
* assert_equals(evts[0].elapsedTime, 0.0);
* assert_equals(evts[1].elapsedTime, 2.0);
* });
- * ```
*/
this.wait_for = function(types, options) {
if (waitingFor) {
@@ -865,6 +1003,9 @@
});
};
+ /**
+ * Stop listening for events
+ */
function stop_watching() {
for (var i = 0; i < eventTypes.length; i++) {
watchedNode.removeEventListener(eventTypes[i], eventHandler, false);
@@ -877,6 +1018,46 @@
}
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;
@@ -893,7 +1074,18 @@
test_environment.on_new_harness_properties(properties);
}
- function promise_setup(func, maybe_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,
@@ -910,7 +1102,6 @@
tests.promise_tests = tests.promise_tests
.then(function()
{
- var properties = maybe_properties || {};
var result;
tests.setup(null, properties);
@@ -931,6 +1122,17 @@
});
}
+ /**
+ * 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
+ * ``explict_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
@@ -952,6 +1154,20 @@
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)
{
@@ -965,23 +1181,35 @@
});
}
- /*
- * Register a function as a DOM event listener to the given object for the
- * event bubbling phase.
+ /**
+ * @deprecated
+ *
+ * Register a function as a DOM event listener to the
+ * given object for the event bubbling phase.
*
- * This function was deprecated in November of 2019.
+ * @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);
}
- function step_timeout(f, t) {
+ /**
+ * 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);
return setTimeout(function() {
- f.apply(outer_this, args);
- }, t * tests.timeout_multiplier);
+ func.apply(outer_this, args);
+ }, timeout * tests.timeout_multiplier);
}
expose(test, 'test');
@@ -1079,8 +1307,30 @@
"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)
{
@@ -1187,12 +1437,8 @@
status = Test.statuses.PASS;
return rv;
} catch(e) {
- if (e instanceof AssertionError) {
- status = Test.statuses.FAIL;
- stack = e.stack;
- } else {
- status = Test.statuses.ERROR;
- }
+ status = Test.statuses.FAIL;
+ stack = e.stack ? e.stack : null;
throw e;
} finally {
if (tests.output && !stack) {
@@ -1206,6 +1452,12 @@
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,
@@ -1213,6 +1465,12 @@
}
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,
@@ -1232,6 +1490,17 @@
return x === y;
}
+ /**
+ * Assert that ``actual`` is the same value as ``expected``.
+ *
+ * For objects this compares by cobject 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)
{
/*
@@ -1250,18 +1519,32 @@
}
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)
{
- /*
- * Test if two primitives are unequal or two objects
- * are different objects
- */
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,
@@ -1272,6 +1555,18 @@
// 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
+ * `_ 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,
@@ -1308,6 +1603,14 @@
}
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;
@@ -1365,6 +1668,16 @@
}
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)
{
/*
@@ -1393,6 +1706,14 @@
}
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)
{
/*
@@ -1416,6 +1737,13 @@
}
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)
{
/*
@@ -1433,6 +1761,13 @@
}
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)
{
/*
@@ -1450,6 +1785,15 @@
}
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)
{
/*
@@ -1468,6 +1812,14 @@
}
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)
{
/*
@@ -1485,6 +1837,14 @@
}
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)
{
/*
@@ -1502,6 +1862,15 @@
}
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)
{
/*
@@ -1520,6 +1889,13 @@
}
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)
@@ -1531,6 +1907,14 @@
}
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 + "]";
@@ -1540,6 +1924,13 @@
}
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,
@@ -1547,6 +1938,13 @@
}
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,
@@ -1579,9 +1977,44 @@
{p:property_name});
};
}
- expose_assert(_assert_inherits("assert_inherits"), "assert_inherits");
- expose_assert(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
+ /**
+ * 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];
@@ -1604,7 +2037,7 @@
*
* @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.
+ * @param {string} [description] Error description for the case that the error is not thrown.
*/
function assert_throws_js(constructor, func, description)
{
@@ -1669,20 +2102,13 @@
}
}
+ // 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.
*
- * @param {number|string} type The expected exception name or code. See the
- * table of names and codes at
- * https://heycam.github.io/webidl/#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").
- *
- * For the remaining arguments, there are two ways of calling
- * promise_rejects_dom:
+ * 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,
@@ -1692,6 +2118,22 @@
* 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
+ * `_. 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)
{
@@ -1865,7 +2307,7 @@
*
* @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.
+ * @param {string} [description] Error description for the case that the error is not thrown.
*/
function assert_throws_exactly(exception, func, description)
{
@@ -1896,15 +2338,43 @@
}
}
+ /**
+ * 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");
- function assert_any(assert_func, actual, expected_array)
+ /**
+ * @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 args = [].slice.call(arguments, 3);
var errors = [];
var passed = false;
forEach(expected_array,
@@ -1936,7 +2406,7 @@
* 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.
+ * @param {string} [description] Error description for the case that the condition is not truthy.
*/
function assert_implements(condition, description) {
assert(!!condition, "assert_implements", description);
@@ -1954,25 +2424,37 @@
* "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.
+ * @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")
+ 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;
@@ -1983,7 +2465,9 @@
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 = [];
@@ -2003,6 +2487,16 @@
tests.push(this);
}
+ /**
+ * Enum of possible test statuses.
+ *
+ * :values:
+ * - ``PASS``
+ * - ``FAIL``
+ * - ``TIMEOUT``
+ * - ``NOTRUN``
+ * - ``PRECONDITION_FAILED``
+ */
Test.statuses = {
PASS:0,
FAIL:1,
@@ -2052,6 +2546,15 @@
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) {
@@ -2101,6 +2604,26 @@
}
};
+ /**
+ * 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;
@@ -2116,6 +2639,18 @@
};
};
+ /**
+ * 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;
@@ -2134,6 +2669,14 @@
};
};
+ /**
+ * 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() {
@@ -2141,37 +2684,68 @@
});
};
- Test.prototype.step_timeout = function(f, timeout) {
+ /**
+ * 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);
return setTimeout(this.step_func(function() {
- return f.apply(test_this, args);
+ 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.
+ *
+ * @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. The callback is called
+ * when this function returns 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) {
- /**
- * 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. The callback is called
- * when this function returns 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
- *
- **/
-
var timeout_full = timeout * tests.timeout_multiplier;
var remaining = Math.ceil(timeout_full / interval);
var test_this = this;
@@ -2192,57 +2766,62 @@
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.
+ *
+ * @param {Function} cond A function taking no arguments and
+ * returning a boolean. The callback is called
+ * when this function returns 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) {
- /**
- * 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.
- *
- * @param {Function} cond A function taking no arguments and
- * returning a boolean. The callback is called
- * when this function returns 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
- *
- **/
-
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.
+ * @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) {
- /**
- * 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.
- *
- * @param {Function} cond A function taking no arguments and
- * returning a boolean.
- * @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.
- *
- **/
-
return new Promise(resolve => {
this.step_wait_func(cond, resolve, description, timeout, interval);
});
@@ -2258,11 +2837,16 @@
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
+ * 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;
@@ -2287,6 +2871,9 @@
this.stack = stack ? stack : null;
};
+ /**
+ * Manually set the test status to ``TIMEOUT``.
+ */
Test.prototype.timeout = function()
{
this.timeout_id = null;
@@ -2295,11 +2882,24 @@
this.done();
};
- Test.prototype.force_timeout = Test.prototype.timeout;
+ /**
+ * Manually set the test status to ``TIMEOUT``.
+ *
+ * Alias for `Test.timeout <#Test.timeout>`_.
+ */
+ Test.prototype.force_timeout = function() {
+ return this.timeout();
+ };
/**
- * Update the test status, initiate "cleanup" functions, and signal test
- * completion.
+ * 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()
{
@@ -2318,7 +2918,7 @@
if (settings.debug) {
console.log("TEST DONE",
this.status,
- this.name,)
+ this.name);
}
this.cleanup();
@@ -2340,10 +2940,10 @@
* be cancelled.
*/
Test.prototype.cleanup = function() {
- var error_count = 0;
+ var errors = [];
var bad_value_count = 0;
- function on_error() {
- error_count += 1;
+ function on_error(e) {
+ errors.push(e);
// Abort tests immediately so that tests declared within subsequent
// cleanup functions are not run.
tests.abort();
@@ -2360,7 +2960,7 @@
try {
result = cleanup_callback();
} catch (e) {
- on_error();
+ on_error(e);
return;
}
@@ -2375,7 +2975,7 @@
});
if (!this._is_promise_test) {
- cleanup_done(this_obj, error_count, bad_value_count);
+ cleanup_done(this_obj, errors, bad_value_count);
} else {
all_async(results,
function(result, done) {
@@ -2388,12 +2988,12 @@
}
},
function() {
- cleanup_done(this_obj, error_count, bad_value_count);
+ 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.
@@ -2410,17 +3010,21 @@
return false;
}
- function cleanup_done(test, error_count, bad_value_count) {
- if (error_count || bad_value_count) {
+ 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 (error_count) {
- tests.status.message += ", and " + error_count + " failed";
+ 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) {
@@ -2431,8 +3035,6 @@
}
tests.status.message += ".";
-
- tests.status.stack = null;
}
test.phase = test.phases.COMPLETE;
@@ -2444,7 +3046,7 @@
test._done_callbacks.length = 0;
}
- /*
+ /**
* 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
@@ -2629,7 +3231,7 @@
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.sack);
+ tests.set_status(data.status.status, data.status.message, data.status.stack);
}
for (let assert of data.asserts) {
@@ -2671,17 +3273,29 @@
complete: RemoteContext.prototype.remote_done
};
- /*
- * Harness
+ /**
+ * @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,
@@ -2696,8 +3310,7 @@
1: "Error",
2: "Timeout",
3: "Optional Feature Unsupported"
- }
-
+ };
TestsStatus.prototype.structured_clone = function()
{
@@ -2715,13 +3328,25 @@
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;
}
@@ -2731,8 +3356,8 @@
test: this.test ? this.test.structured_clone() : null,
args: this.args,
status: this.status,
- }
- }
+ };
+ };
function Tests()
{
@@ -2942,7 +3567,8 @@
};
Tests.prototype.all_done = function() {
- return this.tests.length > 0 && test_environment.all_loaded &&
+ 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; });
@@ -3178,6 +3804,14 @@
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);
}
@@ -3191,11 +3825,68 @@
this.pending_remotes.push(this.create_remote_window(remote));
};
+ /**
+ * 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) {
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("beign_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 ``explict_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();
@@ -3203,18 +3894,49 @@
}
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);
}
@@ -3336,7 +4058,7 @@
Output.prototype.show_status = function() {
if (this.phase < this.STARTED) {
- this.init();
+ this.init({});
}
if (!this.enabled || this.phase === this.COMPLETE) {
return;
@@ -3413,7 +4135,12 @@
["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) {
@@ -3764,6 +4491,12 @@
}
}
+ /**
+ * @class
+ * Exception type that represents a failing assert.
+ *
+ * @param {string} message - Error message.
+ */
function AssertionError(message)
{
if (typeof message == "string") {
@@ -3778,14 +4511,6 @@
const get_stack = function() {
var stack = new Error().stack;
- // IE11 does not initialize 'Error.stack' until the object is thrown.
- if (!stack) {
- try {
- throw new Error();
- } catch (e) {
- stack = e.stack;
- }
- }
// 'Error.stack' is not supported in all browsers/versions
if (!stack) {
@@ -3888,21 +4613,23 @@
* 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
+ * 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 provided `values`. Two
- * arguments will be available in each
- * invocation: the value from `values` and
- * a function that must be invoked to
- * signal completion
+ * ``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
+ * ``iter_callback`` function have signaled
* completion
*/
function all_async(values, iter_callback, done_callback)
@@ -4012,43 +4739,6 @@
return "Untitled";
}
- function supports_post_message(w)
- {
- var supports;
- var type;
- // Given IE implements postMessage across nested iframes but not across
- // windows or tabs, you can't infer cross-origin communication from the presence
- // of postMessage on the current window object only.
- //
- // Touching the postMessage prop on a window can throw if the window is
- // not from the same origin AND post message is not supported in that
- // browser. So just doing an existence test here won't do, you also need
- // to wrap it in a try..catch block.
- try {
- type = typeof w.postMessage;
- if (type === "function") {
- supports = true;
- }
-
- // IE8 supports postMessage, but implements it as a host object which
- // returns "object" as its `typeof`.
- else if (type === "object") {
- supports = true;
- }
-
- // This is the case where postMessage isn't supported AND accessing a
- // window property across origins does NOT throw (e.g. old Safari browser).
- else {
- supports = false;
- }
- } catch (e) {
- // This is the case where postMessage isn't supported AND accessing a
- // window property across origins throws (e.g. old Firefox browser).
- supports = false;
- }
- return supports;
- }
-
/**
* Setup globals
*/
diff --git a/test/fixtures/wpt/resources/webidl2/build.sh b/test/fixtures/wpt/resources/webidl2/build.sh
new file mode 100644
index 00000000000000..a631268224f842
--- /dev/null
+++ b/test/fixtures/wpt/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/test/fixtures/wpt/resources/webidl2/lib/README.md b/test/fixtures/wpt/resources/webidl2/lib/README.md
index af0af3a902f2f3..1bd583269d2929 100644
--- a/test/fixtures/wpt/resources/webidl2/lib/README.md
+++ b/test/fixtures/wpt/resources/webidl2/lib/README.md
@@ -1,6 +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.
-Currently using webidl2.js@24.1.1 (372ea83eaa10f60adff49bd0f4f3ce6a11d6fbec).
-
The `webidl2.js.headers` file is a local addition to ensure the script is interpreted as UTF-8.
diff --git a/test/fixtures/wpt/resources/webidl2/lib/VERSION.md b/test/fixtures/wpt/resources/webidl2/lib/VERSION.md
new file mode 100644
index 00000000000000..10bdc008209a91
--- /dev/null
+++ b/test/fixtures/wpt/resources/webidl2/lib/VERSION.md
@@ -0,0 +1 @@
+Currently using webidl2.js@1fd6709ef9311f2ea0ed4ff0016ecf6f5d615104.
diff --git a/test/fixtures/wpt/resources/webidl2/lib/webidl2.js b/test/fixtures/wpt/resources/webidl2/lib/webidl2.js
index 322f0e11a6ae56..2861354e47da9b 100644
--- a/test/fixtures/wpt/resources/webidl2/lib/webidl2.js
+++ b/test/fixtures/wpt/resources/webidl2/lib/webidl2.js
@@ -31,6 +31,8 @@ __webpack_require__.r(__webpack_exports__);
/* 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);
+
@@ -91,6 +93,15 @@ function parseByTokens(tokeniser, options) {
}
function definition() {
+ if (options.productions) {
+ for (const production of options.productions) {
+ const result = production(tokeniser);
+ if (result) {
+ return result;
+ }
+ }
+ }
+
return (
callback() ||
interface_() ||
@@ -116,7 +127,7 @@ function parseByTokens(tokeniser, options) {
(0,_productions_helpers_js__WEBPACK_IMPORTED_MODULE_11__.autoParenter)(def).extAttrs = ea;
defs.push(def);
}
- const eof = tokeniser.consumeType("eof");
+ const eof = _productions_token_js__WEBPACK_IMPORTED_MODULE_12__.Eof.parse(tokeniser);
if (options.concrete) {
defs.push(eof);
}
@@ -132,6 +143,8 @@ function parseByTokens(tokeniser, options) {
* @param {object} [options]
* @param {*} [options.sourceName]
* @param {boolean} [options.concrete]
+ * @param {Function[]} [options.productions]
+ * @return {import("./productions/base").Base[]}
*/
function parse(str, options = {}) {
const tokeniser = new _tokeniser_js__WEBPACK_IMPORTED_MODULE_0__.Tokeniser(str);
@@ -170,7 +183,7 @@ const tokenRe = {
identifier: /[_-]?[A-Za-z][0-9A-Z_a-z-]*/y,
string: /"[^"]*"/y,
whitespace: /[\t\n\r ]+/y,
- comment: /\/\/.*|\/\*(.|\n)*?\*\//y,
+ comment: /\/\/.*|\/\*[\s\S]*?\*\//y,
other: /[^\t\n\r 0-9A-Za-z]/y,
};
@@ -184,6 +197,8 @@ const typeNameKeywords = [
"Uint16Array",
"Uint32Array",
"Uint8ClampedArray",
+ "BigInt64Array",
+ "BigUint64Array",
"Float32Array",
"Float64Array",
"any",
@@ -259,6 +274,7 @@ const punctuations = [
"=",
">",
"?",
+ "*",
"[",
"]",
"{",
@@ -355,6 +371,8 @@ function tokenise(str) {
type: "eof",
value: "",
trivia,
+ line,
+ index,
});
return tokens;
@@ -401,7 +419,7 @@ class Tokeniser {
/**
* @param {string} type
*/
- probeType(type) {
+ probeKind(type) {
return (
this.source.length > this.position &&
this.source[this.position].type === type
@@ -413,16 +431,16 @@ class Tokeniser {
*/
probe(value) {
return (
- this.probeType("inline") && this.source[this.position].value === value
+ this.probeKind("inline") && this.source[this.position].value === value
);
}
/**
- * @param {...string} candidates
+ * @param {...string} candidates
*/
- consumeType(...candidates) {
+ consumeKind(...candidates) {
for (const type of candidates) {
- if (!this.probeType(type)) continue;
+ if (!this.probeKind(type)) continue;
const token = this.source[this.position];
this.position++;
return token;
@@ -430,10 +448,10 @@ class Tokeniser {
}
/**
- * @param {...string} candidates
+ * @param {...string} candidates
*/
consume(...candidates) {
- if (!this.probeType("inline")) return;
+ if (!this.probeKind("inline")) return;
const token = this.source[this.position];
for (const value of candidates) {
if (token.value !== value) continue;
@@ -442,6 +460,19 @@ class Tokeniser {
}
}
+ /**
+ * @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
*/
@@ -523,6 +554,8 @@ function contextAsText(node) {
* @property {"error" | "warning"} [level]
* @property {Function} [autofix]
*
+ * @typedef {ReturnType} WebIDLErrorData
+ *
* @param {string} message error message
* @param {"Syntax" | "Validation"} kind error type
* @param {WebIDL2ErrorOptions} [options]
@@ -705,7 +738,7 @@ function list(tokeniser, { parser, allowDangler, listName = "list" }) {
*/
function const_value(tokeniser) {
return (
- tokeniser.consumeType("decimal", "integer") ||
+ tokeniser.consumeKind("decimal", "integer") ||
tokeniser.consume("true", "false", "Infinity", "-Infinity", "NaN")
);
}
@@ -1066,7 +1099,7 @@ 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.consumeType("identifier") ||
+ tokeniser.consumeKind("identifier") ||
tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.stringTypes, ..._tokeniser_js__WEBPACK_IMPORTED_MODULE_2__.typeNameKeywords);
if (!base) {
return;
@@ -1164,7 +1197,7 @@ class Type extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
if (this.idlType === "void") {
const message = `\`void\` is now replaced by \`undefined\`. Refer to the \
-[relevant GitHub issue](https://github.com/heycam/webidl/issues/60) \
+[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),
@@ -1457,8 +1490,13 @@ class ExtendedAttributeParameters extends _base_js__WEBPACK_IMPORTED_MODULE_0__.
const ret = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.autoParenter)(
new ExtendedAttributeParameters({ source: tokeniser.source, tokens })
);
+ ret.list = [];
if (tokens.assign) {
- tokens.secondaryName = tokeniser.consumeType(...extAttrValueSyntax);
+ tokens.asterisk = tokeniser.consume("*");
+ if (tokens.asterisk) {
+ return ret.this;
+ }
+ tokens.secondaryName = tokeniser.consumeKind(...extAttrValueSyntax);
}
tokens.open = tokeniser.consume("(");
if (tokens.open) {
@@ -1470,20 +1508,25 @@ class ExtendedAttributeParameters extends _base_js__WEBPACK_IMPORTED_MODULE_0__.
tokens.close =
tokeniser.consume(")") ||
tokeniser.error("Unexpected token in extended attribute argument list");
- } else if (ret.hasRhs && !tokens.secondaryName) {
+ } 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.secondaryName;
+ 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;
}
@@ -1492,26 +1535,17 @@ class ExtendedAttributeParameters extends _base_js__WEBPACK_IMPORTED_MODULE_0__.
/** @param {import("../writer.js").Writer)} w */
write(w) {
- function extended_attribute_listitem(item) {
- return w.ts.wrap([
- w.token(item.tokens.value),
- w.token(item.tokens.separator),
- ]);
- }
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
- ? []
- : this.list.map((p) => {
- return rhsType === "identifier-list"
- ? w.identifier(p, this.parent)
- : rhsType && rhsType.endsWith("-list")
- ? extended_attribute_listitem(p)
- : p.write(w);
- })),
+ ...this.list.map((p) => {
+ return rhsType === "identifier-list"
+ ? w.identifier(p, this.parent)
+ : p.write(w);
+ }),
w.token(this.tokens.close),
]);
}
@@ -1522,7 +1556,7 @@ class SimpleExtendedAttribute extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base
* @param {import("../tokeniser").Tokeniser} tokeniser
*/
static parse(tokeniser) {
- const name = tokeniser.consumeType("identifier");
+ const name = tokeniser.consumeKind("identifier");
if (name) {
return new SimpleExtendedAttribute({
source: tokeniser.source,
@@ -1551,7 +1585,9 @@ class SimpleExtendedAttribute extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base
}
const value = this.params.rhsIsList
? list
- : (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(tokens.secondaryName.value);
+ : this.params.tokens.secondaryName
+ ? (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.unescape)(tokens.secondaryName.value)
+ : null;
return { type, value };
}
get arguments() {
@@ -1567,7 +1603,7 @@ class SimpleExtendedAttribute extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base
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/heycam/webidl/pull/609) for more \
+[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,
@@ -1579,7 +1615,7 @@ information.`;
} 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/heycam/webidl/pull/870) for more \
+[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",
@@ -1638,9 +1674,12 @@ class ExtendedAttributes extends _array_base_js__WEBPACK_IMPORTED_MODULE_1__.Arr
);
tokens.close =
tokeniser.consume("]") ||
- tokeniser.error("Unexpected closing token of extended attribute");
+ tokeniser.error(
+ "Expected a closing token for the extended attribute list"
+ );
if (!ret.length) {
- tokeniser.error("Found an empty extended attribute");
+ tokeniser.unconsume(tokens.close.index);
+ tokeniser.error("An extended attribute list must not be empty");
}
if (tokeniser.probe("[")) {
tokeniser.error(
@@ -1696,7 +1735,8 @@ class ArrayBase extends Array {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
-/* harmony export */ "WrappedToken": () => (/* binding */ WrappedToken)
+/* harmony export */ "WrappedToken": () => (/* binding */ WrappedToken),
+/* harmony export */ "Eof": () => (/* binding */ Eof)
/* 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);
@@ -1712,7 +1752,7 @@ class WrappedToken extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
*/
static parser(tokeniser, type) {
return () => {
- const value = tokeniser.consumeType(type);
+ const value = tokeniser.consumeKind(type);
if (value) {
return new WrappedToken({
source: tokeniser.source,
@@ -1725,6 +1765,30 @@ class WrappedToken extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
get value() {
return (0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.unescape)(this.tokens.value.value);
}
+
+ /** @param {import("../writer").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").Tokeniser} tokeniser
+ */
+ static parse(tokeniser) {
+ const value = tokeniser.consumeKind("eof");
+ if (value) {
+ return new Eof({ source: tokeniser.source, tokens: { value } });
+ }
+ }
+
+ get type() {
+ return "eof";
+ }
}
@@ -1774,7 +1838,7 @@ class Argument extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
tokens.variadic = tokeniser.consume("...");
}
tokens.name =
- tokeniser.consumeType("identifier") ||
+ tokeniser.consumeKind("identifier") ||
tokeniser.consume(..._tokeniser_js__WEBPACK_IMPORTED_MODULE_4__.argumentNameKeywords);
if (!tokens.name) {
return tokeniser.unconsume(start_position);
@@ -1877,9 +1941,9 @@ function autofixDictionaryArgumentOptionality(arg) {
return () => {
const firstToken = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.getFirstToken)(arg.idlType);
arg.tokens.optional = {
+ ...firstToken,
type: "optional",
value: "optional",
- trivia: firstToken.trivia,
};
firstToken.trivia = " ";
autofixOptionalDictionaryDefaultValue(arg)();
@@ -1920,7 +1984,7 @@ class Default extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
}
const def =
(0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.const_value)(tokeniser) ||
- tokeniser.consumeType("string") ||
+ tokeniser.consumeKind("string") ||
tokeniser.consume("null", "[", "{") ||
tokeniser.error("No value for default");
const expression = [def];
@@ -2010,7 +2074,7 @@ class Operation extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
ret.idlType =
(0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.return_type)(tokeniser) || tokeniser.error("Missing return type");
tokens.name =
- tokeniser.consumeType("identifier") || tokeniser.consume("includes");
+ 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);
@@ -2135,7 +2199,7 @@ class Attribute extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base {
(0,_helpers_js__WEBPACK_IMPORTED_MODULE_3__.type_with_extended_attributes)(tokeniser, "attribute-type") ||
tokeniser.error("Attribute lacks a type");
tokens.name =
- tokeniser.consumeType("identifier") ||
+ tokeniser.consumeKind("identifier") ||
tokeniser.consume("async", "required") ||
tokeniser.error("Attribute lacks a name");
tokens.termination =
@@ -2233,7 +2297,7 @@ class EnumValue extends _token_js__WEBPACK_IMPORTED_MODULE_1__.WrappedToken {
* @param {import("../tokeniser").Tokeniser} tokeniser
*/
static parse(tokeniser) {
- const value = tokeniser.consumeType("string");
+ const value = tokeniser.consumeKind("string");
if (value) {
return new EnumValue({ source: tokeniser.source, tokens: { value } });
}
@@ -2272,7 +2336,7 @@ class Enum extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base {
return;
}
tokens.name =
- tokeniser.consumeType("identifier") ||
+ 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;
@@ -2282,7 +2346,7 @@ class Enum extends _base_js__WEBPACK_IMPORTED_MODULE_2__.Base {
allowDangler: true,
listName: "enumeration",
});
- if (tokeniser.probeType("string")) {
+ if (tokeniser.probeKind("string")) {
tokeniser.error("No comma between enum values");
}
tokens.close =
@@ -2340,7 +2404,7 @@ class Includes extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
* @param {import("../tokeniser").Tokeniser} tokeniser
*/
static parse(tokeniser) {
- const target = tokeniser.consumeType("identifier");
+ const target = tokeniser.consumeKind("identifier");
if (!target) {
return;
}
@@ -2351,7 +2415,7 @@ class Includes extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
return;
}
tokens.mixin =
- tokeniser.consumeType("identifier") ||
+ tokeniser.consumeKind("identifier") ||
tokeniser.error("Incomplete includes statement");
tokens.termination =
tokeniser.consume(";") ||
@@ -2414,7 +2478,7 @@ class Typedef extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
(0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "typedef-type") ||
tokeniser.error("Typedef lacks a type");
tokens.name =
- tokeniser.consumeType("identifier") ||
+ tokeniser.consumeKind("identifier") ||
tokeniser.error("Typedef lacks a name");
tokeniser.current = ret.this;
tokens.termination =
@@ -2473,7 +2537,7 @@ class CallbackFunction extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
new CallbackFunction({ source: tokeniser.source, tokens })
);
tokens.name =
- tokeniser.consumeType("identifier") ||
+ tokeniser.consumeKind("identifier") ||
tokeniser.error("Callback lacks a name");
tokeniser.current = ret.this;
tokens.assign =
@@ -2578,7 +2642,6 @@ class Interface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
tokeniser,
new Interface({ source: tokeniser.source, tokens }),
{
- type: "interface",
inheritable: !partial,
allowedMembers: [
[_constant_js__WEBPACK_IMPORTED_MODULE_3__.Constant.parse],
@@ -2737,7 +2800,7 @@ function inheritance(tokeniser) {
return {};
}
const inheritance =
- tokeniser.consumeType("identifier") ||
+ tokeniser.consumeKind("identifier") ||
tokeniser.error("Inheritance lacks a type");
return { colon, inheritance };
}
@@ -2749,11 +2812,11 @@ class Container extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
* @param {T} instance
* @param {*} args
*/
- static parse(tokeniser, instance, { type, inheritable, allowedMembers }) {
- const { tokens } = instance;
+ static parse(tokeniser, instance, { inheritable, allowedMembers }) {
+ const { tokens, type } = instance;
tokens.name =
- tokeniser.consumeType("identifier") ||
- tokeniser.error(`Missing name in ${instance.type}`);
+ tokeniser.consumeKind("identifier") ||
+ tokeniser.error(`Missing name in ${type}`);
tokeniser.current = instance;
instance = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.autoParenter)(instance);
if (inheritable) {
@@ -2870,7 +2933,7 @@ class Constant extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
let idlType = (0,_helpers_js__WEBPACK_IMPORTED_MODULE_2__.primitive_type)(tokeniser);
if (!idlType) {
const base =
- tokeniser.consumeType("identifier") ||
+ tokeniser.consumeKind("identifier") ||
tokeniser.error("Const lacks a type");
idlType = new _type_js__WEBPACK_IMPORTED_MODULE_1__.Type({ source: tokeniser.source, tokens: { base } });
}
@@ -2879,7 +2942,7 @@ class Constant extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
}
idlType.type = "const-type";
tokens.name =
- tokeniser.consumeType("identifier") ||
+ tokeniser.consumeKind("identifier") ||
tokeniser.error("Const lacks a name");
tokens.assign =
tokeniser.consume("=") || tokeniser.error("Const lacks value assignment");
@@ -3199,7 +3262,6 @@ class Mixin extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
tokeniser,
new Mixin({ source: tokeniser.source, tokens }),
{
- type: "interface mixin",
allowedMembers: [
[_constant_js__WEBPACK_IMPORTED_MODULE_1__.Constant.parse],
[_helpers_js__WEBPACK_IMPORTED_MODULE_4__.stringifier],
@@ -3247,7 +3309,6 @@ class Dictionary extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
tokeniser,
new Dictionary({ source: tokeniser.source, tokens }),
{
- type: "dictionary",
inheritable: !partial,
allowedMembers: [[_field_js__WEBPACK_IMPORTED_MODULE_1__.Field.parse]],
}
@@ -3291,7 +3352,7 @@ class Field extends _base_js__WEBPACK_IMPORTED_MODULE_0__.Base {
(0,_helpers_js__WEBPACK_IMPORTED_MODULE_1__.type_with_extended_attributes)(tokeniser, "dictionary-type") ||
tokeniser.error("Dictionary member lacks a type");
tokens.name =
- tokeniser.consumeType("identifier") ||
+ 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)
@@ -3371,7 +3432,6 @@ class Namespace extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Container {
tokeniser,
new Namespace({ source: tokeniser.source, tokens }),
{
- type: "namespace",
allowedMembers: [
[_attribute_js__WEBPACK_IMPORTED_MODULE_1__.Attribute.parse, { noInherit: true, readonly: true }],
[_constant_js__WEBPACK_IMPORTED_MODULE_5__.Constant.parse],
@@ -3441,7 +3501,6 @@ class CallbackInterface extends _container_js__WEBPACK_IMPORTED_MODULE_0__.Conta
tokeniser,
new CallbackInterface({ source: tokeniser.source, tokens }),
{
- type: "callback interface",
inheritable: !partial,
allowedMembers: [
[_constant_js__WEBPACK_IMPORTED_MODULE_2__.Constant.parse],
@@ -3525,13 +3584,7 @@ function write(ast, { templates: ts = templates } = {}) {
const w = new Writer(ts);
- function dispatch(it) {
- if (it.type === "eof") {
- return ts.trivia(it.trivia);
- }
- return it.write(w);
- }
- return ts.wrap(ast.map(dispatch));
+ return ts.wrap(ast.map((it) => it.write(w)));
}
@@ -3544,6 +3597,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export */ "validate": () => (/* binding */ validate)
/* harmony export */ });
/* harmony import */ var _error_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3);
+// @ts-check
+
function getMixinMap(all, unique) {
@@ -3632,7 +3687,8 @@ function flatten(array) {
}
/**
- * @param {*} ast AST or array of ASTs
+ * @param {import("./productions/base").Base[]} ast
+ * @return {import("./error").WebIDLErrorData[]} validation errors
*/
function validate(ast) {
return [...validateIterable(flatten(ast))];
diff --git a/test/fixtures/wpt/url/idlharness-shadowrealm.window.js b/test/fixtures/wpt/url/idlharness-shadowrealm.window.js
new file mode 100644
index 00000000000000..2373f84e0e251a
--- /dev/null
+++ b/test/fixtures/wpt/url/idlharness-shadowrealm.window.js
@@ -0,0 +1,2 @@
+// META: script=/resources/idlharness-shadowrealm.js
+idl_test_shadowrealm(["url"], []);
diff --git a/test/fixtures/wpt/url/resources/a-element-origin.js b/test/fixtures/wpt/url/resources/a-element-origin.js
index 3b8cb1cbbe7c75..cb7d4a895c40c4 100644
--- a/test/fixtures/wpt/url/resources/a-element-origin.js
+++ b/test/fixtures/wpt/url/resources/a-element-origin.js
@@ -16,6 +16,8 @@ function runURLTests(urltests) {
for(var i = 0, l = urltests.length; i < l; i++) {
var expected = urltests[i]
if (typeof expected === "string" || !("origin" in expected)) continue
+ // skip without base because you cannot unset the baseURL of a document
+ if (expected.base === null) continue;
test(function() {
var url = bURL(expected.input, expected.base)
diff --git a/test/fixtures/wpt/url/resources/a-element.js b/test/fixtures/wpt/url/resources/a-element.js
index f64531bc8bd528..553855a870c559 100644
--- a/test/fixtures/wpt/url/resources/a-element.js
+++ b/test/fixtures/wpt/url/resources/a-element.js
@@ -16,6 +16,8 @@ function runURLTests(urltests) {
for(var i = 0, l = urltests.length; i < l; i++) {
var expected = urltests[i]
if (typeof expected === "string") continue // skip comments
+ // skip without base because you cannot unset the baseURL of a document
+ if (expected.base === null) continue;
test(function() {
var url = bURL(expected.input, expected.base)
diff --git a/test/fixtures/wpt/url/resources/toascii.json b/test/fixtures/wpt/url/resources/toascii.json
index 1fb57673816e43..b9ceea310676d7 100644
--- a/test/fixtures/wpt/url/resources/toascii.json
+++ b/test/fixtures/wpt/url/resources/toascii.json
@@ -23,30 +23,30 @@
"output": "xn----xhn"
},
{
- "input": "-x.xn--nxa",
- "output": "-x.xn--nxa"
+ "input": "-x.xn--zca",
+ "output": "-x.xn--zca"
},
{
- "input": "-x.β",
- "output": "-x.xn--nxa"
+ "input": "-x.ß",
+ "output": "-x.xn--zca"
},
{
"comment": "Label with trailing hyphen",
- "input": "x-.xn--nxa",
- "output": "x-.xn--nxa"
+ "input": "x-.xn--zca",
+ "output": "x-.xn--zca"
},
{
- "input": "x-.β",
- "output": "x-.xn--nxa"
+ "input": "x-.ß",
+ "output": "x-.xn--zca"
},
{
"comment": "Empty labels",
- "input": "x..xn--nxa",
- "output": "x..xn--nxa"
+ "input": "x..xn--zca",
+ "output": "x..xn--zca"
},
{
- "input": "x..β",
- "output": "x..xn--nxa"
+ "input": "x..ß",
+ "output": "x..xn--zca"
},
{
"comment": "Invalid Punycode",
@@ -54,30 +54,35 @@
"output": null
},
{
- "input": "xn--a.xn--nxa",
+ "input": "xn--a.xn--zca",
"output": null
},
{
- "input": "xn--a.β",
+ "input": "xn--a.ß",
+ "output": null
+ },
+ {
+ "comment": "Invalid Punycode (contains non-ASCII character)",
+ "input": "xn--tešla",
"output": null
},
{
"comment": "Valid Punycode",
- "input": "xn--nxa.xn--nxa",
- "output": "xn--nxa.xn--nxa"
+ "input": "xn--zca.xn--zca",
+ "output": "xn--zca.xn--zca"
},
{
"comment": "Mixed",
- "input": "xn--nxa.β",
- "output": "xn--nxa.xn--nxa"
+ "input": "xn--zca.ß",
+ "output": "xn--zca.xn--zca"
},
{
- "input": "ab--c.xn--nxa",
- "output": "ab--c.xn--nxa"
+ "input": "ab--c.xn--zca",
+ "output": "ab--c.xn--zca"
},
{
- "input": "ab--c.β",
- "output": "ab--c.xn--nxa"
+ "input": "ab--c.ß",
+ "output": "ab--c.xn--zca"
},
{
"comment": "CheckJoiners is true",
@@ -126,12 +131,12 @@
"output": "xn--x01234567890123456789012345678901234567890123456789012345678901-6963b"
},
{
- "input": "x01234567890123456789012345678901234567890123456789012345678901x.xn--nxa",
- "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--nxa"
+ "input": "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca",
+ "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca"
},
{
- "input": "x01234567890123456789012345678901234567890123456789012345678901x.β",
- "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--nxa"
+ "input": "x01234567890123456789012345678901234567890123456789012345678901x.ß",
+ "output": "x01234567890123456789012345678901234567890123456789012345678901x.xn--zca"
},
{
"comment": "Domain excluding TLD longer than 253 code points",
@@ -139,12 +144,12 @@
"output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.x"
},
{
- "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--nxa",
- "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--nxa"
+ "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca",
+ "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca"
},
{
- "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.β",
- "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--nxa"
+ "input": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.ß",
+ "output": "01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.01234567890123456789012345678901234567890123456789.0123456789012345678901234567890123456789012345678.xn--zca"
},
{
"comment": "IDNA ignored code points",
diff --git a/test/fixtures/wpt/url/resources/urltestdata.json b/test/fixtures/wpt/url/resources/urltestdata.json
index a56b30caf98cb1..3cf106965b1ffd 100644
--- a/test/fixtures/wpt/url/resources/urltestdata.json
+++ b/test/fixtures/wpt/url/resources/urltestdata.json
@@ -3303,12 +3303,14 @@
{
"input": "http:@:www.example.com",
"base": "about:blank",
- "failure": true
+ "failure": true,
+ "inputCanBeRelative": true
},
{
"input": "http:/@:www.example.com",
"base": "about:blank",
- "failure": true
+ "failure": true,
+ "inputCanBeRelative": true
},
{
"input": "http://@:www.example.com",
@@ -3693,6 +3695,27 @@
"base": "about:blank",
"failure": true
},
+ "IDNA labels should be matched case-insensitively",
+ {
+ "input": "http://a.b.c.XN--pokxncvks",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a.b.c.Xn--pokxncvks",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://10.0.0.XN--pokxncvks",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://10.0.0.xN--pokxncvks",
+ "base": "about:blank",
+ "failure": true
+ },
"Test name prepping, fullwidth input should be converted to ASCII and NOT IDN-ized. This is 'Go' in fullwidth UTF-8/UTF-16.",
{
"input": "http://Go.com",
@@ -3894,21 +3917,6 @@
"search": "",
"hash": ""
},
- {
- "input": "http://0..0x300/",
- "base": "about:blank",
- "href": "http://0..0x300/",
- "origin": "http://0..0x300",
- "protocol": "http:",
- "username": "",
- "password": "",
- "host": "0..0x300",
- "hostname": "0..0x300",
- "port": "",
- "pathname": "/",
- "search": "",
- "hash": ""
- },
"Broken IPv6",
{
"input": "http://[www.google.com]/",
@@ -4529,16 +4537,6 @@
"search": "",
"hash": ""
},
- {
- "input": "sc://\u0000/",
- "base": "about:blank",
- "failure": true
- },
- {
- "input": "sc:// /",
- "base": "about:blank",
- "failure": true
- },
{
"input": "sc://%/",
"base": "about:blank",
@@ -4573,21 +4571,6 @@
"base": "about:blank",
"failure": true
},
- {
- "input": "sc://[/",
- "base": "about:blank",
- "failure": true
- },
- {
- "input": "sc://\\/",
- "base": "about:blank",
- "failure": true
- },
- {
- "input": "sc://]/",
- "base": "about:blank",
- "failure": true
- },
{
"input": "x",
"base": "sc://ñ",
@@ -4699,42 +4682,47 @@
},
"Forbidden host code points",
{
- "input": "http://ab",
+ "input": "sc://a b/",
"base": "about:blank",
"failure": true
},
{
- "input": "http://a^b",
+ "input": "sc://ab",
"base": "about:blank",
"failure": true
},
{
- "input": "non-special://ab",
+ "input": "sc://a\\b/",
"base": "about:blank",
"failure": true
},
{
- "input": "non-special://a^b",
+ "input": "sc://a]b/",
"base": "about:blank",
"failure": true
},
{
- "input": "foo://ho\u0000st/",
+ "input": "sc://a^b",
"base": "about:blank",
"failure": true
},
{
- "input": "foo://ho|st/",
+ "input": "sc://a|b/",
"base": "about:blank",
"failure": true
},
@@ -4754,51 +4742,425 @@
"username": ""
},
{
- "input": "foo://ho\u000Ast/",
+ "input": "foo://ho\u000Ast/",
+ "base": "about:blank",
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"foo://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "foo://ho\u000Dst/",
+ "base": "about:blank",
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"foo://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "foo:",
+ "search": "",
+ "username": ""
+ },
+ "Forbidden domain code-points",
+ {
+ "input": "http://a\u0000b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0001b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0002b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0003b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0004b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0005b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0006b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0007b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0008b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u000Bb/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u000Cb/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u000Eb/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u000Fb/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0010b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0011b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0012b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0013b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0014b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0015b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0016b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0017b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0018b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u0019b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Ab/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Bb/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Cb/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Db/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Eb/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u001Fb/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a%b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ab",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a[b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a]b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a^b",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a|b/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://a\u007Fb/",
+ "base": "about:blank",
+ "failure": true
+ },
+ "Forbidden domain codepoints: tabs and newlines are removed during preprocessing",
+ {
+ "input": "http://ho\u0009st/",
+ "base": "about:blank",
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"http://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "http:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "http://ho\u000Ast/",
+ "base": "about:blank",
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"http://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "http:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "http://ho\u000Dst/",
+ "base": "about:blank",
+ "hash": "",
+ "host": "host",
+ "hostname": "host",
+ "href":"http://host/",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "http:",
+ "search": "",
+ "username": ""
+ },
+ "Encoded forbidden domain codepoints in special URLs",
+ {
+ "input": "http://ho%00st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%01st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%02st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%03st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%04st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%05st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%06st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%07st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%08st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%09st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Ast/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Bst/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Cst/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Dst/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Est/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%0Fst/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%10st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%11st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%12st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%13st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%14st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%15st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%16st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%17st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%18st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%19st/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://ho%1Ast/",
"base": "about:blank",
- "hash": "",
- "host": "host",
- "hostname": "host",
- "href":"foo://host/",
- "password": "",
- "pathname": "/",
- "port":"",
- "protocol": "foo:",
- "search": "",
- "username": ""
+ "failure": true
},
{
- "input": "foo://ho\u000Dst/",
+ "input": "http://ho%1Bst/",
"base": "about:blank",
- "hash": "",
- "host": "host",
- "hostname": "host",
- "href":"foo://host/",
- "password": "",
- "pathname": "/",
- "port":"",
- "protocol": "foo:",
- "search": "",
- "username": ""
+ "failure": true
},
- "Encoded forbidden host codepoints in special URLs",
{
- "input": "http://ho%00st/",
+ "input": "http://ho%1Cst/",
"base": "about:blank",
"failure": true
},
{
- "input": "http://ho%09st/",
+ "input": "http://ho%1Dst/",
"base": "about:blank",
"failure": true
},
{
- "input": "http://ho%0Ast/",
+ "input": "http://ho%1Est/",
"base": "about:blank",
"failure": true
},
{
- "input": "http://ho%0Dst/",
+ "input": "http://ho%1Fst/",
"base": "about:blank",
"failure": true
},
@@ -4812,6 +5174,11 @@
"base": "about:blank",
"failure": true
},
+ {
+ "input": "http://ho%25st/",
+ "base": "about:blank",
+ "failure": true
+ },
{
"input": "http://ho%2Fst/",
"base": "about:blank",
@@ -4862,32 +5229,37 @@
"base": "about:blank",
"failure": true
},
- "Allowed host code points",
{
- "input": "http://\u001F!\"$&'()*+,-.;=_`{}~/",
+ "input": "http://ho%7Fst/",
+ "base": "about:blank",
+ "failure": true
+ },
+ "Allowed host/domain code points",
+ {
+ "input": "http://!\"$&'()*+,-.;=_`{}~/",
"base": "about:blank",
- "href": "http://\u001F!\"$&'()*+,-.;=_`{}~/",
- "origin": "http://\u001F!\"$&'()*+,-.;=_`{}~",
+ "href": "http://!\"$&'()*+,-.;=_`{}~/",
+ "origin": "http://!\"$&'()*+,-.;=_`{}~",
"protocol": "http:",
"username": "",
"password": "",
- "host": "\u001F!\"$&'()*+,-.;=_`{}~",
- "hostname": "\u001F!\"$&'()*+,-.;=_`{}~",
+ "host": "!\"$&'()*+,-.;=_`{}~",
+ "hostname": "!\"$&'()*+,-.;=_`{}~",
"port": "",
"pathname": "/",
"search": "",
"hash": ""
},
{
- "input": "sc://\u001F!\"$&'()*+,-.;=_`{}~/",
+ "input": "sc://\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u000B\u000C\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u007F!\"$%&'()*+,-.;=_`{}~/",
"base": "about:blank",
- "href": "sc://%1F!\"$&'()*+,-.;=_`{}~/",
+ "href": "sc://%01%02%03%04%05%06%07%08%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%7F!\"$%&'()*+,-.;=_`{}~/",
"origin": "null",
"protocol": "sc:",
"username": "",
"password": "",
- "host": "%1F!\"$&'()*+,-.;=_`{}~",
- "hostname": "%1F!\"$&'()*+,-.;=_`{}~",
+ "host": "%01%02%03%04%05%06%07%08%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%7F!\"$%&'()*+,-.;=_`{}~",
+ "hostname": "%01%02%03%04%05%06%07%08%0B%0C%0E%0F%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F%7F!\"$%&'()*+,-.;=_`{}~",
"port": "",
"pathname": "/",
"search": "",
@@ -5159,6 +5531,36 @@
"hash": "#foo%60bar"
},
"# IPv4 parsing (via https://github.com/nodejs/node/pull/10317)",
+ {
+ "input": "http://1.2.3.4/",
+ "base": "http://other.com/",
+ "href": "http://1.2.3.4/",
+ "origin": "http://1.2.3.4",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "1.2.3.4",
+ "hostname": "1.2.3.4",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
+ {
+ "input": "http://1.2.3.4./",
+ "base": "http://other.com/",
+ "href": "http://1.2.3.4/",
+ "origin": "http://1.2.3.4",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "1.2.3.4",
+ "hostname": "1.2.3.4",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
{
"input": "http://192.168.257",
"base": "http://other.com/",
@@ -5174,6 +5576,21 @@
"search": "",
"hash": ""
},
+ {
+ "input": "http://192.168.257.",
+ "base": "http://other.com/",
+ "href": "http://192.168.1.1/",
+ "origin": "http://192.168.1.1",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "192.168.1.1",
+ "hostname": "192.168.1.1",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
{
"input": "http://192.168.257.com",
"base": "http://other.com/",
@@ -5234,6 +5651,21 @@
"search": "",
"hash": ""
},
+ {
+ "input": "http://999999999.",
+ "base": "http://other.com/",
+ "href": "http://59.154.201.255/",
+ "origin": "http://59.154.201.255",
+ "protocol": "http:",
+ "username": "",
+ "password": "",
+ "host": "59.154.201.255",
+ "hostname": "59.154.201.255",
+ "port": "",
+ "pathname": "/",
+ "search": "",
+ "hash": ""
+ },
{
"input": "http://999999999.com",
"base": "http://other.com/",
@@ -5314,21 +5746,6 @@
"base": "http://other.com/",
"failure": true
},
- {
- "input": "http://256.256.256.256.256",
- "base": "http://other.com/",
- "href": "http://256.256.256.256.256/",
- "origin": "http://256.256.256.256.256",
- "protocol": "http:",
- "username": "",
- "password": "",
- "host": "256.256.256.256.256",
- "hostname": "256.256.256.256.256",
- "port": "",
- "pathname": "/",
- "search": "",
- "hash": ""
- },
{
"input": "https://0x.0x.0",
"base": "about:blank",
@@ -6373,7 +6790,8 @@
{
"input": "\\\\\\.\\Y:",
"base": "about:blank",
- "failure": true
+ "failure": true,
+ "inputCanBeRelative": true
},
"# file: drive letter cases from https://crbug.com/1078698 but lowercased",
{
@@ -6435,7 +6853,8 @@
{
"input": "\\\\\\.\\y:",
"base": "about:blank",
- "failure": true
+ "failure": true,
+ "inputCanBeRelative": true
},
"# Additional file URL tests for (https://github.com/whatwg/url/issues/405)",
{
@@ -7269,6 +7688,7 @@
"input": "blob:https://example.com:443/",
"base": "about:blank",
"href": "blob:https://example.com:443/",
+ "origin": "https://example.com",
"protocol": "blob:",
"username": "",
"password": "",
@@ -7283,6 +7703,7 @@
"input": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf",
"base": "about:blank",
"href": "blob:d3958f5c-0777-0845-9dcf-2cb28783acaf",
+ "origin": "null",
"protocol": "blob:",
"username": "",
"password": "",
@@ -7293,21 +7714,22 @@
"search": "",
"hash": ""
},
- "Invalid IPv4 radix digits",
{
- "input": "http://0177.0.0.0189",
+ "input": "blob:",
"base": "about:blank",
- "href": "http://0177.0.0.0189/",
- "protocol": "http:",
+ "href": "blob:",
+ "origin": "null",
+ "protocol": "blob:",
"username": "",
"password": "",
- "host": "0177.0.0.0189",
- "hostname": "0177.0.0.0189",
+ "host": "",
+ "hostname": "",
"port": "",
- "pathname": "/",
+ "pathname": "",
"search": "",
"hash": ""
},
+ "Invalid IPv4 radix digits",
{
"input": "http://0x7f.0.0.0x7g",
"base": "about:blank",
@@ -8016,5 +8438,176 @@
"protocol": "abc:",
"search": "",
"username": ""
+ },
+ "Empty query and fragment with blank should throw an error",
+ {
+ "input": "#",
+ "base": null,
+ "failure": true
+ },
+ {
+ "input": "?",
+ "base": null,
+ "failure": true
+ },
+ "Last component looks like a number, but not valid IPv4",
+ {
+ "input": "http://1.2.3.4.5",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://1.2.3.4.5.",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://0..0x300/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://0..0x300./",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://256.256.256.256.256",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://256.256.256.256.256.",
+ "base": "http://other.com/",
+ "failure": true
+ },
+ {
+ "input": "http://1.2.3.08",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://1.2.3.08.",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://1.2.3.09",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://09.2.3.4",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://09.2.3.4.",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://01.2.3.4.5",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://01.2.3.4.5.",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://0x100.2.3.4",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://0x100.2.3.4.",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://0x1.2.3.4.5",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://0x1.2.3.4.5.",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.1.2.3.4",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.1.2.3.4.",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.2.3.4",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.2.3.4.",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.09",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.09.",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.0x4",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.0x4.",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.09..",
+ "base": "about:blank",
+ "hash": "",
+ "host": "foo.09..",
+ "hostname": "foo.09..",
+ "href":"http://foo.09../",
+ "password": "",
+ "pathname": "/",
+ "port":"",
+ "protocol": "http:",
+ "search": "",
+ "username": ""
+ },
+ {
+ "input": "http://0999999999999999999/",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.0x",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://foo.0XFfFfFfFfFfFfFfFfFfAcE123",
+ "base": "about:blank",
+ "failure": true
+ },
+ {
+ "input": "http://💩.123/",
+ "base": "about:blank",
+ "failure": true
}
]
diff --git a/test/fixtures/wpt/url/url-constructor.any.js b/test/fixtures/wpt/url/url-constructor.any.js
index 3f4af56d2a9654..dfa98092fa6475 100644
--- a/test/fixtures/wpt/url/url-constructor.any.js
+++ b/test/fixtures/wpt/url/url-constructor.any.js
@@ -1,7 +1,7 @@
// META: timeout=long
function bURL(url, base) {
- return new URL(url, base || "about:blank")
+ return base ? new URL(url, base) : new URL(url)
}
function runURLTests(urltests) {
diff --git a/test/fixtures/wpt/url/url-origin.any.js b/test/fixtures/wpt/url/url-origin.any.js
index d9ef64c73b8bcc..9c1f97ed2e5949 100644
--- a/test/fixtures/wpt/url/url-origin.any.js
+++ b/test/fixtures/wpt/url/url-origin.any.js
@@ -1,7 +1,7 @@
promise_test(() => fetch("resources/urltestdata.json").then(res => res.json()).then(runURLTests), "Loading data…");
function bURL(url, base) {
- return new URL(url, base || "about:blank")
+ return base ? new URL(url, base) : new URL(url)
}
function runURLTests(urltests) {
diff --git a/test/fixtures/wpt/url/url-setters-stripping.any.js b/test/fixtures/wpt/url/url-setters-stripping.any.js
index 3413c6cd5ad21d..ac90cc17e0bfd5 100644
--- a/test/fixtures/wpt/url/url-setters-stripping.any.js
+++ b/test/fixtures/wpt/url/url-setters-stripping.any.js
@@ -66,7 +66,7 @@ for(const scheme of ["https", "wpt++"]) {
["trailing", "test" + (scheme === "https" ? cpString : encodeURIComponent(cpString)), "test" + String.fromCodePoint(i)]
]) {
test(() => {
- const expected = i === 0x00 ? "host" : stripped ? "test" : expectedPart;
+ const expected = i === 0x00 || (scheme === "https" && i === 0x1F) ? "host" : stripped ? "test" : expectedPart;
const url = urlRecord(scheme);
url.host = input;
assert_equals(url.host, expected + ":8000", "property");
@@ -74,7 +74,7 @@ for(const scheme of ["https", "wpt++"]) {
}, `Setting host with ${type} ${cpReference} (${scheme}:)`);
test(() => {
- const expected = i === 0x00 ? "host" : stripped ? "test" : expectedPart;
+ const expected = i === 0x00 || (scheme === "https" && i === 0x1F) ? "host" : stripped ? "test" : expectedPart;
const url = urlRecord(scheme);
url.hostname = input;
assert_equals(url.hostname, expected, "property");
diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json
index bde6cf862f6358..fa4a5e62404044 100644
--- a/test/fixtures/wpt/versions.json
+++ b/test/fixtures/wpt/versions.json
@@ -48,7 +48,7 @@
"path": "performance-timeline"
},
"resources": {
- "commit": "fbee645164468c030072c46a934e2c876b143f8e",
+ "commit": "c5b428f15acfb17fe59b5a6f04a21c288a76ed36",
"path": "resources"
},
"streams": {
@@ -56,7 +56,7 @@
"path": "streams"
},
"url": {
- "commit": "77d54aa9e0405f737987b59331f3584e3e1c26f9",
+ "commit": "0e5b126cd0a8da9186b738b8c9278d19b594c51f",
"path": "url"
},
"user-timing": {
@@ -79,4 +79,4 @@
"commit": "a370aad338d6ed743abb4d2c6ae84a7f1058558c",
"path": "webidl/ecmascript-binding/es-exceptions"
}
-}
\ No newline at end of file
+}