From 2dccda26f920e192372d02f93a977e3233a3ba3d Mon Sep 17 00:00:00 2001 From: Feng Yu Date: Fri, 1 Jul 2022 00:51:11 +0800 Subject: [PATCH] url: update WHATWG URL parser to align with latest spec PR-URL: https://github.com/nodejs/node/pull/43190 Reviewed-By: James M Snell Reviewed-By: Daijiro Wachi --- src/node_url.cc | 81 +- src/util.cc | 6 +- src/util.h | 4 +- test/common/wpt/worker.js | 3 +- test/fixtures/wpt/README.md | 4 +- test/fixtures/wpt/resources/accesskey.js | 34 + test/fixtures/wpt/resources/blank.html | 16 + test/fixtures/wpt/resources/channel.sub.js | 1097 ++++++++++++++++ .../fixtures/wpt/resources/check-layout-th.js | 2 +- test/fixtures/wpt/resources/check-layout.js | 245 ++++ .../wpt/resources/idlharness-shadowrealm.js | 81 ++ test/fixtures/wpt/resources/idlharness.js | 52 +- test/fixtures/wpt/resources/sriharness.js | 23 +- .../wpt/resources/testdriver-actions.js | 33 +- test/fixtures/wpt/resources/testdriver.js | 294 ++++- test/fixtures/wpt/resources/testharness.js | 1116 +++++++++++++---- test/fixtures/wpt/resources/webidl2/build.sh | 12 + .../wpt/resources/webidl2/lib/README.md | 2 - .../wpt/resources/webidl2/lib/VERSION.md | 1 + .../wpt/resources/webidl2/lib/webidl2.js | 198 +-- .../wpt/url/idlharness-shadowrealm.window.js | 2 + .../wpt/url/resources/a-element-origin.js | 2 + test/fixtures/wpt/url/resources/a-element.js | 2 + test/fixtures/wpt/url/resources/toascii.json | 65 +- .../wpt/url/resources/urltestdata.json | 815 ++++++++++-- test/fixtures/wpt/url/url-constructor.any.js | 2 +- test/fixtures/wpt/url/url-origin.any.js | 2 +- .../wpt/url/url-setters-stripping.any.js | 4 +- test/fixtures/wpt/versions.json | 4 +- test/parallel/test-whatwg-url-constructor.js | 144 --- test/parallel/test-whatwg-url-origin.js | 53 - test/parallel/test-whatwg-url-setters.js | 79 -- 32 files changed, 3657 insertions(+), 821 deletions(-) create mode 100644 test/fixtures/wpt/resources/accesskey.js create mode 100644 test/fixtures/wpt/resources/blank.html create mode 100644 test/fixtures/wpt/resources/channel.sub.js create mode 100644 test/fixtures/wpt/resources/check-layout.js create mode 100644 test/fixtures/wpt/resources/idlharness-shadowrealm.js create mode 100644 test/fixtures/wpt/resources/webidl2/build.sh create mode 100644 test/fixtures/wpt/resources/webidl2/lib/VERSION.md create mode 100644 test/fixtures/wpt/url/idlharness-shadowrealm.window.js delete mode 100644 test/parallel/test-whatwg-url-constructor.js delete mode 100644 test/parallel/test-whatwg-url-origin.js delete mode 100644 test/parallel/test-whatwg-url-setters.js diff --git a/src/node_url.cc b/src/node_url.cc index 3cbe6ab5f354e2..41e467108614f6 100644 --- a/src/node_url.cc +++ b/src/node_url.cc @@ -5,6 +5,7 @@ #include "node_i18n.h" #include "util-inl.h" +#include #include #include #include @@ -58,7 +59,7 @@ class URLHost { public: ~URLHost(); - void ParseIPv4Host(const char* input, size_t length, bool* is_ipv4); + void ParseIPv4Host(const char* input, size_t length); void ParseIPv6Host(const char* input, size_t length); void ParseOpaqueHost(const char* input, size_t length); void ParseHost(const char* input, @@ -165,6 +166,9 @@ enum url_cb_args { // https://infra.spec.whatwg.org/#ascii-tab-or-newline CHAR_TEST(8, IsASCIITabOrNewline, (ch == '\t' || ch == '\n' || ch == '\r')) +// https://infra.spec.whatwg.org/#c0-control +CHAR_TEST(8, IsC0Control, (ch >= '\0' && ch <= '\x1f')) + // https://infra.spec.whatwg.org/#c0-control-or-space CHAR_TEST(8, IsC0ControlOrSpace, (ch >= '\0' && ch <= ' ')) @@ -190,12 +194,18 @@ T ASCIILowercase(T ch) { } // https://url.spec.whatwg.org/#forbidden-host-code-point -CHAR_TEST(8, IsForbiddenHostCodePoint, - ch == '\0' || ch == '\t' || ch == '\n' || ch == '\r' || - ch == ' ' || ch == '#' || ch == '%' || ch == '/' || - ch == ':' || ch == '?' || ch == '@' || ch == '[' || - ch == '<' || ch == '>' || ch == '\\' || ch == ']' || - ch == '^' || ch == '|') +CHAR_TEST(8, + IsForbiddenHostCodePoint, + ch == '\0' || ch == '\t' || ch == '\n' || ch == '\r' || ch == ' ' || + ch == '#' || ch == '/' || ch == ':' || ch == '?' || ch == '@' || + ch == '[' || ch == '<' || ch == '>' || ch == '\\' || ch == ']' || + ch == '^' || ch == '|') + +// https://url.spec.whatwg.org/#forbidden-domain-code-point +CHAR_TEST(8, + IsForbiddenDomainCodePoint, + IsForbiddenHostCodePoint(ch) || IsC0Control(ch) || ch == '%' || + ch == '\x7f') // https://url.spec.whatwg.org/#windows-drive-letter TWO_CHAR_STRING_TEST(8, IsWindowsDriveLetter, @@ -359,18 +369,21 @@ void URLHost::ParseIPv6Host(const char* input, size_t length) { type_ = HostType::H_IPV6; } -int64_t ParseNumber(const char* start, const char* end) { +// https://url.spec.whatwg.org/#ipv4-number-parser +int64_t ParseIPv4Number(const char* start, const char* end) { + if (end - start == 0) return -1; + unsigned R = 10; if (end - start >= 2 && start[0] == '0' && (start[1] | 0x20) == 'x') { start += 2; R = 16; - } - if (end - start == 0) { - return 0; - } else if (R == 10 && end - start > 1 && start[0] == '0') { + } else if (end - start >= 2 && start[0] == '0') { start++; R = 8; } + + if (end - start == 0) return 0; + const char* p = start; while (p < end) { @@ -394,9 +407,33 @@ int64_t ParseNumber(const char* start, const char* end) { return strtoll(start, nullptr, R); } -void URLHost::ParseIPv4Host(const char* input, size_t length, bool* is_ipv4) { +// https://url.spec.whatwg.org/#ends-in-a-number-checker +bool EndsInANumber(const std::string& input) { + std::vector parts = SplitString(input, '.', false); + + if (parts.empty()) return false; + + if (parts.back().empty()) { + if (parts.size() == 1) return false; + parts.pop_back(); + } + + const std::string& last = parts.back(); + + // If last is non-empty and contains only ASCII digits, then return true + if (!last.empty() && std::all_of(last.begin(), last.end(), ::isdigit)) { + return true; + } + + const char* last_str = last.c_str(); + int64_t num = ParseIPv4Number(last_str, last_str + last.size()); + if (num >= 0) return true; + + return false; +} + +void URLHost::ParseIPv4Host(const char* input, size_t length) { CHECK_EQ(type_, HostType::H_FAILED); - *is_ipv4 = false; const char* pointer = input; const char* mark = input; const char* end = pointer + length; @@ -414,7 +451,7 @@ void URLHost::ParseIPv4Host(const char* input, size_t length, bool* is_ipv4) { if (++parts > static_cast(arraysize(numbers))) return; if (pointer == mark) return; - int64_t n = ParseNumber(mark, pointer); + int64_t n = ParseIPv4Number(mark, pointer); if (n < 0) return; @@ -429,7 +466,6 @@ void URLHost::ParseIPv4Host(const char* input, size_t length, bool* is_ipv4) { pointer++; } CHECK_GT(parts, 0); - *is_ipv4 = true; // If any but the last item in numbers is greater than 255, return failure. // If the last item in numbers is greater than or equal to @@ -457,7 +493,7 @@ void URLHost::ParseOpaqueHost(const char* input, size_t length) { output.reserve(length); for (size_t i = 0; i < length; i++) { const char ch = input[i]; - if (ch != '%' && IsForbiddenHostCodePoint(ch)) { + if (IsForbiddenHostCodePoint(ch)) { return; } else { AppendOrEscape(&output, ch, C0_CONTROL_ENCODE_SET); @@ -496,16 +532,15 @@ void URLHost::ParseHost(const char* input, // If any of the following characters are still present, we have to fail for (size_t n = 0; n < decoded.size(); n++) { const char ch = decoded[n]; - if (IsForbiddenHostCodePoint(ch)) { + if (IsForbiddenDomainCodePoint(ch)) { return; } } - // Check to see if it's an IPv4 IP address - bool is_ipv4; - ParseIPv4Host(decoded.c_str(), decoded.length(), &is_ipv4); - if (is_ipv4) - return; + // If domain ends in a number, then return the result of IPv4 parsing domain + if (EndsInANumber(decoded)) { + return ParseIPv4Host(decoded.c_str(), decoded.length()); + } // If the unicode flag is set, run the result through punycode ToUnicode if (unicode && !ToUnicode(decoded, &decoded)) diff --git a/src/util.cc b/src/util.cc index bae5a47a06d8de..b881f9f9f88cee 100644 --- a/src/util.cc +++ b/src/util.cc @@ -164,7 +164,9 @@ std::string GetHumanReadableProcessName() { return SPrintF("%s[%d]", GetProcessTitle("Node.js"), uv_os_getpid()); } -std::vector SplitString(const std::string& in, char delim) { +std::vector SplitString(const std::string& in, + char delim, + bool skipEmpty) { std::vector out; if (in.empty()) return out; @@ -172,7 +174,7 @@ std::vector SplitString(const std::string& in, char delim) { while (in_stream.good()) { std::string item; std::getline(in_stream, item, delim); - if (item.empty()) continue; + if (item.empty() && skipEmpty) continue; out.emplace_back(std::move(item)); } return out; diff --git a/src/util.h b/src/util.h index 14c8758c849904..84977567fb1a53 100644 --- a/src/util.h +++ b/src/util.h @@ -645,7 +645,9 @@ struct FunctionDeleter { template using DeleteFnPtr = typename FunctionDeleter::Pointer; -std::vector SplitString(const std::string& in, char delim); +std::vector SplitString(const std::string& in, + char delim, + bool skipEmpty = true); inline v8::MaybeLocal ToV8Value(v8::Local context, std::string_view str, 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 e4bd0ab9737bf4..bf5539e384a777 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -23,9 +23,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 e9539ebf6410ae..ce97197f1daa0b 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -52,7 +52,7 @@ "path": "performance-timeline" }, "resources": { - "commit": "fbee645164468c030072c46a934e2c876b143f8e", + "commit": "c5b428f15acfb17fe59b5a6f04a21c288a76ed36", "path": "resources" }, "streams": { @@ -60,7 +60,7 @@ "path": "streams" }, "url": { - "commit": "77d54aa9e0405f737987b59331f3584e3e1c26f9", + "commit": "0e5b126cd0a8da9186b738b8c9278d19b594c51f", "path": "url" }, "user-timing": { diff --git a/test/parallel/test-whatwg-url-constructor.js b/test/parallel/test-whatwg-url-constructor.js deleted file mode 100644 index 3dc1c5986027e7..00000000000000 --- a/test/parallel/test-whatwg-url-constructor.js +++ /dev/null @@ -1,144 +0,0 @@ -'use strict'; -const common = require('../common'); -if (!common.hasIntl) { - // A handful of the tests fail when ICU is not included. - common.skip('missing Intl'); -} - -const fixtures = require('../common/fixtures'); -const { test, assert_equals, assert_true, assert_throws } = - require('../common/wpt').harness; - -const request = { - response: require( - fixtures.path('wpt', 'url', 'resources', 'urltestdata.json') - ) -}; - -// The following tests are copied from WPT. Modifications to them should be -// upstreamed first. -// Refs: https://github.com/w3c/web-platform-tests/blob/8791bed/url/url-constructor.html -// License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html - -/* eslint-disable */ -function runURLConstructorTests() { - // var setup = async_test("Loading data…") - // setup.step(function() { - // var request = new XMLHttpRequest() - // request.open("GET", "urltestdata.json") - // request.send() - // request.responseType = "json" - // request.onload = setup.step_func(function() { - runURLTests(request.response) - // setup.done() - // }) - // }) -} - -function bURL(url, base) { - return new URL(url, base || "about:blank") -} - - -function runURLTests(urltests) { - for(var i = 0, l = urltests.length; i < l; i++) { - var expected = urltests[i] - if (typeof expected === "string") continue // skip comments - - test(function() { - if (expected.failure) { - assert_throws(new TypeError(), function() { - bURL(expected.input, expected.base) - }) - return - } - - var url = bURL(expected.input, expected.base) - assert_equals(url.href, expected.href, "href") - assert_equals(url.protocol, expected.protocol, "protocol") - assert_equals(url.username, expected.username, "username") - assert_equals(url.password, expected.password, "password") - assert_equals(url.host, expected.host, "host") - assert_equals(url.hostname, expected.hostname, "hostname") - assert_equals(url.port, expected.port, "port") - assert_equals(url.pathname, expected.pathname, "pathname") - assert_equals(url.search, expected.search, "search") - if ("searchParams" in expected) { - assert_true("searchParams" in url) - assert_equals(url.searchParams.toString(), expected.searchParams, "searchParams") - } - assert_equals(url.hash, expected.hash, "hash") - }, "Parsing: <" + expected.input + "> against <" + expected.base + ">") - } -} - -function runURLSearchParamTests() { - test(function() { - var url = bURL('http://example.org/?a=b') - assert_true("searchParams" in url) - var searchParams = url.searchParams - assert_true(url.searchParams === searchParams, 'Object identity should hold.') - }, 'URL.searchParams getter') - - test(function() { - var url = bURL('http://example.org/?a=b') - assert_true("searchParams" in url) - var searchParams = url.searchParams - assert_equals(searchParams.toString(), 'a=b') - - searchParams.set('a', 'b') - assert_equals(url.searchParams.toString(), 'a=b') - assert_equals(url.search, '?a=b') - url.search = '' - assert_equals(url.searchParams.toString(), '') - assert_equals(url.search, '') - assert_equals(searchParams.toString(), '') - }, 'URL.searchParams updating, clearing') - - test(function() { - 'use strict' - var urlString = 'http://example.org' - var url = bURL(urlString) - assert_throws(TypeError(), function() { url.searchParams = new URLSearchParams(urlString) }) - }, 'URL.searchParams setter, invalid values') - - test(function() { - var url = bURL('http://example.org/file?a=b&c=d') - assert_true("searchParams" in url) - var searchParams = url.searchParams - assert_equals(url.search, '?a=b&c=d') - assert_equals(searchParams.toString(), 'a=b&c=d') - - // Test that setting 'search' propagates to the URL object's query object. - url.search = 'e=f&g=h' - assert_equals(url.search, '?e=f&g=h') - assert_equals(searchParams.toString(), 'e=f&g=h') - - // ..and same but with a leading '?'. - url.search = '?e=f&g=h' - assert_equals(url.search, '?e=f&g=h') - assert_equals(searchParams.toString(), 'e=f&g=h') - - // And in the other direction, altering searchParams propagates - // back to 'search'. - searchParams.append('i', ' j ') - assert_equals(url.search, '?e=f&g=h&i=+j+') - assert_equals(url.searchParams.toString(), 'e=f&g=h&i=+j+') - assert_equals(searchParams.get('i'), ' j ') - - searchParams.set('e', 'updated') - assert_equals(url.search, '?e=updated&g=h&i=+j+') - assert_equals(searchParams.get('e'), 'updated') - - var url2 = bURL('http://example.org/file??a=b&c=d') - assert_equals(url2.search, '??a=b&c=d') - assert_equals(url2.searchParams.toString(), '%3Fa=b&c=d') - - url2.href = 'http://example.org/file??a=b' - assert_equals(url2.search, '??a=b') - assert_equals(url2.searchParams.toString(), '%3Fa=b') - }, 'URL.searchParams and URL.search setters, update propagation') -} -runURLSearchParamTests() -runURLConstructorTests() -/* eslint-enable */ diff --git a/test/parallel/test-whatwg-url-origin.js b/test/parallel/test-whatwg-url-origin.js deleted file mode 100644 index 532ff06bb1152f..00000000000000 --- a/test/parallel/test-whatwg-url-origin.js +++ /dev/null @@ -1,53 +0,0 @@ -'use strict'; -const common = require('../common'); -if (!common.hasIntl) { - // A handful of the tests fail when ICU is not included. - common.skip('missing Intl'); -} - -const fixtures = require('../common/fixtures'); -const { test, assert_equals } = require('../common/wpt').harness; - -const request = { - response: require( - fixtures.path('wpt', 'url', 'resources', 'urltestdata.json') - ) -}; - -// The following tests are copied from WPT. Modifications to them should be -// upstreamed first. -// Refs: https://github.com/w3c/web-platform-tests/blob/8791bed/url/url-origin.html -// License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html - -/* eslint-disable */ -function runURLOriginTests() { - // var setup = async_test("Loading data…") - // setup.step(function() { - // var request = new XMLHttpRequest() - // request.open("GET", "urltestdata.json") - // request.send() - // request.responseType = "json" - // request.onload = setup.step_func(function() { - runURLTests(request.response) - // setup.done() - // }) - // }) -} - -function bURL(url, base) { - return new URL(url, base || "about:blank") -} - -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 - test(function() { - var url = bURL(expected.input, expected.base) - assert_equals(url.origin, expected.origin, "origin") - }, "Origin parsing: <" + expected.input + "> against <" + expected.base + ">") - } -} - -runURLOriginTests() -/* eslint-enable */ diff --git a/test/parallel/test-whatwg-url-setters.js b/test/parallel/test-whatwg-url-setters.js deleted file mode 100644 index 8742ab8ed372de..00000000000000 --- a/test/parallel/test-whatwg-url-setters.js +++ /dev/null @@ -1,79 +0,0 @@ -'use strict'; - -const common = require('../common'); -if (!common.hasIntl) { - // A handful of the tests fail when ICU is not included. - common.skip('missing Intl'); -} - -const { test, assert_equals } = require('../common/wpt').harness; -const fixtures = require('../common/fixtures'); - -const request = { - response: require(fixtures.path( - 'wpt', 'url', 'resources', 'setters_tests.json' - )) -}; - -// The following tests are copied from WPT. Modifications to them should be -// upstreamed first. -// Refs: https://github.com/w3c/web-platform-tests/blob/8791bed/url/url-setters.html -// License: http://www.w3.org/Consortium/Legal/2008/04-testsuite-copyright.html - -/* eslint-disable */ -function startURLSettersTests() { -// var setup = async_test("Loading data…") -// setup.step(function() { -// var request = new XMLHttpRequest() -// request.open("GET", "setters_tests.json") -// request.send() -// request.responseType = "json" -// request.onload = setup.step_func(function() { - runURLSettersTests(request.response) -// setup.done() -// }) -// }) -} - -function runURLSettersTests(all_test_cases) { - for (var attribute_to_be_set in all_test_cases) { - if (attribute_to_be_set == "comment") { - continue; - } - var test_cases = all_test_cases[attribute_to_be_set]; - for(var i = 0, l = test_cases.length; i < l; i++) { - var test_case = test_cases[i]; - var name = `Setting <${test_case.href}>.${attribute_to_be_set}` + - ` = '${test_case.new_value}'`; - if ("comment" in test_case) { - name += ` ${test_case.comment}`; - } - test(function() { - var url = new URL(test_case.href); - url[attribute_to_be_set] = test_case.new_value; - for (var attribute in test_case.expected) { - assert_equals(url[attribute], test_case.expected[attribute]) - } - }, `URL: ${name}`); - // test(function() { - // var url = document.createElement("a"); - // url.href = test_case.href; - // url[attribute_to_be_set] = test_case.new_value; - // for (var attribute in test_case.expected) { - // assert_equals(url[attribute], test_case.expected[attribute]) - // } - // }, ": " + name) - // test(function() { - // var url = document.createElement("area"); - // url.href = test_case.href; - // url[attribute_to_be_set] = test_case.new_value; - // for (var attribute in test_case.expected) { - // assert_equals(url[attribute], test_case.expected[attribute]) - // } - // }, ": " + name) - } - } -} - -startURLSettersTests() -/* eslint-enable */