button.addEventListener("click", resolve));
+
+ return test_driver.click(button)
+ .then(wait_click)
+ .then(function() {
+ button.remove();
+
+ if (typeof action === "function") {
+ return action();
+ }
+ return null;
+ });
+ },
+
+ /**
+ * Triggers a user-initiated click
+ *
+ * This matches the behaviour of the {@link
+ * https://w3c.github.io/webdriver/#element-click|WebDriver
+ * Element Click command}.
+ *
+ * @param {Element} element - element to be clicked
+ * @returns {Promise} fulfilled after click occurs, or rejected in
+ * the cases the WebDriver command errors
+ */
+ click: function(element) {
+ if (!inView(element)) {
+ element.scrollIntoView({behavior: "instant",
+ block: "end",
+ inline: "nearest"});
+ }
+
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ if (pointerInteractablePaintTree.length === 0 ||
+ !element.contains(pointerInteractablePaintTree[0])) {
+ return Promise.reject(new Error("element click intercepted error"));
+ }
+
+ var rect = element.getClientRects()[0];
+ var centerPoint = getInViewCenterPoint(rect);
+ return window.test_driver_internal.click(element,
+ {x: centerPoint[0],
+ y: centerPoint[1]});
+ },
+
+ /**
+ * Deletes all cookies.
+ *
+ * This matches the behaviour of the {@link
+ * https://w3c.github.io/webdriver/#delete-all-cookies|WebDriver
+ * Delete All Cookies command}.
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after cookies are deleted, or rejected in
+ * the cases the WebDriver command errors
+ */
+ delete_all_cookies: function(context=null) {
+ return window.test_driver_internal.delete_all_cookies(context);
+ },
+
+ /**
+ * Send keys to an element
+ *
+ * This matches the behaviour of the {@link
+ * https://w3c.github.io/webdriver/#element-send-keys|WebDriver
+ * Send Keys command}.
+ *
+ * @param {Element} element - element to send keys to
+ * @param {String} keys - keys to send to the element
+ * @returns {Promise} fulfilled after keys are sent, or rejected in
+ * the cases the WebDriver command errors
+ */
+ send_keys: function(element, keys) {
+ if (!inView(element)) {
+ element.scrollIntoView({behavior: "instant",
+ block: "end",
+ inline: "nearest"});
+ }
+
+ var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
+ if (pointerInteractablePaintTree.length === 0 ||
+ !element.contains(pointerInteractablePaintTree[0])) {
+ return Promise.reject(new Error("element send_keys intercepted error"));
+ }
+
+ return window.test_driver_internal.send_keys(element, keys);
+ },
+
+ /**
+ * Freeze the current page
+ *
+ * The freeze function transitions the page from the HIDDEN state to
+ * the FROZEN state as described in {@link
+ * https://github.com/WICG/page-lifecycle/blob/master/README.md|Lifecycle API
+ * for Web Pages}
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the freeze request is sent, or rejected
+ * in case the WebDriver command errors
+ */
+ freeze: function(context=null) {
+ return window.test_driver_internal.freeze();
+ },
+
+ /**
+ * 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
+ * @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
+ * the cases the WebDriver command errors
+ */
+ action_sequence: function(actions, context=null) {
+ return window.test_driver_internal.action_sequence(actions, context);
+ },
+
+ /**
+ * Generates a test report on the current page
+ *
+ * The generate_test_report function generates a report (to be observed
+ * by ReportingObserver) for testing purposes, as described in
+ * {@link https://w3c.github.io/reporting/#generate-test-report-command}
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the report is generated, or
+ * rejected if the report generation fails
+ */
+ generate_test_report: function(message, context=null) {
+ return window.test_driver_internal.generate_test_report(message, context);
+ },
+
+ /**
+ * Sets the state of a permission
+ *
+ * This function simulates a user setting a permission into a particular state as described
+ * in {@link https://w3c.github.io/permissions/#set-permission-command}
+ *
+ * @param {Object} descriptor - a [PermissionDescriptor]{@link
+ * https://w3c.github.io/permissions/#dictdef-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
+ */
+ set_permission: function(descriptor, state, one_realm, context=null) {
+ let permission_params = {
+ descriptor,
+ state,
+ oneRealm: one_realm,
+ };
+ return window.test_driver_internal.set_permission(permission_params, context);
+ },
+
+ /**
+ * Creates a virtual authenticator
+ *
+ * This function creates a virtual authenticator for use with the U2F
+ * and WebAuthn APIs as described in {@link
+ * https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator}
+ *
+ * @param {Object} config - an [Authenticator Configuration]{@link
+ * https://w3c.github.io/webauthn/#authenticator-configuration}
+ * object
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the authenticator is added, or
+ * rejected in the cases the WebDriver command
+ * errors. Returns the ID of the authenticator
+ */
+ add_virtual_authenticator: function(config, context=null) {
+ return window.test_driver_internal.add_virtual_authenticator(config, context);
+ },
+
+ /**
+ * Removes a virtual authenticator
+ *
+ * This function removes a virtual authenticator that has been created
+ * by add_virtual_authenticator
+ * https://w3c.github.io/webauthn/#sctn-automation-remove-virtual-authenticator
+ *
+ * @param {String} authenticator_id - the ID of the authenticator to be
+ * removed.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the authenticator is removed, or
+ * rejected in the cases the WebDriver command
+ * errors
+ */
+ remove_virtual_authenticator: function(authenticator_id, context=null) {
+ return window.test_driver_internal.remove_virtual_authenticator(authenticator_id, context);
+ },
+
+ /**
+ * Adds a credential to a virtual authenticator
+ *
+ * https://w3c.github.io/webauthn/#sctn-automation-add-credential
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {Object} credential - A [Credential Parameters]{@link
+ * https://w3c.github.io/webauthn/#credential-parameters}
+ * object
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credential is added, or
+ * rejected in the cases the WebDriver command
+ * errors
+ */
+ add_credential: function(authenticator_id, credential, context=null) {
+ return window.test_driver_internal.add_credential(authenticator_id, credential, context);
+ },
+
+ /**
+ * Gets all the credentials stored in an authenticator
+ *
+ * This function retrieves all the credentials (added via the U2F API,
+ * WebAuthn, or the add_credential function) stored in a virtual
+ * authenticator
+ * https://w3c.github.io/webauthn/#sctn-automation-get-credentials
+ *
+ * @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}
+ */
+ get_credentials: function(authenticator_id, context=null) {
+ return window.test_driver_internal.get_credentials(authenticator_id, context=null);
+ },
+
+ /**
+ * Remove a credential stored in an authenticator
+ *
+ * https://w3c.github.io/webauthn/#sctn-automation-remove-credential
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {String} credential_id - the ID of the credential
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credential is removed, or
+ * rejected in the cases the WebDriver command
+ * errors.
+ */
+ remove_credential: function(authenticator_id, credential_id, context=null) {
+ return window.test_driver_internal.remove_credential(authenticator_id, credential_id, context);
+ },
+
+ /**
+ * Removes all the credentials stored in a virtual authenticator
+ *
+ * https://w3c.github.io/webauthn/#sctn-automation-remove-all-credentials
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} fulfilled after the credentials are removed, or
+ * rejected in the cases the WebDriver command
+ * errors.
+ */
+ remove_all_credentials: function(authenticator_id, context=null) {
+ return window.test_driver_internal.remove_all_credentials(authenticator_id, context);
+ },
+
+ /**
+ * Sets the User Verified flag on an authenticator
+ *
+ * Sets whether requests requiring user verification will succeed or
+ * fail on a given virtual authenticator
+ * https://w3c.github.io/webauthn/#sctn-automation-set-user-verified
+ *
+ * @param {String} authenticator_id - the ID of the authenticator
+ * @param {boolean} uv - the User Verified flag
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ */
+ set_user_verified: function(authenticator_id, uv, context=null) {
+ return window.test_driver_internal.set_user_verified(authenticator_id, uv, context);
+ },
+
+ /**
+ * Sets the storage access rule for an origin when embedded
+ * in a third-party context.
+ *
+ * {@link https://privacycg.github.io/storage-access/#set-storage-access-command}
+ *
+ * @param {String} origin - A third-party origin to block or allow.
+ * May be "*" to indicate all origins.
+ * @param {String} embedding_origin - an embedding (first-party) origin
+ * on which {origin}'s access should
+ * be blocked or allowed.
+ * May be "*" to indicate all origins.
+ * @param {String} state - The storage access setting.
+ * Must be either "allowed" or "blocked".
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Fulfilled after the storage access rule has been
+ * set, or rejected if setting the rule fails.
+ */
+ set_storage_access: function(origin, embedding_origin, state, context=null) {
+ if (state !== "allowed" && state !== "blocked") {
+ throw new Error("storage access status must be 'allowed' or 'blocked'");
+ }
+ const blocked = state === "blocked";
+ return window.test_driver_internal.set_storage_access(origin, embedding_origin, blocked, context);
+ },
+ };
+
+ window.test_driver_internal = {
+ /**
+ * This flag should be set to `true` by any code which implements the
+ * internal methods defined below for automation purposes. Doing so
+ * allows the library to signal failure immediately when an automated
+ * implementation of one of the methods is not available.
+ */
+ in_automation: false,
+
+ click: function(element, coords) {
+ if (this.in_automation) {
+ return Promise.reject(new Error('Not implemented'));
+ }
+
+ return new Promise(function(resolve, reject) {
+ element.addEventListener("click", resolve);
+ });
+ },
+
+ delete_all_cookies: function(context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ send_keys: function(element, keys) {
+ if (this.in_automation) {
+ return Promise.reject(new Error('Not implemented'));
+ }
+
+ return new Promise(function(resolve, reject) {
+ var seen = "";
+
+ function remove() {
+ element.removeEventListener("keydown", onKeyDown);
+ }
+
+ function onKeyDown(event) {
+ if (event.key.length > 1) {
+ return;
+ }
+
+ seen += event.key;
+
+ if (keys.indexOf(seen) !== 0) {
+ reject(new Error("Unexpected key sequence: " + seen));
+ remove();
+ } else if (seen === keys) {
+ resolve();
+ remove();
+ }
+ }
+
+ element.addEventListener("keydown", onKeyDown);
+ });
+ },
+
+ freeze: function(context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ action_sequence: function(actions, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ generate_test_report: function(message, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+
+ set_permission: function(permission_params, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ add_virtual_authenticator: function(config, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ remove_virtual_authenticator: function(authenticator_id, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ add_credential: function(authenticator_id, credential, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ get_credentials: function(authenticator_id, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ remove_credential: function(authenticator_id, credential_id, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ remove_all_credentials: function(authenticator_id, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ set_user_verified: function(authenticator_id, uv, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ set_storage_access: function(origin, embedding_origin, blocked, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+ };
+})();
diff --git a/test/fixtures/wpt/resources/testdriver.js.headers b/test/fixtures/wpt/resources/testdriver.js.headers
new file mode 100644
index 0000000..5e8f640
--- /dev/null
+++ b/test/fixtures/wpt/resources/testdriver.js.headers
@@ -0,0 +1,2 @@
+Content-Type: text/javascript; charset=utf-8
+Cache-Control: max-age=3600
diff --git a/test/fixtures/wpt/resources/testharness.js b/test/fixtures/wpt/resources/testharness.js
new file mode 100644
index 0000000..a49e604
--- /dev/null
+++ b/test/fixtures/wpt/resources/testharness.js
@@ -0,0 +1,4221 @@
+/*global self*/
+/*jshint latedef: nofunc*/
+/*
+Distributed under both the W3C Test Suite License [1] and the W3C
+3-clause BSD License [2]. To contribute to a W3C Test Suite, see the
+policies and contribution forms [3].
+
+[1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license
+[2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license
+[3] http://www.w3.org/2004/10/27-testcases
+*/
+
+/* Documentation: https://web-platform-tests.org/writing-tests/testharness-api.html
+ * (../docs/_writing-tests/testharness-api.md) */
+
+(function (global_scope)
+{
+ // default timeout is 10 seconds, test can override if needed
+ var settings = {
+ output:true,
+ harness_timeout:{
+ "normal":10000,
+ "long":60000
+ },
+ test_timeout:null,
+ message_events: ["start", "test_state", "result", "completion"],
+ debug: false,
+ };
+
+ var xhtml_ns = "http://www.w3.org/1999/xhtml";
+
+ /*
+ * TestEnvironment is an abstraction for the environment in which the test
+ * harness is used. Each implementation of a test environment has to provide
+ * the following interface:
+ *
+ * interface TestEnvironment {
+ * // Invoked after the global 'tests' object has been created and it's
+ * // safe to call add_*_callback() to register event handlers.
+ * void on_tests_ready();
+ *
+ * // Invoked after setup() has been called to notify the test environment
+ * // of changes to the test harness properties.
+ * void on_new_harness_properties(object properties);
+ *
+ * // Should return a new unique default test name.
+ * DOMString next_default_test_name();
+ *
+ * // Should return the test harness timeout duration in milliseconds.
+ * float test_timeout();
+ * };
+ */
+
+ /*
+ * A test environment with a DOM. The global object is 'window'. By default
+ * test results are displayed in a table. Any parent windows receive
+ * callbacks or messages via postMessage() when test events occur. See
+ * apisample11.html and apisample12.html.
+ */
+ function WindowTestEnvironment() {
+ this.name_counter = 0;
+ this.window_cache = null;
+ this.output_handler = null;
+ this.all_loaded = false;
+ var this_obj = this;
+ this.message_events = [];
+ this.dispatched_messages = [];
+
+ this.message_functions = {
+ start: [add_start_callback, remove_start_callback,
+ function (properties) {
+ this_obj._dispatch("start_callback", [properties],
+ {type: "start", properties: properties});
+ }],
+
+ test_state: [add_test_state_callback, remove_test_state_callback,
+ function(test) {
+ this_obj._dispatch("test_state_callback", [test],
+ {type: "test_state",
+ test: test.structured_clone()});
+ }],
+ result: [add_result_callback, remove_result_callback,
+ function (test) {
+ this_obj.output_handler.show_status();
+ this_obj._dispatch("result_callback", [test],
+ {type: "result",
+ test: test.structured_clone()});
+ }],
+ completion: [add_completion_callback, remove_completion_callback,
+ function (tests, harness_status, asserts) {
+ var cloned_tests = map(tests, function(test) {
+ return test.structured_clone();
+ });
+ this_obj._dispatch("completion_callback", [tests, harness_status],
+ {type: "complete",
+ tests: cloned_tests,
+ status: harness_status.structured_clone(),
+ asserts: asserts.map(assert => assert.structured_clone())});
+ }]
+ }
+
+ on_event(window, 'load', function() {
+ this_obj.all_loaded = true;
+ });
+
+ on_event(window, 'message', function(event) {
+ if (event.data && event.data.type === "getmessages" && event.source) {
+ // A window can post "getmessages" to receive a duplicate of every
+ // message posted by this environment so far. This allows subscribers
+ // from fetch_tests_from_window to 'catch up' to the current state of
+ // this environment.
+ for (var i = 0; i < this_obj.dispatched_messages.length; ++i)
+ {
+ event.source.postMessage(this_obj.dispatched_messages[i], "*");
+ }
+ }
+ });
+ }
+
+ WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) {
+ this.dispatched_messages.push(message_arg);
+ this._forEach_windows(
+ function(w, same_origin) {
+ if (same_origin) {
+ try {
+ var has_selector = selector in w;
+ } catch(e) {
+ // If document.domain was set at some point same_origin can be
+ // wrong and the above will fail.
+ has_selector = false;
+ }
+ if (has_selector) {
+ try {
+ w[selector].apply(undefined, callback_args);
+ } catch (e) {}
+ }
+ }
+ if (supports_post_message(w) && w !== self) {
+ w.postMessage(message_arg, "*");
+ }
+ });
+ };
+
+ WindowTestEnvironment.prototype._forEach_windows = function(callback) {
+ // Iterate over the windows [self ... top, opener]. The callback is passed
+ // two objects, the first one is the window object itself, the second one
+ // is a boolean indicating whether or not it's on the same origin as the
+ // current window.
+ var cache = this.window_cache;
+ if (!cache) {
+ cache = [[self, true]];
+ var w = self;
+ var i = 0;
+ var so;
+ while (w != w.parent) {
+ w = w.parent;
+ so = is_same_origin(w);
+ cache.push([w, so]);
+ i++;
+ }
+ w = window.opener;
+ if (w) {
+ cache.push([w, is_same_origin(w)]);
+ }
+ this.window_cache = cache;
+ }
+
+ forEach(cache,
+ function(a) {
+ callback.apply(null, a);
+ });
+ };
+
+ WindowTestEnvironment.prototype.on_tests_ready = function() {
+ var output = new Output();
+ this.output_handler = output;
+
+ var this_obj = this;
+
+ add_start_callback(function (properties) {
+ this_obj.output_handler.init(properties);
+ });
+
+ add_test_state_callback(function(test) {
+ this_obj.output_handler.show_status();
+ });
+
+ add_result_callback(function (test) {
+ this_obj.output_handler.show_status();
+ });
+
+ add_completion_callback(function (tests, harness_status, asserts_run) {
+ this_obj.output_handler.show_results(tests, harness_status, asserts_run);
+ });
+ this.setup_messages(settings.message_events);
+ };
+
+ WindowTestEnvironment.prototype.setup_messages = function(new_events) {
+ var this_obj = this;
+ forEach(settings.message_events, function(x) {
+ var current_dispatch = this_obj.message_events.indexOf(x) !== -1;
+ var new_dispatch = new_events.indexOf(x) !== -1;
+ if (!current_dispatch && new_dispatch) {
+ this_obj.message_functions[x][0](this_obj.message_functions[x][2]);
+ } else if (current_dispatch && !new_dispatch) {
+ this_obj.message_functions[x][1](this_obj.message_functions[x][2]);
+ }
+ });
+ this.message_events = new_events;
+ }
+
+ WindowTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) {
+ this.output_handler.setup(properties);
+ if (properties.hasOwnProperty("message_events")) {
+ this.setup_messages(properties.message_events);
+ }
+ };
+
+ WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ on_event(window, 'load', callback);
+ };
+
+ WindowTestEnvironment.prototype.test_timeout = function() {
+ var metas = document.getElementsByTagName("meta");
+ for (var i = 0; i < metas.length; i++) {
+ if (metas[i].name == "timeout") {
+ if (metas[i].content == "long") {
+ return settings.harness_timeout.long;
+ }
+ break;
+ }
+ }
+ return settings.harness_timeout.normal;
+ };
+
+ /*
+ * Base TestEnvironment implementation for a generic web worker.
+ *
+ * Workers accumulate test results. One or more clients can connect and
+ * retrieve results from a worker at any time.
+ *
+ * WorkerTestEnvironment supports communicating with a client via a
+ * MessagePort. The mechanism for determining the appropriate MessagePort
+ * for communicating with a client depends on the type of worker and is
+ * implemented by the various specializations of WorkerTestEnvironment
+ * below.
+ *
+ * A client document using testharness can use fetch_tests_from_worker() to
+ * retrieve results from a worker. See apisample16.html.
+ */
+ function WorkerTestEnvironment() {
+ this.name_counter = 0;
+ this.all_loaded = true;
+ this.message_list = [];
+ this.message_ports = [];
+ }
+
+ WorkerTestEnvironment.prototype._dispatch = function(message) {
+ this.message_list.push(message);
+ for (var i = 0; i < this.message_ports.length; ++i)
+ {
+ this.message_ports[i].postMessage(message);
+ }
+ };
+
+ // The only requirement is that port has a postMessage() method. It doesn't
+ // have to be an instance of a MessagePort, and often isn't.
+ WorkerTestEnvironment.prototype._add_message_port = function(port) {
+ this.message_ports.push(port);
+ for (var i = 0; i < this.message_list.length; ++i)
+ {
+ port.postMessage(this.message_list[i]);
+ }
+ };
+
+ WorkerTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return get_title() + suffix;
+ };
+
+ WorkerTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+ WorkerTestEnvironment.prototype.on_tests_ready = function() {
+ var this_obj = this;
+ add_start_callback(
+ function(properties) {
+ this_obj._dispatch({
+ type: "start",
+ properties: properties,
+ });
+ });
+ add_test_state_callback(
+ function(test) {
+ this_obj._dispatch({
+ type: "test_state",
+ test: test.structured_clone()
+ });
+ });
+ add_result_callback(
+ function(test) {
+ this_obj._dispatch({
+ type: "result",
+ test: test.structured_clone()
+ });
+ });
+ add_completion_callback(
+ function(tests, harness_status, asserts) {
+ this_obj._dispatch({
+ type: "complete",
+ tests: map(tests,
+ function(test) {
+ return test.structured_clone();
+ }),
+ status: harness_status.structured_clone(),
+ asserts: asserts.map(assert => assert.structured_clone()),
+ });
+ });
+ };
+
+ WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {};
+
+ WorkerTestEnvironment.prototype.test_timeout = function() {
+ // Tests running in a worker don't have a default timeout. I.e. all
+ // worker tests behave as if settings.explicit_timeout is true.
+ return null;
+ };
+
+ /*
+ * Dedicated web workers.
+ * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a dedicated worker.
+ */
+ function DedicatedWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ // self is an instance of DedicatedWorkerGlobalScope which exposes
+ // a postMessage() method for communicating via the message channel
+ // established when the worker is created.
+ this._add_message_port(self);
+ }
+ DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+ WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+ // In the absence of an onload notification, we a require dedicated
+ // workers to explicitly signal when the tests are done.
+ tests.wait_for_finish = true;
+ };
+
+ /*
+ * Shared web workers.
+ * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a shared web worker.
+ */
+ function SharedWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ var this_obj = this;
+ // Shared workers receive message ports via the 'onconnect' event for
+ // each connection.
+ self.addEventListener("connect",
+ function(message_event) {
+ this_obj._add_message_port(message_event.source);
+ }, false);
+ }
+ SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ SharedWorkerTestEnvironment.prototype.on_tests_ready = function() {
+ WorkerTestEnvironment.prototype.on_tests_ready.call(this);
+ // In the absence of an onload notification, we a require shared
+ // workers to explicitly signal when the tests are done.
+ tests.wait_for_finish = true;
+ };
+
+ /*
+ * Service workers.
+ * http://www.w3.org/TR/service-workers/
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a service worker.
+ */
+ function ServiceWorkerTestEnvironment() {
+ WorkerTestEnvironment.call(this);
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ var this_obj = this;
+ self.addEventListener("message",
+ function(event) {
+ if (event.data && event.data.type && event.data.type === "connect") {
+ this_obj._add_message_port(event.source);
+ }
+ }, false);
+
+ // The oninstall event is received after the service worker script and
+ // all imported scripts have been fetched and executed. It's the
+ // equivalent of an onload event for a document. All tests should have
+ // been added by the time this event is received, thus it's not
+ // necessary to wait until the onactivate event. However, tests for
+ // installed service workers need another event which is equivalent to
+ // the onload event because oninstall is fired only on installation. The
+ // onmessage event is used for that purpose since tests using
+ // testharness.js should ask the result to its service worker by
+ // PostMessage. If the onmessage event is triggered on the service
+ // worker's context, that means the worker's script has been evaluated.
+ on_event(self, "install", on_all_loaded);
+ on_event(self, "message", on_all_loaded);
+ function on_all_loaded() {
+ if (this_obj.all_loaded)
+ return;
+ this_obj.all_loaded = true;
+ if (this_obj.on_loaded_callback) {
+ this_obj.on_loaded_callback();
+ }
+ }
+ }
+
+ ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype);
+
+ ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ /*
+ * JavaScript shells.
+ *
+ * This class is used as the test_environment when testharness is running
+ * inside a JavaScript shell.
+ */
+ function ShellTestEnvironment() {
+ this.name_counter = 0;
+ this.all_loaded = false;
+ this.on_loaded_callback = null;
+ Promise.resolve().then(function() {
+ this.all_loaded = true
+ if (this.on_loaded_callback) {
+ this.on_loaded_callback();
+ }
+ }.bind(this));
+ this.message_list = [];
+ this.message_ports = [];
+ }
+
+ ShellTestEnvironment.prototype.next_default_test_name = function() {
+ var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
+ this.name_counter++;
+ return "Untitled" + suffix;
+ };
+
+ ShellTestEnvironment.prototype.on_new_harness_properties = function() {};
+
+ ShellTestEnvironment.prototype.on_tests_ready = function() {};
+
+ ShellTestEnvironment.prototype.add_on_loaded_callback = function(callback) {
+ if (this.all_loaded) {
+ callback();
+ } else {
+ this.on_loaded_callback = callback;
+ }
+ };
+
+ ShellTestEnvironment.prototype.test_timeout = function() {
+ // Tests running in a shell don't have a default timeout, so behave as
+ // if settings.explicit_timeout is true.
+ return null;
+ };
+
+ function create_test_environment() {
+ if ('document' in global_scope) {
+ return new WindowTestEnvironment();
+ }
+ if ('DedicatedWorkerGlobalScope' in global_scope &&
+ global_scope instanceof DedicatedWorkerGlobalScope) {
+ return new DedicatedWorkerTestEnvironment();
+ }
+ if ('SharedWorkerGlobalScope' in global_scope &&
+ global_scope instanceof SharedWorkerGlobalScope) {
+ return new SharedWorkerTestEnvironment();
+ }
+ if ('ServiceWorkerGlobalScope' in global_scope &&
+ global_scope instanceof ServiceWorkerGlobalScope) {
+ return new ServiceWorkerTestEnvironment();
+ }
+ if ('WorkerGlobalScope' in global_scope &&
+ global_scope instanceof WorkerGlobalScope) {
+ return new DedicatedWorkerTestEnvironment();
+ }
+
+ return new ShellTestEnvironment();
+ }
+
+ var test_environment = create_test_environment();
+
+ function is_shared_worker(worker) {
+ return 'SharedWorker' in global_scope && worker instanceof SharedWorker;
+ }
+
+ function is_service_worker(worker) {
+ // The worker object may be from another execution context,
+ // so do not use instanceof here.
+ return 'ServiceWorker' in global_scope &&
+ Object.prototype.toString.call(worker) == '[object ServiceWorker]';
+ }
+
+ var seen_func_name = Object.create(null);
+
+ function get_test_name(func, name)
+ {
+ if (name) {
+ return name;
+ }
+
+ if (func) {
+ var func_code = func.toString();
+
+ // Try and match with brackets, but fallback to matching without
+ var arrow = func_code.match(/^\(\)\s*=>\s*(?:{(.*)}\s*|(.*))$/);
+
+ // Check for JS line separators
+ if (arrow !== null && !/[\u000A\u000D\u2028\u2029]/.test(func_code)) {
+ var trimmed = (arrow[1] !== undefined ? arrow[1] : arrow[2]).trim();
+ // drop trailing ; if there's no earlier ones
+ trimmed = trimmed.replace(/^([^;]*)(;\s*)+$/, "$1");
+
+ if (trimmed) {
+ let name = trimmed;
+ if (seen_func_name[trimmed]) {
+ // This subtest name already exists, so add a suffix.
+ name += " " + seen_func_name[trimmed];
+ } else {
+ seen_func_name[trimmed] = 0;
+ }
+ seen_func_name[trimmed] += 1;
+ return name;
+ }
+ }
+ }
+
+ return test_environment.next_default_test_name();
+ }
+
+ /*
+ * API functions
+ */
+ function test(func, name, properties)
+ {
+ if (tests.promise_setup_called) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = '`test` invoked after `promise_setup`';
+ tests.complete();
+ }
+ var test_name = get_test_name(func, name);
+ var test_obj = new Test(test_name, properties);
+ var value = test_obj.step(func, test_obj, test_obj);
+
+ if (value !== undefined) {
+ var msg = 'Test named "' + test_name +
+ '" passed a function to `test` that returned a value.';
+
+ try {
+ if (value && typeof value.then === 'function') {
+ msg += ' Consider using `promise_test` instead when ' +
+ 'using Promises or async/await.';
+ }
+ } catch (err) {}
+
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = msg;
+ }
+
+ if (test_obj.phase === test_obj.phases.STARTED) {
+ test_obj.done();
+ }
+ }
+
+ function async_test(func, name, properties)
+ {
+ if (tests.promise_setup_called) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = '`async_test` invoked after `promise_setup`';
+ tests.complete();
+ }
+ if (typeof func !== "function") {
+ properties = name;
+ name = func;
+ func = null;
+ }
+ var test_name = get_test_name(func, name);
+ var test_obj = new Test(test_name, properties);
+ if (func) {
+ var value = test_obj.step(func, test_obj, test_obj);
+
+ // Test authors sometimes return values to async_test, expecting us
+ // to handle the value somehow. Make doing so a harness error to be
+ // clear this is invalid, and point authors to promise_test if it
+ // may be appropriate.
+ //
+ // Note that we only perform this check on the initial function
+ // passed to async_test, not on any later steps - we haven't seen a
+ // consistent problem with those (and it's harder to check).
+ if (value !== undefined) {
+ var msg = 'Test named "' + test_name +
+ '" passed a function to `async_test` that returned a value.';
+
+ try {
+ if (value && typeof value.then === 'function') {
+ msg += ' Consider using `promise_test` instead when ' +
+ 'using Promises or async/await.';
+ }
+ } catch (err) {}
+
+ tests.set_status(tests.status.ERROR, msg);
+ tests.complete();
+ }
+ }
+ return test_obj;
+ }
+
+ function promise_test(func, name, properties) {
+ if (typeof func !== "function") {
+ properties = name;
+ name = func;
+ func = null;
+ }
+ var test_name = get_test_name(func, name);
+ var test = new Test(test_name, properties);
+ test._is_promise_test = true;
+
+ // If there is no promise tests queue make one.
+ if (!tests.promise_tests) {
+ tests.promise_tests = Promise.resolve();
+ }
+ tests.promise_tests = tests.promise_tests.then(function() {
+ return new Promise(function(resolve) {
+ var promise = test.step(func, test, test);
+
+ test.step(function() {
+ assert(!!promise, "promise_test", null,
+ "test body must return a 'thenable' object (received ${value})",
+ {value:promise});
+ assert(typeof promise.then === "function", "promise_test", null,
+ "test body must return a 'thenable' object (received an object with no `then` method)",
+ null);
+ });
+
+ // Test authors may use the `step` method within a
+ // `promise_test` even though this reflects a mixture of
+ // asynchronous control flow paradigms. The "done" callback
+ // should be registered prior to the resolution of the
+ // user-provided Promise to avoid timeouts in cases where the
+ // Promise does not settle but a `step` function has thrown an
+ // error.
+ add_test_done_callback(test, resolve);
+
+ Promise.resolve(promise)
+ .catch(test.step_func(
+ function(value) {
+ if (value instanceof AssertionError) {
+ throw value;
+ }
+ assert(false, "promise_test", null,
+ "Unhandled rejection with value: ${value}", {value:value});
+ }))
+ .then(function() {
+ test.done();
+ });
+ });
+ });
+ }
+
+ /**
+ * Make a copy of a Promise in the current realm.
+ *
+ * @param {Promise} promise the given promise that may be from a different
+ * realm
+ * @returns {Promise}
+ *
+ * An arbitrary promise provided by the caller may have originated in
+ * another frame that have since navigated away, rendering the frame's
+ * document inactive. Such a promise cannot be used with `await` or
+ * Promise.resolve(), as microtasks associated with it may be prevented
+ * from being run. See https://github.com/whatwg/html/issues/5319 for a
+ * particular case.
+ *
+ * In functions we define here, there is an expectation from the caller
+ * that the promise is from the current realm, that can always be used with
+ * `await`, etc. We therefore create a new promise in this realm that
+ * inherit the value and status from the given promise.
+ */
+
+ function bring_promise_to_current_realm(promise) {
+ return new Promise(promise.then.bind(promise));
+ }
+
+ function promise_rejects_js(test, constructor, promise, description) {
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_js_impl(constructor, function() { throw e },
+ description, "promise_rejects_js");
+ });
+ }
+
+ /**
+ * Assert that a Promise is rejected with the right DOMException.
+ *
+ * @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:
+ *
+ * 1) If the DOMException is expected to come from the current global, the
+ * third argument should be the promise expected to reject, and a fourth,
+ * optional, argument is the assertion description.
+ *
+ * 2) If the DOMException is expected to come from some other global, the
+ * third argument should be the DOMException constructor from that global,
+ * the fourth argument the promise expected to reject, and the fifth,
+ * optional, argument the assertion description.
+ */
+
+ function promise_rejects_dom(test, type, promiseOrConstructor, descriptionOrPromise, maybeDescription) {
+ let constructor, promise, description;
+ if (typeof promiseOrConstructor === "function" &&
+ promiseOrConstructor.name === "DOMException") {
+ constructor = promiseOrConstructor;
+ promise = descriptionOrPromise;
+ description = maybeDescription;
+ } else {
+ constructor = self.DOMException;
+ promise = promiseOrConstructor;
+ description = descriptionOrPromise;
+ assert(maybeDescription === undefined,
+ "Too many args pased to no-constructor version of promise_rejects_dom");
+ }
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_dom_impl(type, function() { throw e }, description,
+ "promise_rejects_dom", constructor);
+ });
+ }
+
+ function promise_rejects_exactly(test, exception, promise, description) {
+ return bring_promise_to_current_realm(promise)
+ .then(test.unreached_func("Should have rejected: " + description))
+ .catch(function(e) {
+ assert_throws_exactly_impl(exception, function() { throw e },
+ description, "promise_rejects_exactly");
+ });
+ }
+
+ /**
+ * 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,
+ * including ensuring that unexpected events are not fired at any point.
+ */
+ function EventWatcher(test, watchedNode, eventTypes, timeoutPromise)
+ {
+ if (typeof eventTypes == 'string') {
+ eventTypes = [eventTypes];
+ }
+
+ var waitingFor = null;
+
+ // This is null unless we are recording all events, in which case it
+ // will be an Array object.
+ var recordedEvents = null;
+
+ var eventHandler = test.step_func(function(evt) {
+ assert_true(!!waitingFor,
+ 'Not expecting event, but got ' + evt.type + ' event');
+ assert_equals(evt.type, waitingFor.types[0],
+ 'Expected ' + waitingFor.types[0] + ' event, but got ' +
+ evt.type + ' event instead');
+
+ if (Array.isArray(recordedEvents)) {
+ recordedEvents.push(evt);
+ }
+
+ if (waitingFor.types.length > 1) {
+ // Pop first event from array
+ waitingFor.types.shift();
+ return;
+ }
+ // We need to null out waitingFor before calling the resolve function
+ // since the Promise's resolve handlers may call wait_for() which will
+ // need to set waitingFor.
+ var resolveFunc = waitingFor.resolve;
+ waitingFor = null;
+ // Likewise, we should reset the state of recordedEvents.
+ var result = recordedEvents || evt;
+ recordedEvents = null;
+ resolveFunc(result);
+ });
+
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.addEventListener(eventTypes[i], eventHandler, false);
+ }
+
+ /**
+ * Returns a Promise that will resolve after the specified event or
+ * series of events has occurred.
+ *
+ * @param 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
+ * const watcher = new EventWatcher(t, div, [ 'animationstart',
+ * 'animationiteration',
+ * 'animationend' ]);
+ * return watcher.wait_for([ 'animationstart', 'animationend' ],
+ * { record: 'all' }).then(evts => {
+ * assert_equals(evts[0].elapsedTime, 0.0);
+ * assert_equals(evts[1].elapsedTime, 2.0);
+ * });
+ * ```
+ */
+ this.wait_for = function(types, options) {
+ if (waitingFor) {
+ return Promise.reject('Already waiting for an event or events');
+ }
+ if (typeof types == 'string') {
+ types = [types];
+ }
+ if (options && options.record && options.record === 'all') {
+ recordedEvents = [];
+ }
+ return new Promise(function(resolve, reject) {
+ var timeout = test.step_func(function() {
+ // If the timeout fires after the events have been received
+ // or during a subsequent call to wait_for, ignore it.
+ if (!waitingFor || waitingFor.resolve !== resolve)
+ return;
+
+ // This should always fail, otherwise we should have
+ // resolved the promise.
+ assert_true(waitingFor.types.length == 0,
+ 'Timed out waiting for ' + waitingFor.types.join(', '));
+ var result = recordedEvents;
+ recordedEvents = null;
+ var resolveFunc = waitingFor.resolve;
+ waitingFor = null;
+ resolveFunc(result);
+ });
+
+ if (timeoutPromise) {
+ timeoutPromise().then(timeout);
+ }
+
+ waitingFor = {
+ types: types,
+ resolve: resolve,
+ reject: reject
+ };
+ });
+ };
+
+ function stop_watching() {
+ for (var i = 0; i < eventTypes.length; i++) {
+ watchedNode.removeEventListener(eventTypes[i], eventHandler, false);
+ }
+ };
+
+ test._add_cleanup(stop_watching);
+
+ return this;
+ }
+ expose(EventWatcher, 'EventWatcher');
+
+ function setup(func_or_properties, maybe_properties)
+ {
+ var func = null;
+ var properties = {};
+ if (arguments.length === 2) {
+ func = func_or_properties;
+ properties = maybe_properties;
+ } else if (func_or_properties instanceof Function) {
+ func = func_or_properties;
+ } else {
+ properties = func_or_properties;
+ }
+ tests.setup(func, properties);
+ test_environment.on_new_harness_properties(properties);
+ }
+
+ function promise_setup(func, maybe_properties)
+ {
+ if (typeof func !== "function") {
+ tests.set_status(tests.status.ERROR,
+ "promise_test invoked without a function");
+ tests.complete();
+ return;
+ }
+ tests.promise_setup_called = true;
+
+ if (!tests.promise_tests) {
+ tests.promise_tests = Promise.resolve();
+ }
+
+ tests.promise_tests = tests.promise_tests
+ .then(function()
+ {
+ var properties = maybe_properties || {};
+ var result;
+
+ tests.setup(null, properties);
+ result = func();
+ test_environment.on_new_harness_properties(properties);
+
+ if (!result || typeof result.then !== "function") {
+ throw "Non-thenable returned by function passed to `promise_setup`";
+ }
+ return result;
+ })
+ .catch(function(e)
+ {
+ tests.set_status(tests.status.ERROR,
+ String(e),
+ e && e.stack);
+ tests.complete();
+ });
+ }
+
+ function done() {
+ if (tests.tests.length === 0) {
+ // `done` is invoked after handling uncaught exceptions, so if the
+ // harness status is already set, the corresponding message is more
+ // descriptive than the generic message defined here.
+ if (tests.status.status === null) {
+ tests.status.status = tests.status.ERROR;
+ tests.status.message = "done() was called without first defining any tests";
+ }
+
+ tests.complete();
+ return;
+ }
+ if (tests.file_is_test) {
+ // file is test files never have asynchronous cleanup logic,
+ // meaning the fully-synchronous `done` function can be used here.
+ tests.tests[0].done();
+ }
+ tests.end_wait();
+ }
+
+ function generate_tests(func, args, properties) {
+ forEach(args, function(x, i)
+ {
+ var name = x[0];
+ test(function()
+ {
+ func.apply(this, x.slice(1));
+ },
+ name,
+ Array.isArray(properties) ? properties[i] : properties);
+ });
+ }
+
+ /*
+ * 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.
+ */
+ function on_event(object, event, callback)
+ {
+ object.addEventListener(event, callback, false);
+ }
+
+ function step_timeout(f, t) {
+ var outer_this = this;
+ var args = Array.prototype.slice.call(arguments, 2);
+ return setTimeout(function() {
+ f.apply(outer_this, args);
+ }, t * tests.timeout_multiplier);
+ }
+
+ expose(test, 'test');
+ expose(async_test, 'async_test');
+ expose(promise_test, 'promise_test');
+ expose(promise_rejects_js, 'promise_rejects_js');
+ expose(promise_rejects_dom, 'promise_rejects_dom');
+ expose(promise_rejects_exactly, 'promise_rejects_exactly');
+ expose(generate_tests, 'generate_tests');
+ expose(setup, 'setup');
+ expose(promise_setup, 'promise_setup');
+ expose(done, 'done');
+ expose(on_event, 'on_event');
+ expose(step_timeout, 'step_timeout');
+
+ /*
+ * Return a string truncated to the given length, with ... added at the end
+ * if it was longer.
+ */
+ function truncate(s, len)
+ {
+ if (s.length > len) {
+ return s.substring(0, len - 3) + "...";
+ }
+ return s;
+ }
+
+ /*
+ * Return true if object is probably a Node object.
+ */
+ function is_node(object)
+ {
+ // I use duck-typing instead of instanceof, because
+ // instanceof doesn't work if the node is from another window (like an
+ // iframe's contentWindow):
+ // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295
+ try {
+ var has_node_properties = ("nodeType" in object &&
+ "nodeName" in object &&
+ "nodeValue" in object &&
+ "childNodes" in object);
+ } catch (e) {
+ // We're probably cross-origin, which means we aren't a node
+ return false;
+ }
+
+ if (has_node_properties) {
+ try {
+ object.nodeType;
+ } catch (e) {
+ // The object is probably Node.prototype or another prototype
+ // object that inherits from it, and not a Node instance.
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ var replacements = {
+ "0": "0",
+ "1": "x01",
+ "2": "x02",
+ "3": "x03",
+ "4": "x04",
+ "5": "x05",
+ "6": "x06",
+ "7": "x07",
+ "8": "b",
+ "9": "t",
+ "10": "n",
+ "11": "v",
+ "12": "f",
+ "13": "r",
+ "14": "x0e",
+ "15": "x0f",
+ "16": "x10",
+ "17": "x11",
+ "18": "x12",
+ "19": "x13",
+ "20": "x14",
+ "21": "x15",
+ "22": "x16",
+ "23": "x17",
+ "24": "x18",
+ "25": "x19",
+ "26": "x1a",
+ "27": "x1b",
+ "28": "x1c",
+ "29": "x1d",
+ "30": "x1e",
+ "31": "x1f",
+ "0xfffd": "ufffd",
+ "0xfffe": "ufffe",
+ "0xffff": "uffff",
+ };
+
+ /*
+ * Convert a value to a nice, human-readable string
+ */
+ function format_value(val, seen)
+ {
+ if (!seen) {
+ seen = [];
+ }
+ if (typeof val === "object" && val !== null) {
+ if (seen.indexOf(val) >= 0) {
+ return "[...]";
+ }
+ seen.push(val);
+ }
+ if (Array.isArray(val)) {
+ let output = "[";
+ if (val.beginEllipsis !== undefined) {
+ output += "…, ";
+ }
+ output += val.map(function(x) {return format_value(x, seen);}).join(", ");
+ if (val.endEllipsis !== undefined) {
+ output += ", …";
+ }
+ return output + "]";
+ }
+
+ switch (typeof val) {
+ case "string":
+ val = val.replace(/\\/g, "\\\\");
+ for (var p in replacements) {
+ var replace = "\\" + replacements[p];
+ val = val.replace(RegExp(String.fromCharCode(p), "g"), replace);
+ }
+ return '"' + val.replace(/"/g, '\\"') + '"';
+ case "boolean":
+ case "undefined":
+ return String(val);
+ case "number":
+ // In JavaScript, -0 === 0 and String(-0) == "0", so we have to
+ // special-case.
+ if (val === -0 && 1/val === -Infinity) {
+ return "-0";
+ }
+ return String(val);
+ case "object":
+ if (val === null) {
+ return "null";
+ }
+
+ // Special-case Node objects, since those come up a lot in my tests. I
+ // ignore namespaces.
+ if (is_node(val)) {
+ switch (val.nodeType) {
+ case Node.ELEMENT_NODE:
+ var ret = "<" + val.localName;
+ for (var i = 0; i < val.attributes.length; i++) {
+ ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"';
+ }
+ ret += ">" + val.innerHTML + "" + val.localName + ">";
+ return "Element node " + truncate(ret, 60);
+ case Node.TEXT_NODE:
+ return 'Text node "' + truncate(val.data, 60) + '"';
+ case Node.PROCESSING_INSTRUCTION_NODE:
+ return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60));
+ case Node.COMMENT_NODE:
+ return "Comment node ";
+ case Node.DOCUMENT_NODE:
+ return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+ case Node.DOCUMENT_TYPE_NODE:
+ return "DocumentType node";
+ case Node.DOCUMENT_FRAGMENT_NODE:
+ return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children");
+ default:
+ return "Node object of unknown type";
+ }
+ }
+
+ /* falls through */
+ default:
+ try {
+ return typeof val + ' "' + truncate(String(val), 1000) + '"';
+ } catch(e) {
+ return ("[stringifying object threw " + String(e) +
+ " with type " + String(typeof e) + "]");
+ }
+ }
+ }
+ expose(format_value, "format_value");
+
+ /*
+ * Assertions
+ */
+
+ function expose_assert(f, name) {
+ function assert_wrapper(...args) {
+ let status = Test.statuses.TIMEOUT;
+ let stack = null;
+ try {
+ if (settings.debug) {
+ console.debug("ASSERT", name, tests.current_test && tests.current_test.name, args);
+ }
+ if (tests.output) {
+ tests.set_assert(name, ...args);
+ }
+ const rv = f(...args);
+ status = Test.statuses.PASS;
+ return rv;
+ } catch(e) {
+ if (e instanceof AssertionError) {
+ status = Test.statuses.FAIL;
+ stack = e.stack;
+ } else {
+ status = Test.statuses.ERROR;
+ }
+ throw e;
+ } finally {
+ if (tests.output && !stack) {
+ stack = get_stack();
+ }
+ if (tests.output) {
+ tests.set_assert_status(status, stack);
+ }
+ }
+ }
+ expose(assert_wrapper, name);
+ }
+
+ function assert_true(actual, description)
+ {
+ assert(actual === true, "assert_true", description,
+ "expected true got ${actual}", {actual:actual});
+ }
+ expose_assert(assert_true, "assert_true");
+
+ function assert_false(actual, description)
+ {
+ assert(actual === false, "assert_false", description,
+ "expected false got ${actual}", {actual:actual});
+ }
+ expose_assert(assert_false, "assert_false");
+
+ function same_value(x, y) {
+ if (y !== y) {
+ //NaN case
+ return x !== x;
+ }
+ if (x === 0 && y === 0) {
+ //Distinguish +0 and -0
+ return 1/x === 1/y;
+ }
+ return x === y;
+ }
+
+ function assert_equals(actual, expected, description)
+ {
+ /*
+ * Test if two primitives are equal or two objects
+ * are the same object
+ */
+ if (typeof actual != typeof expected) {
+ assert(false, "assert_equals", description,
+ "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}",
+ {expected:expected, actual:actual});
+ return;
+ }
+ assert(same_value(actual, expected), "assert_equals", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_equals, "assert_equals");
+
+ 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");
+
+ function assert_in_array(actual, expected, description)
+ {
+ assert(expected.indexOf(actual) != -1, "assert_in_array", description,
+ "value ${actual} not in array ${expected}",
+ {actual:actual, expected:expected});
+ }
+ expose_assert(assert_in_array, "assert_in_array");
+
+ // This function was deprecated in July of 2015.
+ // See https://github.com/web-platform-tests/wpt/issues/2033
+ function assert_object_equals(actual, expected, description)
+ {
+ assert(typeof actual === "object" && actual !== null, "assert_object_equals", description,
+ "value is ${actual}, expected object",
+ {actual: actual});
+ //This needs to be improved a great deal
+ function check_equal(actual, expected, stack)
+ {
+ stack.push(actual);
+
+ var p;
+ for (p in actual) {
+ assert(expected.hasOwnProperty(p), "assert_object_equals", description,
+ "unexpected property ${p}", {p:p});
+
+ if (typeof actual[p] === "object" && actual[p] !== null) {
+ if (stack.indexOf(actual[p]) === -1) {
+ check_equal(actual[p], expected[p], stack);
+ }
+ } else {
+ assert(same_value(actual[p], expected[p]), "assert_object_equals", description,
+ "property ${p} expected ${expected} got ${actual}",
+ {p:p, expected:expected[p], actual:actual[p]});
+ }
+ }
+ for (p in expected) {
+ assert(actual.hasOwnProperty(p),
+ "assert_object_equals", description,
+ "expected property ${p} missing", {p:p});
+ }
+ stack.pop();
+ }
+ check_equal(actual, expected, []);
+ }
+ expose_assert(assert_object_equals, "assert_object_equals");
+
+ function assert_array_equals(actual, expected, description)
+ {
+ const max_array_length = 20;
+ function shorten_array(arr, offset = 0) {
+ // Make ", …" only show up when it would likely reduce the length, not accounting for
+ // fonts.
+ if (arr.length < max_array_length + 2) {
+ return arr;
+ }
+ // By default we want half the elements after the offset and half before
+ // But if that takes us past the end of the array, we have more before, and
+ // if it takes us before the start we have more after.
+ const length_after_offset = Math.floor(max_array_length / 2);
+ let upper_bound = Math.min(length_after_offset + offset, arr.length);
+ const lower_bound = Math.max(upper_bound - max_array_length, 0);
+
+ if (lower_bound === 0) {
+ upper_bound = max_array_length;
+ }
+
+ const output = arr.slice(lower_bound, upper_bound);
+ if (lower_bound > 0) {
+ output.beginEllipsis = true;
+ }
+ if (upper_bound < arr.length) {
+ output.endEllipsis = true;
+ }
+ return output;
+ }
+
+ assert(typeof actual === "object" && actual !== null && "length" in actual,
+ "assert_array_equals", description,
+ "value is ${actual}, expected array",
+ {actual:actual});
+ assert(actual.length === expected.length,
+ "assert_array_equals", description,
+ "lengths differ, expected array ${expected} length ${expectedLength}, got ${actual} length ${actualLength}",
+ {expected:shorten_array(expected, expected.length - 1), expectedLength:expected.length,
+ actual:shorten_array(actual, actual.length - 1), actualLength:actual.length
+ });
+
+ for (var i = 0; i < actual.length; i++) {
+ assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+ "assert_array_equals", description,
+ "expected property ${i} to be ${expected} but was ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+ {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+ actual:actual.hasOwnProperty(i) ? "present" : "missing",
+ arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+ assert(same_value(expected[i], actual[i]),
+ "assert_array_equals", description,
+ "expected property ${i} to be ${expected} but got ${actual} (expected array ${arrayExpected} got ${arrayActual})",
+ {i:i, expected:expected[i], actual:actual[i],
+ arrayExpected:shorten_array(expected, i), arrayActual:shorten_array(actual, i)});
+ }
+ }
+ expose_assert(assert_array_equals, "assert_array_equals");
+
+ function assert_array_approx_equals(actual, expected, epsilon, description)
+ {
+ /*
+ * Test if two primitive arrays are equal within +/- epsilon
+ */
+ assert(actual.length === expected.length,
+ "assert_array_approx_equals", description,
+ "lengths differ, expected ${expected} got ${actual}",
+ {expected:expected.length, actual:actual.length});
+
+ for (var i = 0; i < actual.length; i++) {
+ assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i),
+ "assert_array_approx_equals", description,
+ "property ${i}, property expected to be ${expected} but was ${actual}",
+ {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing",
+ actual:actual.hasOwnProperty(i) ? "present" : "missing"});
+ assert(typeof actual[i] === "number",
+ "assert_array_approx_equals", description,
+ "property ${i}, expected a number but got a ${type_actual}",
+ {i:i, type_actual:typeof actual[i]});
+ assert(Math.abs(actual[i] - expected[i]) <= epsilon,
+ "assert_array_approx_equals", description,
+ "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}",
+ {i:i, expected:expected[i], actual:actual[i], epsilon:epsilon});
+ }
+ }
+ expose_assert(assert_array_approx_equals, "assert_array_approx_equals");
+
+ function assert_approx_equals(actual, expected, epsilon, description)
+ {
+ /*
+ * Test if two primitive numbers are equal within +/- epsilon
+ */
+ assert(typeof actual === "number",
+ "assert_approx_equals", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ // The epsilon math below does not place nice with NaN and Infinity
+ // But in this case Infinity = Infinity and NaN = NaN
+ if (isFinite(actual) || isFinite(expected)) {
+ assert(Math.abs(actual - expected) <= epsilon,
+ "assert_approx_equals", description,
+ "expected ${expected} +/- ${epsilon} but got ${actual}",
+ {expected:expected, actual:actual, epsilon:epsilon});
+ } else {
+ assert_equals(actual, expected);
+ }
+ }
+ expose_assert(assert_approx_equals, "assert_approx_equals");
+
+ function assert_less_than(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is less than another
+ */
+ assert(typeof actual === "number",
+ "assert_less_than", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual < expected,
+ "assert_less_than", description,
+ "expected a number less than ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_less_than, "assert_less_than");
+
+ function assert_greater_than(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is greater than another
+ */
+ assert(typeof actual === "number",
+ "assert_greater_than", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual > expected,
+ "assert_greater_than", description,
+ "expected a number greater than ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_greater_than, "assert_greater_than");
+
+ function assert_between_exclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between two others
+ */
+ assert(typeof actual === "number",
+ "assert_between_exclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual > lower && actual < upper,
+ "assert_between_exclusive", description,
+ "expected a number greater than ${lower} " +
+ "and less than ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose_assert(assert_between_exclusive, "assert_between_exclusive");
+
+ function assert_less_than_equal(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is less than or equal to another
+ */
+ assert(typeof actual === "number",
+ "assert_less_than_equal", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual <= expected,
+ "assert_less_than_equal", description,
+ "expected a number less than or equal to ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_less_than_equal, "assert_less_than_equal");
+
+ function assert_greater_than_equal(actual, expected, description)
+ {
+ /*
+ * Test if a primitive number is greater than or equal to another
+ */
+ assert(typeof actual === "number",
+ "assert_greater_than_equal", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual >= expected,
+ "assert_greater_than_equal", description,
+ "expected a number greater than or equal to ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_greater_than_equal, "assert_greater_than_equal");
+
+ function assert_between_inclusive(actual, lower, upper, description)
+ {
+ /*
+ * Test if a primitive number is between to two others or equal to either of them
+ */
+ assert(typeof actual === "number",
+ "assert_between_inclusive", description,
+ "expected a number but got a ${type_actual}",
+ {type_actual:typeof actual});
+
+ assert(actual >= lower && actual <= upper,
+ "assert_between_inclusive", description,
+ "expected a number greater than or equal to ${lower} " +
+ "and less than or equal to ${upper} but got ${actual}",
+ {lower:lower, upper:upper, actual:actual});
+ }
+ expose_assert(assert_between_inclusive, "assert_between_inclusive");
+
+ function assert_regexp_match(actual, expected, description) {
+ /*
+ * Test if a string (actual) matches a regexp (expected)
+ */
+ assert(expected.test(actual),
+ "assert_regexp_match", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_regexp_match, "assert_regexp_match");
+
+ function assert_class_string(object, class_string, description) {
+ var actual = {}.toString.call(object);
+ var expected = "[object " + class_string + "]";
+ assert(same_value(actual, expected), "assert_class_string", description,
+ "expected ${expected} but got ${actual}",
+ {expected:expected, actual:actual});
+ }
+ expose_assert(assert_class_string, "assert_class_string");
+
+ function assert_own_property(object, property_name, description) {
+ assert(object.hasOwnProperty(property_name),
+ "assert_own_property", description,
+ "expected property ${p} missing", {p:property_name});
+ }
+ expose_assert(assert_own_property, "assert_own_property");
+
+ function assert_not_own_property(object, property_name, description) {
+ assert(!object.hasOwnProperty(property_name),
+ "assert_not_own_property", description,
+ "unexpected property ${p} is found on object", {p:property_name});
+ }
+ expose_assert(assert_not_own_property, "assert_not_own_property");
+
+ function _assert_inherits(name) {
+ return function (object, property_name, description)
+ {
+ assert(typeof object === "object" || typeof object === "function" ||
+ // Or has [[IsHTMLDDA]] slot
+ String(object) === "[object HTMLAllCollection]",
+ name, description,
+ "provided value is not an object");
+
+ assert("hasOwnProperty" in object,
+ name, description,
+ "provided value is an object but has no hasOwnProperty method");
+
+ assert(!object.hasOwnProperty(property_name),
+ name, description,
+ "property ${p} found on object expected in prototype chain",
+ {p:property_name});
+
+ assert(property_name in object,
+ name, description,
+ "property ${p} not found in prototype chain",
+ {p:property_name});
+ };
+ }
+ expose_assert(_assert_inherits("assert_inherits"), "assert_inherits");
+ expose_assert(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute");
+
+ function assert_readonly(object, property_name, description)
+ {
+ var initial_value = object[property_name];
+ try {
+ //Note that this can have side effects in the case where
+ //the property has PutForwards
+ object[property_name] = initial_value + "a"; //XXX use some other value here?
+ assert(same_value(object[property_name], initial_value),
+ "assert_readonly", description,
+ "changing property ${p} succeeded",
+ {p:property_name});
+ } finally {
+ object[property_name] = initial_value;
+ }
+ }
+ expose_assert(assert_readonly, "assert_readonly");
+
+ /**
+ * Assert a JS Error with the expected constructor is thrown.
+ *
+ * @param {object} constructor The expected exception constructor.
+ * @param {Function} func Function which should throw.
+ * @param {string} description Error description for the case that the error is not thrown.
+ */
+ function assert_throws_js(constructor, func, description)
+ {
+ assert_throws_js_impl(constructor, func, description,
+ "assert_throws_js");
+ }
+ expose_assert(assert_throws_js, "assert_throws_js");
+
+ /**
+ * Like assert_throws_js but allows specifying the assertion type
+ * (assert_throws_js or promise_rejects_js, in practice).
+ */
+ function assert_throws_js_impl(constructor, func, description,
+ assertion_type)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ // Basic sanity-checks on the thrown exception.
+ assert(typeof e === "object",
+ assertion_type, description,
+ "${func} threw ${e} with type ${type}, not an object",
+ {func:func, e:e, type:typeof e});
+
+ assert(e !== null,
+ assertion_type, description,
+ "${func} threw null, not an object",
+ {func:func});
+
+ // Basic sanity-check on the passed-in constructor
+ assert(typeof constructor == "function",
+ assertion_type, description,
+ "${constructor} is not a constructor",
+ {constructor:constructor});
+ var obj = constructor;
+ while (obj) {
+ if (typeof obj === "function" &&
+ obj.name === "Error") {
+ break;
+ }
+ obj = Object.getPrototypeOf(obj);
+ }
+ assert(obj != null,
+ assertion_type, description,
+ "${constructor} is not an Error subtype",
+ {constructor:constructor});
+
+ // And checking that our exception is reasonable
+ assert(e.constructor === constructor &&
+ e.name === constructor.name,
+ assertion_type, description,
+ "${func} threw ${actual} (${actual_name}) expected instance of ${expected} (${expected_name})",
+ {func:func, actual:e, actual_name:e.name,
+ expected:constructor,
+ expected_name:constructor.name});
+ }
+ }
+
+ /**
+ * 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:
+ *
+ * 1) If the DOMException is expected to come from the current global, the
+ * second argument should be the function expected to throw and a third,
+ * optional, argument is the assertion description.
+ *
+ * 2) If the DOMException is expected to come from some other global, the
+ * second argument should be the DOMException constructor from that global,
+ * the third argument the function expected to throw, and the fourth, optional,
+ * argument the assertion description.
+ */
+ function assert_throws_dom(type, funcOrConstructor, descriptionOrFunc, maybeDescription)
+ {
+ let constructor, func, description;
+ if (funcOrConstructor.name === "DOMException") {
+ constructor = funcOrConstructor;
+ func = descriptionOrFunc;
+ description = maybeDescription;
+ } else {
+ constructor = self.DOMException;
+ func = funcOrConstructor;
+ description = descriptionOrFunc;
+ assert(maybeDescription === undefined,
+ "Too many args pased to no-constructor version of assert_throws_dom");
+ }
+ assert_throws_dom_impl(type, func, description, "assert_throws_dom", constructor)
+ }
+ expose_assert(assert_throws_dom, "assert_throws_dom");
+
+ /**
+ * Similar to assert_throws_dom but allows specifying the assertion type
+ * (assert_throws_dom or promise_rejects_dom, in practice). The
+ * "constructor" argument must be the DOMException constructor from the
+ * global we expect the exception to come from.
+ */
+ function assert_throws_dom_impl(type, func, description, assertion_type, constructor)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ // Basic sanity-checks on the thrown exception.
+ assert(typeof e === "object",
+ assertion_type, description,
+ "${func} threw ${e} with type ${type}, not an object",
+ {func:func, e:e, type:typeof e});
+
+ assert(e !== null,
+ assertion_type, description,
+ "${func} threw null, not an object",
+ {func:func});
+
+ // Sanity-check our type
+ assert(typeof type == "number" ||
+ typeof type == "string",
+ assertion_type, description,
+ "${type} is not a number or string",
+ {type:type});
+
+ var codename_name_map = {
+ INDEX_SIZE_ERR: 'IndexSizeError',
+ HIERARCHY_REQUEST_ERR: 'HierarchyRequestError',
+ WRONG_DOCUMENT_ERR: 'WrongDocumentError',
+ INVALID_CHARACTER_ERR: 'InvalidCharacterError',
+ NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError',
+ NOT_FOUND_ERR: 'NotFoundError',
+ NOT_SUPPORTED_ERR: 'NotSupportedError',
+ INUSE_ATTRIBUTE_ERR: 'InUseAttributeError',
+ INVALID_STATE_ERR: 'InvalidStateError',
+ SYNTAX_ERR: 'SyntaxError',
+ INVALID_MODIFICATION_ERR: 'InvalidModificationError',
+ NAMESPACE_ERR: 'NamespaceError',
+ INVALID_ACCESS_ERR: 'InvalidAccessError',
+ TYPE_MISMATCH_ERR: 'TypeMismatchError',
+ SECURITY_ERR: 'SecurityError',
+ NETWORK_ERR: 'NetworkError',
+ ABORT_ERR: 'AbortError',
+ URL_MISMATCH_ERR: 'URLMismatchError',
+ QUOTA_EXCEEDED_ERR: 'QuotaExceededError',
+ TIMEOUT_ERR: 'TimeoutError',
+ INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError',
+ DATA_CLONE_ERR: 'DataCloneError'
+ };
+
+ var name_code_map = {
+ IndexSizeError: 1,
+ HierarchyRequestError: 3,
+ WrongDocumentError: 4,
+ InvalidCharacterError: 5,
+ NoModificationAllowedError: 7,
+ NotFoundError: 8,
+ NotSupportedError: 9,
+ InUseAttributeError: 10,
+ InvalidStateError: 11,
+ SyntaxError: 12,
+ InvalidModificationError: 13,
+ NamespaceError: 14,
+ InvalidAccessError: 15,
+ TypeMismatchError: 17,
+ SecurityError: 18,
+ NetworkError: 19,
+ AbortError: 20,
+ URLMismatchError: 21,
+ QuotaExceededError: 22,
+ TimeoutError: 23,
+ InvalidNodeTypeError: 24,
+ DataCloneError: 25,
+
+ EncodingError: 0,
+ NotReadableError: 0,
+ UnknownError: 0,
+ ConstraintError: 0,
+ DataError: 0,
+ TransactionInactiveError: 0,
+ ReadOnlyError: 0,
+ VersionError: 0,
+ OperationError: 0,
+ NotAllowedError: 0
+ };
+
+ var code_name_map = {};
+ for (var key in name_code_map) {
+ if (name_code_map[key] > 0) {
+ code_name_map[name_code_map[key]] = key;
+ }
+ }
+
+ var required_props = {};
+ var name;
+
+ if (typeof type === "number") {
+ if (type === 0) {
+ throw new AssertionError('Test bug: ambiguous DOMException code 0 passed to assert_throws_dom()');
+ } else if (!(type in code_name_map)) {
+ throw new AssertionError('Test bug: unrecognized DOMException code "' + type + '" passed to assert_throws_dom()');
+ }
+ name = code_name_map[type];
+ required_props.code = type;
+ } else if (typeof type === "string") {
+ name = type in codename_name_map ? codename_name_map[type] : type;
+ if (!(name in name_code_map)) {
+ throw new AssertionError('Test bug: unrecognized DOMException code name or name "' + type + '" passed to assert_throws_dom()');
+ }
+
+ required_props.code = name_code_map[name];
+ }
+
+ if (required_props.code === 0 ||
+ ("name" in e &&
+ e.name !== e.name.toUpperCase() &&
+ e.name !== "DOMException")) {
+ // New style exception: also test the name property.
+ required_props.name = name;
+ }
+
+ for (var prop in required_props) {
+ assert(prop in e && e[prop] == required_props[prop],
+ assertion_type, description,
+ "${func} threw ${e} that is not a DOMException " + type + ": property ${prop} is equal to ${actual}, expected ${expected}",
+ {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]});
+ }
+
+ // Check that the exception is from the right global. This check is last
+ // so more specific, and more informative, checks on the properties can
+ // happen in case a totally incorrect exception is thrown.
+ assert(e.constructor === constructor,
+ assertion_type, description,
+ "${func} threw an exception from the wrong global",
+ {func});
+
+ }
+ }
+
+ /**
+ * Assert the provided value is thrown.
+ *
+ * @param {value} exception The expected exception.
+ * @param {Function} func Function which should throw.
+ * @param {string} description Error description for the case that the error is not thrown.
+ */
+ function assert_throws_exactly(exception, func, description)
+ {
+ assert_throws_exactly_impl(exception, func, description,
+ "assert_throws_exactly");
+ }
+ expose_assert(assert_throws_exactly, "assert_throws_exactly");
+
+ /**
+ * Like assert_throws_exactly but allows specifying the assertion type
+ * (assert_throws_exactly or promise_rejects_exactly, in practice).
+ */
+ function assert_throws_exactly_impl(exception, func, description,
+ assertion_type)
+ {
+ try {
+ func.call(this);
+ assert(false, assertion_type, description,
+ "${func} did not throw", {func:func});
+ } catch (e) {
+ if (e instanceof AssertionError) {
+ throw e;
+ }
+
+ assert(same_value(e, exception), assertion_type, description,
+ "${func} threw ${e} but we expected it to throw ${exception}",
+ {func:func, e:e, exception:exception});
+ }
+ }
+
+ 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)
+ {
+ var args = [].slice.call(arguments, 3);
+ var errors = [];
+ var passed = false;
+ forEach(expected_array,
+ function(expected)
+ {
+ try {
+ assert_func.apply(this, [actual, expected].concat(args));
+ passed = true;
+ } catch (e) {
+ errors.push(e.message);
+ }
+ });
+ if (!passed) {
+ throw new AssertionError(errors.join("\n\n"));
+ }
+ }
+ // FIXME: assert_any cannot use expose_assert, because assert_wrapper does
+ // not support nested assert calls (e.g. to assert_func). We need to
+ // support bypassing assert_wrapper for the inner asserts here.
+ expose(assert_any, "assert_any");
+
+ /**
+ * Assert that a feature is implemented, based on a 'truthy' condition.
+ *
+ * This function should be used to early-exit from tests in which there is
+ * no point continuing without support for a non-optional spec or spec
+ * feature. For example:
+ *
+ * assert_implements(window.Foo, 'Foo is not supported');
+ *
+ * @param {object} condition The truthy value to test
+ * @param {string} description Error description for the case that the condition is not truthy.
+ */
+ function assert_implements(condition, description) {
+ assert(!!condition, "assert_implements", description);
+ }
+ expose_assert(assert_implements, "assert_implements")
+
+ /**
+ * Assert that an optional feature is implemented, based on a 'truthy' condition.
+ *
+ * This function should be used to early-exit from tests in which there is
+ * no point continuing without support for an explicitly optional spec or
+ * spec feature. For example:
+ *
+ * assert_implements_optional(video.canPlayType("video/webm"),
+ * "webm video playback not supported");
+ *
+ * @param {object} condition The truthy value to test
+ * @param {string} description Error description for the case that the condition is not truthy.
+ */
+ function assert_implements_optional(condition, description) {
+ if (!condition) {
+ throw new OptionalFeatureUnsupportedError(description);
+ }
+ }
+ expose_assert(assert_implements_optional, "assert_implements_optional")
+
+ function Test(name, properties)
+ {
+ if (tests.file_is_test && tests.tests.length) {
+ throw new Error("Tried to create a test with file_is_test");
+ }
+ this.name = name;
+
+ this.phase = (tests.is_aborted || tests.phase === tests.phases.COMPLETE) ?
+ this.phases.COMPLETE : this.phases.INITIAL;
+
+ this.status = this.NOTRUN;
+ this.timeout_id = null;
+ this.index = null;
+
+ this.properties = properties || {};
+ this.timeout_length = settings.test_timeout;
+ if (this.timeout_length !== null) {
+ this.timeout_length *= tests.timeout_multiplier;
+ }
+
+ this.message = null;
+ this.stack = null;
+
+ this.steps = [];
+ this._is_promise_test = false;
+
+ this.cleanup_callbacks = [];
+ this._user_defined_cleanup_count = 0;
+ this._done_callbacks = [];
+
+ // Tests declared following harness completion are likely an indication
+ // of a programming error, but they cannot be reported
+ // deterministically.
+ if (tests.phase === tests.phases.COMPLETE) {
+ return;
+ }
+
+ tests.push(this);
+ }
+
+ Test.statuses = {
+ PASS:0,
+ FAIL:1,
+ TIMEOUT:2,
+ NOTRUN:3,
+ PRECONDITION_FAILED:4
+ };
+
+ Test.prototype = merge({}, Test.statuses);
+
+ Test.prototype.phases = {
+ INITIAL:0,
+ STARTED:1,
+ HAS_RESULT:2,
+ CLEANING:3,
+ COMPLETE:4
+ };
+
+ Test.prototype.status_formats = {
+ 0: "Pass",
+ 1: "Fail",
+ 2: "Timeout",
+ 3: "Not Run",
+ 4: "Optional Feature Unsupported",
+ }
+
+ Test.prototype.format_status = function() {
+ return this.status_formats[this.status];
+ }
+
+ Test.prototype.structured_clone = function()
+ {
+ if (!this._structured_clone) {
+ var msg = this.message;
+ msg = msg ? String(msg) : msg;
+ this._structured_clone = merge({
+ name:String(this.name),
+ properties:merge({}, this.properties),
+ phases:merge({}, this.phases)
+ }, Test.statuses);
+ }
+ this._structured_clone.status = this.status;
+ this._structured_clone.message = this.message;
+ this._structured_clone.stack = this.stack;
+ this._structured_clone.index = this.index;
+ this._structured_clone.phase = this.phase;
+ return this._structured_clone;
+ };
+
+ Test.prototype.step = function(func, this_obj)
+ {
+ if (this.phase > this.phases.STARTED) {
+ return;
+ }
+
+ if (settings.debug && this.phase !== this.phases.STARTED) {
+ console.log("TEST START", this.name);
+ }
+ this.phase = this.phases.STARTED;
+ //If we don't get a result before the harness times out that will be a test timeout
+ this.set_status(this.TIMEOUT, "Test timed out");
+
+ tests.started = true;
+ tests.current_test = this;
+ tests.notify_test_state(this);
+
+ if (this.timeout_id === null) {
+ this.set_timeout();
+ }
+
+ this.steps.push(func);
+
+ if (arguments.length === 1) {
+ this_obj = this;
+ }
+
+ if (settings.debug) {
+ console.debug("TEST STEP", this.name);
+ }
+
+ try {
+ return func.apply(this_obj, Array.prototype.slice.call(arguments, 2));
+ } catch (e) {
+ if (this.phase >= this.phases.HAS_RESULT) {
+ return;
+ }
+ var status = e instanceof OptionalFeatureUnsupportedError ? this.PRECONDITION_FAILED : this.FAIL;
+ var message = String((typeof e === "object" && e !== null) ? e.message : e);
+ var stack = e.stack ? e.stack : null;
+
+ this.set_status(status, message, stack);
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ } finally {
+ this.current_test = null;
+ }
+ };
+
+ Test.prototype.step_func = function(func, this_obj)
+ {
+ var test_this = this;
+
+ if (arguments.length === 1) {
+ this_obj = test_this;
+ }
+
+ return function()
+ {
+ return test_this.step.apply(test_this, [func, this_obj].concat(
+ Array.prototype.slice.call(arguments)));
+ };
+ };
+
+ Test.prototype.step_func_done = function(func, this_obj)
+ {
+ var test_this = this;
+
+ if (arguments.length === 1) {
+ this_obj = test_this;
+ }
+
+ return function()
+ {
+ if (func) {
+ test_this.step.apply(test_this, [func, this_obj].concat(
+ Array.prototype.slice.call(arguments)));
+ }
+ test_this.done();
+ };
+ };
+
+ Test.prototype.unreached_func = function(description)
+ {
+ return this.step_func(function() {
+ assert_unreached(description);
+ });
+ };
+
+ Test.prototype.step_timeout = function(f, 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);
+ }), timeout * tests.timeout_multiplier);
+ };
+
+ 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;
+
+ var wait_for_inner = test_this.step_func(() => {
+ if (cond()) {
+ func();
+ } else {
+ if(remaining === 0) {
+ assert(false, "step_wait_func", description,
+ "Timed out waiting on condition");
+ }
+ remaining--;
+ setTimeout(wait_for_inner, interval);
+ }
+ });
+
+ wait_for_inner();
+ };
+
+ 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);
+ }
+
+ 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);
+ });
+ }
+
+ /*
+ * Private method for registering cleanup functions. `testharness.js`
+ * internals should use this method instead of the public `add_cleanup`
+ * method in order to hide implementation details from the harness status
+ * message in the case errors.
+ */
+ Test.prototype._add_cleanup = function(callback) {
+ this.cleanup_callbacks.push(callback);
+ };
+
+ /*
+ * Schedule a function to be run after the test result is known, regardless
+ * of passing or failing state. The behavior of this function will not
+ * influence the result of the test, but if an exception is thrown, the
+ * test harness will report an error.
+ */
+ Test.prototype.add_cleanup = function(callback) {
+ this._user_defined_cleanup_count += 1;
+ this._add_cleanup(callback);
+ };
+
+ Test.prototype.set_timeout = function()
+ {
+ if (this.timeout_length !== null) {
+ var this_obj = this;
+ this.timeout_id = setTimeout(function()
+ {
+ this_obj.timeout();
+ }, this.timeout_length);
+ }
+ };
+
+ Test.prototype.set_status = function(status, message, stack)
+ {
+ this.status = status;
+ this.message = message;
+ this.stack = stack ? stack : null;
+ };
+
+ Test.prototype.timeout = function()
+ {
+ this.timeout_id = null;
+ this.set_status(this.TIMEOUT, "Test timed out");
+ this.phase = this.phases.HAS_RESULT;
+ this.done();
+ };
+
+ Test.prototype.force_timeout = Test.prototype.timeout;
+
+ /**
+ * Update the test status, initiate "cleanup" functions, and signal test
+ * completion.
+ */
+ Test.prototype.done = function()
+ {
+ if (this.phase >= this.phases.CLEANING) {
+ return;
+ }
+
+ if (this.phase <= this.phases.STARTED) {
+ this.set_status(this.PASS, null);
+ }
+
+ if (global_scope.clearTimeout) {
+ clearTimeout(this.timeout_id);
+ }
+
+ if (settings.debug) {
+ console.log("TEST DONE",
+ this.status,
+ this.name,)
+ }
+
+ this.cleanup();
+ };
+
+ function add_test_done_callback(test, callback)
+ {
+ if (test.phase === test.phases.COMPLETE) {
+ callback();
+ return;
+ }
+
+ test._done_callbacks.push(callback);
+ }
+
+ /*
+ * Invoke all specified cleanup functions. If one or more produce an error,
+ * the context is in an unpredictable state, so all further testing should
+ * be cancelled.
+ */
+ Test.prototype.cleanup = function() {
+ var error_count = 0;
+ var bad_value_count = 0;
+ function on_error() {
+ error_count += 1;
+ // Abort tests immediately so that tests declared within subsequent
+ // cleanup functions are not run.
+ tests.abort();
+ }
+ var this_obj = this;
+ var results = [];
+
+ this.phase = this.phases.CLEANING;
+
+ forEach(this.cleanup_callbacks,
+ function(cleanup_callback) {
+ var result;
+
+ try {
+ result = cleanup_callback();
+ } catch (e) {
+ on_error();
+ return;
+ }
+
+ if (!is_valid_cleanup_result(this_obj, result)) {
+ bad_value_count += 1;
+ // Abort tests immediately so that tests declared
+ // within subsequent cleanup functions are not run.
+ tests.abort();
+ }
+
+ results.push(result);
+ });
+
+ if (!this._is_promise_test) {
+ cleanup_done(this_obj, error_count, bad_value_count);
+ } else {
+ all_async(results,
+ function(result, done) {
+ if (result && typeof result.then === "function") {
+ result
+ .then(null, on_error)
+ .then(done);
+ } else {
+ done();
+ }
+ },
+ function() {
+ cleanup_done(this_obj, error_count, bad_value_count);
+ });
+ }
+ };
+
+ /**
+ * Determine if the return value of a cleanup function is valid for a given
+ * test. Any test may return the value `undefined`. Tests created with
+ * `promise_test` may alternatively return "thenable" object values.
+ */
+ function is_valid_cleanup_result(test, result) {
+ if (result === undefined) {
+ return true;
+ }
+
+ if (test._is_promise_test) {
+ return result && typeof result.then === "function";
+ }
+
+ return false;
+ }
+
+ function cleanup_done(test, error_count, bad_value_count) {
+ if (error_count || bad_value_count) {
+ var total = test._user_defined_cleanup_count;
+
+ tests.status.status = tests.status.ERROR;
+ 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 (bad_value_count) {
+ var type = test._is_promise_test ?
+ "non-thenable" : "non-undefined";
+ tests.status.message += ", and " + bad_value_count +
+ " returned a " + type + " value";
+ }
+
+ tests.status.message += ".";
+
+ tests.status.stack = null;
+ }
+
+ test.phase = test.phases.COMPLETE;
+ tests.result(test);
+ forEach(test._done_callbacks,
+ function(callback) {
+ callback();
+ });
+ 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
+ * on the local document. This allows listeners (test result reporting
+ * etc..) to transparently handle local and remote events.
+ */
+ function RemoteTest(clone) {
+ var this_obj = this;
+ Object.keys(clone).forEach(
+ function(key) {
+ this_obj[key] = clone[key];
+ });
+ this.index = null;
+ this.phase = this.phases.INITIAL;
+ this.update_state_from(clone);
+ this._done_callbacks = [];
+ tests.push(this);
+ }
+
+ RemoteTest.prototype.structured_clone = function() {
+ var clone = {};
+ Object.keys(this).forEach(
+ (function(key) {
+ var value = this[key];
+ // `RemoteTest` instances are responsible for managing
+ // their own "done" callback functions, so those functions
+ // are not relevant in other execution contexts. Because of
+ // this (and because Function values cannot be serialized
+ // for cross-realm transmittance), the property should not
+ // be considered when cloning instances.
+ if (key === '_done_callbacks' ) {
+ return;
+ }
+
+ if (typeof value === "object" && value !== null) {
+ clone[key] = merge({}, value);
+ } else {
+ clone[key] = value;
+ }
+ }).bind(this));
+ clone.phases = merge({}, this.phases);
+ return clone;
+ };
+
+ /**
+ * `RemoteTest` instances are objects which represent tests running in
+ * another realm. They do not define "cleanup" functions (if necessary,
+ * such functions are defined on the associated `Test` instance within the
+ * external realm). However, `RemoteTests` may have "done" callbacks (e.g.
+ * as attached by the `Tests` instance responsible for tracking the overall
+ * test status in the parent realm). The `cleanup` method delegates to
+ * `done` in order to ensure that such callbacks are invoked following the
+ * completion of the `RemoteTest`.
+ */
+ RemoteTest.prototype.cleanup = function() {
+ this.done();
+ };
+ RemoteTest.prototype.phases = Test.prototype.phases;
+ RemoteTest.prototype.update_state_from = function(clone) {
+ this.status = clone.status;
+ this.message = clone.message;
+ this.stack = clone.stack;
+ if (this.phase === this.phases.INITIAL) {
+ this.phase = this.phases.STARTED;
+ }
+ };
+ RemoteTest.prototype.done = function() {
+ this.phase = this.phases.COMPLETE;
+
+ forEach(this._done_callbacks,
+ function(callback) {
+ callback();
+ });
+ }
+
+ RemoteTest.prototype.format_status = function() {
+ return Test.prototype.status_formats[this.status];
+ }
+
+ /*
+ * A RemoteContext listens for test events from a remote test context, such
+ * as another window or a worker. These events are then used to construct
+ * and maintain RemoteTest objects that mirror the tests running in the
+ * remote context.
+ *
+ * An optional third parameter can be used as a predicate to filter incoming
+ * MessageEvents.
+ */
+ function RemoteContext(remote, message_target, message_filter) {
+ this.running = true;
+ this.started = false;
+ this.tests = new Array();
+ this.early_exception = null;
+
+ var this_obj = this;
+ // If remote context is cross origin assigning to onerror is not
+ // possible, so silently catch those errors.
+ try {
+ remote.onerror = function(error) { this_obj.remote_error(error); };
+ } catch (e) {
+ // Ignore.
+ }
+
+ // Keeping a reference to the remote object and the message handler until
+ // remote_done() is seen prevents the remote object and its message channel
+ // from going away before all the messages are dispatched.
+ this.remote = remote;
+ this.message_target = message_target;
+ this.message_handler = function(message) {
+ var passesFilter = !message_filter || message_filter(message);
+ // The reference to the `running` property in the following
+ // condition is unnecessary because that value is only set to
+ // `false` after the `message_handler` function has been
+ // unsubscribed.
+ // TODO: Simplify the condition by removing the reference.
+ if (this_obj.running && message.data && passesFilter &&
+ (message.data.type in this_obj.message_handlers)) {
+ this_obj.message_handlers[message.data.type].call(this_obj, message.data);
+ }
+ };
+
+ if (self.Promise) {
+ this.done = new Promise(function(resolve) {
+ this_obj.doneResolve = resolve;
+ });
+ }
+
+ this.message_target.addEventListener("message", this.message_handler);
+ }
+
+ RemoteContext.prototype.remote_error = function(error) {
+ if (error.preventDefault) {
+ error.preventDefault();
+ }
+
+ // Defer interpretation of errors until the testing protocol has
+ // started and the remote test's `allow_uncaught_exception` property
+ // is available.
+ if (!this.started) {
+ this.early_exception = error;
+ } else if (!this.allow_uncaught_exception) {
+ this.report_uncaught(error);
+ }
+ };
+
+ RemoteContext.prototype.report_uncaught = function(error) {
+ var message = error.message || String(error);
+ var filename = (error.filename ? " " + error.filename: "");
+ // FIXME: Display remote error states separately from main document
+ // error state.
+ tests.set_status(tests.status.ERROR,
+ "Error in remote" + filename + ": " + message,
+ error.stack);
+ };
+
+ RemoteContext.prototype.start = function(data) {
+ this.started = true;
+ this.allow_uncaught_exception = data.properties.allow_uncaught_exception;
+
+ if (this.early_exception && !this.allow_uncaught_exception) {
+ this.report_uncaught(this.early_exception);
+ }
+ };
+
+ RemoteContext.prototype.test_state = function(data) {
+ var remote_test = this.tests[data.test.index];
+ if (!remote_test) {
+ remote_test = new RemoteTest(data.test);
+ this.tests[data.test.index] = remote_test;
+ }
+ remote_test.update_state_from(data.test);
+ tests.notify_test_state(remote_test);
+ };
+
+ RemoteContext.prototype.test_done = function(data) {
+ var remote_test = this.tests[data.test.index];
+ remote_test.update_state_from(data.test);
+ remote_test.done();
+ tests.result(remote_test);
+ };
+
+ RemoteContext.prototype.remote_done = function(data) {
+ if (tests.status.status === null &&
+ data.status.status !== data.status.OK) {
+ tests.set_status(data.status.status, data.status.message, data.status.sack);
+ }
+
+ for (let assert of data.asserts) {
+ var record = new AssertRecord();
+ record.assert_name = assert.assert_name;
+ record.args = assert.args;
+ record.test = assert.test != null ? this.tests[assert.test.index] : null;
+ record.status = assert.status;
+ record.stack = assert.stack;
+ tests.asserts_run.push(record);
+ }
+
+ this.message_target.removeEventListener("message", this.message_handler);
+ this.running = false;
+
+ // If remote context is cross origin assigning to onerror is not
+ // possible, so silently catch those errors.
+ try {
+ this.remote.onerror = null;
+ } catch (e) {
+ // Ignore.
+ }
+
+ this.remote = null;
+ this.message_target = null;
+ if (this.doneResolve) {
+ this.doneResolve();
+ }
+
+ if (tests.all_done()) {
+ tests.complete();
+ }
+ };
+
+ RemoteContext.prototype.message_handlers = {
+ start: RemoteContext.prototype.start,
+ test_state: RemoteContext.prototype.test_state,
+ result: RemoteContext.prototype.test_done,
+ complete: RemoteContext.prototype.remote_done
+ };
+
+ /*
+ * Harness
+ */
+
+ function TestsStatus()
+ {
+ this.status = null;
+ this.message = null;
+ this.stack = null;
+ }
+
+ TestsStatus.statuses = {
+ OK:0,
+ ERROR:1,
+ TIMEOUT:2,
+ PRECONDITION_FAILED:3
+ };
+
+ TestsStatus.prototype = merge({}, TestsStatus.statuses);
+
+ TestsStatus.prototype.formats = {
+ 0: "OK",
+ 1: "Error",
+ 2: "Timeout",
+ 3: "Optional Feature Unsupported"
+ }
+
+
+ TestsStatus.prototype.structured_clone = function()
+ {
+ if (!this._structured_clone) {
+ var msg = this.message;
+ msg = msg ? String(msg) : msg;
+ this._structured_clone = merge({
+ status:this.status,
+ message:msg,
+ stack:this.stack
+ }, TestsStatus.statuses);
+ }
+ return this._structured_clone;
+ };
+
+ TestsStatus.prototype.format_status = function() {
+ return this.formats[this.status];
+ }
+
+ function AssertRecord(test, assert_name, ...args) {
+ this.assert_name = assert_name;
+ this.test = test;
+ // Avoid keeping complex objects alive
+ this.args = args.map(x => format_value(x).replace(/\n/g, " "));
+ this.status = null;
+ }
+
+ AssertRecord.prototype.structured_clone = function() {
+ return {
+ assert_name: this.assert_name,
+ test: this.test ? this.test.structured_clone() : null,
+ args: this.args,
+ status: this.status,
+ }
+ }
+
+ function Tests()
+ {
+ this.tests = [];
+ this.num_pending = 0;
+
+ this.phases = {
+ INITIAL:0,
+ SETUP:1,
+ HAVE_TESTS:2,
+ HAVE_RESULTS:3,
+ COMPLETE:4
+ };
+ this.phase = this.phases.INITIAL;
+
+ this.properties = {};
+
+ this.wait_for_finish = false;
+ this.processing_callbacks = false;
+
+ this.allow_uncaught_exception = false;
+
+ this.file_is_test = false;
+ // This value is lazily initialized in order to avoid introducing a
+ // dependency on ECMAScript 2015 Promises to all tests.
+ this.promise_tests = null;
+ this.promise_setup_called = false;
+
+ this.timeout_multiplier = 1;
+ this.timeout_length = test_environment.test_timeout();
+ this.timeout_id = null;
+
+ this.start_callbacks = [];
+ this.test_state_callbacks = [];
+ this.test_done_callbacks = [];
+ this.all_done_callbacks = [];
+
+ this.hide_test_state = false;
+ this.pending_remotes = [];
+
+ this.current_test = null;
+ this.asserts_run = [];
+
+ // Track whether output is enabled, and thus whether or not we should
+ // track asserts.
+ //
+ // On workers we don't get properties set from testharnessreport.js, so
+ // we don't know whether or not to track asserts. To avoid the
+ // resulting performance hit, we assume we are not meant to. This means
+ // that assert tracking does not function on workers.
+ this.output = settings.output && 'document' in global_scope;
+
+ this.status = new TestsStatus();
+
+ var this_obj = this;
+
+ test_environment.add_on_loaded_callback(function() {
+ if (this_obj.all_done()) {
+ this_obj.complete();
+ }
+ });
+
+ this.set_timeout();
+ }
+
+ Tests.prototype.setup = function(func, properties)
+ {
+ if (this.phase >= this.phases.HAVE_RESULTS) {
+ return;
+ }
+
+ if (this.phase < this.phases.SETUP) {
+ this.phase = this.phases.SETUP;
+ }
+
+ this.properties = properties;
+
+ for (var p in properties) {
+ if (properties.hasOwnProperty(p)) {
+ var value = properties[p];
+ if (p == "allow_uncaught_exception") {
+ this.allow_uncaught_exception = value;
+ } else if (p == "explicit_done" && value) {
+ this.wait_for_finish = true;
+ } else if (p == "explicit_timeout" && value) {
+ this.timeout_length = null;
+ if (this.timeout_id)
+ {
+ clearTimeout(this.timeout_id);
+ }
+ } else if (p == "single_test" && value) {
+ this.set_file_is_test();
+ } else if (p == "timeout_multiplier") {
+ this.timeout_multiplier = value;
+ if (this.timeout_length) {
+ this.timeout_length *= this.timeout_multiplier;
+ }
+ } else if (p == "hide_test_state") {
+ this.hide_test_state = value;
+ } else if (p == "output") {
+ this.output = value;
+ } else if (p === "debug") {
+ settings.debug = value;
+ }
+ }
+ }
+
+ if (func) {
+ try {
+ func();
+ } catch (e) {
+ this.status.status = e instanceof OptionalFeatureUnsupportedError ? this.status.PRECONDITION_FAILED : this.status.ERROR;
+ this.status.message = String(e);
+ this.status.stack = e.stack ? e.stack : null;
+ this.complete();
+ }
+ }
+ this.set_timeout();
+ };
+
+ Tests.prototype.set_file_is_test = function() {
+ if (this.tests.length > 0) {
+ throw new Error("Tried to set file as test after creating a test");
+ }
+ this.wait_for_finish = true;
+ this.file_is_test = true;
+ // Create the test, which will add it to the list of tests
+ tests.current_test = async_test();
+ };
+
+ Tests.prototype.set_status = function(status, message, stack)
+ {
+ this.status.status = status;
+ this.status.message = message;
+ this.status.stack = stack ? stack : null;
+ };
+
+ Tests.prototype.set_timeout = function() {
+ if (global_scope.clearTimeout) {
+ var this_obj = this;
+ clearTimeout(this.timeout_id);
+ if (this.timeout_length !== null) {
+ this.timeout_id = setTimeout(function() {
+ this_obj.timeout();
+ }, this.timeout_length);
+ }
+ }
+ };
+
+ Tests.prototype.timeout = function() {
+ var test_in_cleanup = null;
+
+ if (this.status.status === null) {
+ forEach(this.tests,
+ function(test) {
+ // No more than one test is expected to be in the
+ // "CLEANUP" phase at any time
+ if (test.phase === test.phases.CLEANING) {
+ test_in_cleanup = test;
+ }
+
+ test.phase = test.phases.COMPLETE;
+ });
+
+ // Timeouts that occur while a test is in the "cleanup" phase
+ // indicate that some global state was not properly reverted. This
+ // invalidates the overall test execution, so the timeout should be
+ // reported as an error and cancel the execution of any remaining
+ // tests.
+ if (test_in_cleanup) {
+ this.status.status = this.status.ERROR;
+ this.status.message = "Timeout while running cleanup for " +
+ "test named \"" + test_in_cleanup.name + "\".";
+ tests.status.stack = null;
+ } else {
+ this.status.status = this.status.TIMEOUT;
+ }
+ }
+
+ this.complete();
+ };
+
+ Tests.prototype.end_wait = function()
+ {
+ this.wait_for_finish = false;
+ if (this.all_done()) {
+ this.complete();
+ }
+ };
+
+ Tests.prototype.push = function(test)
+ {
+ if (this.phase < this.phases.HAVE_TESTS) {
+ this.start();
+ }
+ this.num_pending++;
+ test.index = this.tests.push(test);
+ this.notify_test_state(test);
+ };
+
+ Tests.prototype.notify_test_state = function(test) {
+ var this_obj = this;
+ forEach(this.test_state_callbacks,
+ function(callback) {
+ callback(test, this_obj);
+ });
+ };
+
+ Tests.prototype.all_done = function() {
+ return this.tests.length > 0 && test_environment.all_loaded &&
+ (this.num_pending === 0 || this.is_aborted) && !this.wait_for_finish &&
+ !this.processing_callbacks &&
+ !this.pending_remotes.some(function(w) { return w.running; });
+ };
+
+ Tests.prototype.start = function() {
+ this.phase = this.phases.HAVE_TESTS;
+ this.notify_start();
+ };
+
+ Tests.prototype.notify_start = function() {
+ var this_obj = this;
+ forEach (this.start_callbacks,
+ function(callback)
+ {
+ callback(this_obj.properties);
+ });
+ };
+
+ Tests.prototype.result = function(test)
+ {
+ // If the harness has already transitioned beyond the `HAVE_RESULTS`
+ // phase, subsequent tests should not cause it to revert.
+ if (this.phase <= this.phases.HAVE_RESULTS) {
+ this.phase = this.phases.HAVE_RESULTS;
+ }
+ this.num_pending--;
+ this.notify_result(test);
+ };
+
+ Tests.prototype.notify_result = function(test) {
+ var this_obj = this;
+ this.processing_callbacks = true;
+ forEach(this.test_done_callbacks,
+ function(callback)
+ {
+ callback(test, this_obj);
+ });
+ this.processing_callbacks = false;
+ if (this_obj.all_done()) {
+ this_obj.complete();
+ }
+ };
+
+ Tests.prototype.complete = function() {
+ if (this.phase === this.phases.COMPLETE) {
+ return;
+ }
+ var this_obj = this;
+ var all_complete = function() {
+ this_obj.phase = this_obj.phases.COMPLETE;
+ this_obj.notify_complete();
+ };
+ var incomplete = filter(this.tests,
+ function(test) {
+ return test.phase < test.phases.COMPLETE;
+ });
+
+ /**
+ * To preserve legacy behavior, overall test completion must be
+ * signaled synchronously.
+ */
+ if (incomplete.length === 0) {
+ all_complete();
+ return;
+ }
+
+ all_async(incomplete,
+ function(test, testDone)
+ {
+ if (test.phase === test.phases.INITIAL) {
+ test.phase = test.phases.COMPLETE;
+ testDone();
+ } else {
+ add_test_done_callback(test, testDone);
+ test.cleanup();
+ }
+ },
+ all_complete);
+ };
+
+ Tests.prototype.set_assert = function(assert_name, ...args) {
+ this.asserts_run.push(new AssertRecord(this.current_test, assert_name, ...args))
+ }
+
+ Tests.prototype.set_assert_status = function(status, stack) {
+ let assert_record = this.asserts_run[this.asserts_run.length - 1];
+ assert_record.status = status;
+ assert_record.stack = stack;
+ }
+
+ /**
+ * Update the harness status to reflect an unrecoverable harness error that
+ * should cancel all further testing. Update all previously-defined tests
+ * which have not yet started to indicate that they will not be executed.
+ */
+ Tests.prototype.abort = function() {
+ this.status.status = this.status.ERROR;
+ this.is_aborted = true;
+
+ forEach(this.tests,
+ function(test) {
+ if (test.phase === test.phases.INITIAL) {
+ test.phase = test.phases.COMPLETE;
+ }
+ });
+ };
+
+ /*
+ * Determine if any tests share the same `name` property. Return an array
+ * containing the names of any such duplicates.
+ */
+ Tests.prototype.find_duplicates = function() {
+ var names = Object.create(null);
+ var duplicates = [];
+
+ forEach (this.tests,
+ function(test)
+ {
+ if (test.name in names && duplicates.indexOf(test.name) === -1) {
+ duplicates.push(test.name);
+ }
+ names[test.name] = true;
+ });
+
+ return duplicates;
+ };
+
+ function code_unit_str(char) {
+ return 'U+' + char.charCodeAt(0).toString(16);
+ }
+
+ function sanitize_unpaired_surrogates(str) {
+ return str.replace(
+ /([\ud800-\udbff]+)(?![\udc00-\udfff])|(^|[^\ud800-\udbff])([\udc00-\udfff]+)/g,
+ function(_, low, prefix, high) {
+ var output = prefix || ""; // prefix may be undefined
+ var string = low || high; // only one of these alternates can match
+ for (var i = 0; i < string.length; i++) {
+ output += code_unit_str(string[i]);
+ }
+ return output;
+ });
+ }
+
+ function sanitize_all_unpaired_surrogates(tests) {
+ forEach (tests,
+ function (test)
+ {
+ var sanitized = sanitize_unpaired_surrogates(test.name);
+
+ if (test.name !== sanitized) {
+ test.name = sanitized;
+ delete test._structured_clone;
+ }
+ });
+ }
+
+ Tests.prototype.notify_complete = function() {
+ var this_obj = this;
+ var duplicates;
+
+ if (this.status.status === null) {
+ duplicates = this.find_duplicates();
+
+ // Some transports adhere to UTF-8's restriction on unpaired
+ // surrogates. Sanitize the titles so that the results can be
+ // consistently sent via all transports.
+ sanitize_all_unpaired_surrogates(this.tests);
+
+ // Test names are presumed to be unique within test files--this
+ // allows consumers to use them for identification purposes.
+ // Duplicated names violate this expectation and should therefore
+ // be reported as an error.
+ if (duplicates.length) {
+ this.status.status = this.status.ERROR;
+ this.status.message =
+ duplicates.length + ' duplicate test name' +
+ (duplicates.length > 1 ? 's' : '') + ': "' +
+ duplicates.join('", "') + '"';
+ } else {
+ this.status.status = this.status.OK;
+ }
+ }
+
+ forEach (this.all_done_callbacks,
+ function(callback)
+ {
+ callback(this_obj.tests, this_obj.status, this_obj.asserts_run);
+ });
+ };
+
+ /*
+ * Constructs a RemoteContext that tracks tests from a specific worker.
+ */
+ Tests.prototype.create_remote_worker = function(worker) {
+ var message_port;
+
+ if (is_service_worker(worker)) {
+ message_port = navigator.serviceWorker;
+ worker.postMessage({type: "connect"});
+ } else if (is_shared_worker(worker)) {
+ message_port = worker.port;
+ message_port.start();
+ } else {
+ message_port = worker;
+ }
+
+ return new RemoteContext(worker, message_port);
+ };
+
+ /*
+ * Constructs a RemoteContext that tracks tests from a specific window.
+ */
+ Tests.prototype.create_remote_window = function(remote) {
+ remote.postMessage({type: "getmessages"}, "*");
+ return new RemoteContext(
+ remote,
+ window,
+ function(msg) {
+ return msg.source === remote;
+ }
+ );
+ };
+
+ Tests.prototype.fetch_tests_from_worker = function(worker) {
+ if (this.phase >= this.phases.COMPLETE) {
+ return;
+ }
+
+ var remoteContext = this.create_remote_worker(worker);
+ this.pending_remotes.push(remoteContext);
+ return remoteContext.done;
+ };
+
+ function fetch_tests_from_worker(port) {
+ return tests.fetch_tests_from_worker(port);
+ }
+ expose(fetch_tests_from_worker, 'fetch_tests_from_worker');
+
+ Tests.prototype.fetch_tests_from_window = function(remote) {
+ if (this.phase >= this.phases.COMPLETE) {
+ return;
+ }
+
+ this.pending_remotes.push(this.create_remote_window(remote));
+ };
+
+ function fetch_tests_from_window(window) {
+ tests.fetch_tests_from_window(window);
+ }
+ expose(fetch_tests_from_window, 'fetch_tests_from_window');
+
+ function timeout() {
+ if (tests.timeout_length === null) {
+ tests.timeout();
+ }
+ }
+ expose(timeout, 'timeout');
+
+ function add_start_callback(callback) {
+ tests.start_callbacks.push(callback);
+ }
+
+ function add_test_state_callback(callback) {
+ tests.test_state_callbacks.push(callback);
+ }
+
+ function add_result_callback(callback) {
+ tests.test_done_callbacks.push(callback);
+ }
+
+ function add_completion_callback(callback) {
+ tests.all_done_callbacks.push(callback);
+ }
+
+ expose(add_start_callback, 'add_start_callback');
+ expose(add_test_state_callback, 'add_test_state_callback');
+ expose(add_result_callback, 'add_result_callback');
+ expose(add_completion_callback, 'add_completion_callback');
+
+ function remove(array, item) {
+ var index = array.indexOf(item);
+ if (index > -1) {
+ array.splice(index, 1);
+ }
+ }
+
+ function remove_start_callback(callback) {
+ remove(tests.start_callbacks, callback);
+ }
+
+ function remove_test_state_callback(callback) {
+ remove(tests.test_state_callbacks, callback);
+ }
+
+ function remove_result_callback(callback) {
+ remove(tests.test_done_callbacks, callback);
+ }
+
+ function remove_completion_callback(callback) {
+ remove(tests.all_done_callbacks, callback);
+ }
+
+ /*
+ * Output listener
+ */
+
+ function Output() {
+ this.output_document = document;
+ this.output_node = null;
+ this.enabled = settings.output;
+ this.phase = this.INITIAL;
+ }
+
+ Output.prototype.INITIAL = 0;
+ Output.prototype.STARTED = 1;
+ Output.prototype.HAVE_RESULTS = 2;
+ Output.prototype.COMPLETE = 3;
+
+ Output.prototype.setup = function(properties) {
+ if (this.phase > this.INITIAL) {
+ return;
+ }
+
+ //If output is disabled in testharnessreport.js the test shouldn't be
+ //able to override that
+ this.enabled = this.enabled && (properties.hasOwnProperty("output") ?
+ properties.output : settings.output);
+ };
+
+ Output.prototype.init = function(properties) {
+ if (this.phase >= this.STARTED) {
+ return;
+ }
+ if (properties.output_document) {
+ this.output_document = properties.output_document;
+ } else {
+ this.output_document = document;
+ }
+ this.phase = this.STARTED;
+ };
+
+ Output.prototype.resolve_log = function() {
+ var output_document;
+ if (this.output_node) {
+ return;
+ }
+ if (typeof this.output_document === "function") {
+ output_document = this.output_document.apply(undefined);
+ } else {
+ output_document = this.output_document;
+ }
+ if (!output_document) {
+ return;
+ }
+ var node = output_document.getElementById("log");
+ if (!node) {
+ if (output_document.readyState === "loading") {
+ return;
+ }
+ node = output_document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ node.id = "log";
+ if (output_document.body) {
+ output_document.body.appendChild(node);
+ } else {
+ var root = output_document.documentElement;
+ var is_html = (root &&
+ root.namespaceURI == "http://www.w3.org/1999/xhtml" &&
+ root.localName == "html");
+ var is_svg = (output_document.defaultView &&
+ "SVGSVGElement" in output_document.defaultView &&
+ root instanceof output_document.defaultView.SVGSVGElement);
+ if (is_svg) {
+ var foreignObject = output_document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
+ foreignObject.setAttribute("width", "100%");
+ foreignObject.setAttribute("height", "100%");
+ root.appendChild(foreignObject);
+ foreignObject.appendChild(node);
+ } else if (is_html) {
+ root.appendChild(output_document.createElementNS("http://www.w3.org/1999/xhtml", "body"))
+ .appendChild(node);
+ } else {
+ root.appendChild(node);
+ }
+ }
+ }
+ this.output_document = output_document;
+ this.output_node = node;
+ };
+
+ Output.prototype.show_status = function() {
+ if (this.phase < this.STARTED) {
+ this.init();
+ }
+ if (!this.enabled || this.phase === this.COMPLETE) {
+ return;
+ }
+ this.resolve_log();
+ if (this.phase < this.HAVE_RESULTS) {
+ this.phase = this.HAVE_RESULTS;
+ }
+ var done_count = tests.tests.length - tests.num_pending;
+ if (this.output_node && !tests.hide_test_state) {
+ if (done_count < 100 ||
+ (done_count < 1000 && done_count % 100 === 0) ||
+ done_count % 1000 === 0) {
+ this.output_node.textContent = "Running, " +
+ done_count + " complete, " +
+ tests.num_pending + " remain";
+ }
+ }
+ };
+
+ Output.prototype.show_results = function (tests, harness_status, asserts_run) {
+ if (this.phase >= this.COMPLETE) {
+ return;
+ }
+ if (!this.enabled) {
+ return;
+ }
+ if (!this.output_node) {
+ this.resolve_log();
+ }
+ this.phase = this.COMPLETE;
+
+ var log = this.output_node;
+ if (!log) {
+ return;
+ }
+ var output_document = this.output_document;
+
+ while (log.lastChild) {
+ log.removeChild(log.lastChild);
+ }
+
+ var stylesheet = output_document.createElementNS(xhtml_ns, "style");
+ stylesheet.textContent = stylesheetContent;
+ var heads = output_document.getElementsByTagName("head");
+ if (heads.length) {
+ heads[0].appendChild(stylesheet);
+ }
+
+ var status_number = {};
+ forEach(tests,
+ function(test) {
+ var status = test.format_status();
+ if (status_number.hasOwnProperty(status)) {
+ status_number[status] += 1;
+ } else {
+ status_number[status] = 1;
+ }
+ });
+
+ function status_class(status)
+ {
+ return status.replace(/\s/g, '').toLowerCase();
+ }
+
+ var summary_template = ["section", {"id":"summary"},
+ ["h2", {}, "Summary"],
+ function()
+ {
+ var status = harness_status.format_status();
+ var rv = [["section", {},
+ ["p", {},
+ "Harness status: ",
+ ["span", {"class":status_class(status)},
+ status
+ ],
+ ]
+ ]];
+
+ if (harness_status.status === harness_status.ERROR) {
+ rv[0].push(["pre", {}, harness_status.message]);
+ if (harness_status.stack) {
+ rv[0].push(["pre", {}, harness_status.stack]);
+ }
+ }
+ return rv;
+ },
+ ["p", {}, "Found ${num_tests} tests"],
+ function() {
+ var rv = [["div", {}]];
+ var i = 0;
+ while (Test.prototype.status_formats.hasOwnProperty(i)) {
+ if (status_number.hasOwnProperty(Test.prototype.status_formats[i])) {
+ var status = Test.prototype.status_formats[i];
+ rv[0].push(["div", {},
+ ["label", {},
+ ["input", {type:"checkbox", checked:"checked"}],
+ status_number[status] + " ",
+ ["span", {"class":status_class(status)}, status]]]);
+ }
+ i++;
+ }
+ return rv;
+ },
+ ];
+
+ log.appendChild(render(summary_template, {num_tests:tests.length}, output_document));
+
+ forEach(output_document.querySelectorAll("section#summary label"),
+ function(element)
+ {
+ on_event(element, "click",
+ function(e)
+ {
+ if (output_document.getElementById("results") === null) {
+ e.preventDefault();
+ return;
+ }
+ var result_class = element.parentNode.getAttribute("class");
+ var style_element = output_document.querySelector("style#hide-" + result_class);
+ var input_element = element.querySelector("input");
+ if (!style_element && !input_element.checked) {
+ style_element = output_document.createElementNS(xhtml_ns, "style");
+ style_element.id = "hide-" + result_class;
+ style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}";
+ output_document.body.appendChild(style_element);
+ } else if (style_element && input_element.checked) {
+ style_element.parentNode.removeChild(style_element);
+ }
+ });
+ });
+
+ // This use of innerHTML plus manual escaping is not recommended in
+ // general, but is necessary here for performance. Using textContent
+ // on each individual adds tens of seconds of execution time for
+ // large test suites (tens of thousands of tests).
+ function escape_html(s)
+ {
+ return s.replace(/\&/g, "&")
+ .replace(/ {
+ if (!asserts_run_by_test.has(assert.test)) {
+ asserts_run_by_test.set(assert.test, []);
+ }
+ asserts_run_by_test.get(assert.test).push(assert);
+ });
+
+ function get_asserts_output(test) {
+ var asserts = asserts_run_by_test.get(test);
+ if (!asserts) {
+ return "No asserts ran";
+ }
+ rv = "";
+ rv += asserts.map(assert => {
+ var output_fn = "" + escape_html(assert.assert_name) + "(";
+ var prefix_len = output_fn.length;
+ var output_args = assert.args;
+ var output_len = output_args.reduce((prev, current) => prev+current, prefix_len);
+ if (output_len[output_len.length - 1] > 50) {
+ output_args = output_args.map((x, i) =>
+ (i > 0 ? " ".repeat(prefix_len) : "" )+ x + (i < output_args.length - 1 ? ",\n" : ""));
+ } else {
+ output_args = output_args.map((x, i) => x + (i < output_args.length - 1 ? ", " : ""));
+ }
+ output_fn += escape_html(output_args.join(""));
+ output_fn += ')';
+ var output_location;
+ if (assert.stack) {
+ output_location = assert.stack.split("\n", 1)[0].replace(/@?\w+:\/\/[^ "\/]+(?::\d+)?/g, " ");
+ }
+ return "" +
+ Test.prototype.status_formats[assert.status] + " | " +
+ "" +
+ output_fn +
+ (output_location ? "\n" + escape_html(output_location) : "") +
+ " | ";
+ }
+ ).join("\n");
+ rv += " ";
+ return rv;
+ }
+
+ log.appendChild(document.createElementNS(xhtml_ns, "section"));
+ var assertions = has_assertions();
+ var html = "Details" +
+ "Result | Test Name | " +
+ (assertions ? "Assertion | " : "") +
+ "Message | " +
+ "";
+ for (var i = 0; i < tests.length; i++) {
+ var test = tests[i];
+ html += '' +
+ test.format_status() +
+ " | " +
+ escape_html(test.name) +
+ " | " +
+ (assertions ? escape_html(get_assertion(test)) + " | " : "") +
+ escape_html(test.message ? tests[i].message : " ") +
+ (tests[i].stack ? "" +
+ escape_html(tests[i].stack) +
+ " ": "");
+ if (!(test instanceof RemoteTest)) {
+ html += "Asserts run" + get_asserts_output(test) + " "
+ }
+ html += " | ";
+ }
+ html += " ";
+ try {
+ log.lastChild.innerHTML = html;
+ } catch (e) {
+ log.appendChild(document.createElementNS(xhtml_ns, "p"))
+ .textContent = "Setting innerHTML for the log threw an exception.";
+ log.appendChild(document.createElementNS(xhtml_ns, "pre"))
+ .textContent = html;
+ }
+ };
+
+ /*
+ * Template code
+ *
+ * A template is just a JavaScript structure. An element is represented as:
+ *
+ * [tag_name, {attr_name:attr_value}, child1, child2]
+ *
+ * the children can either be strings (which act like text nodes), other templates or
+ * functions (see below)
+ *
+ * A text node is represented as
+ *
+ * ["{text}", value]
+ *
+ * String values have a simple substitution syntax; ${foo} represents a variable foo.
+ *
+ * It is possible to embed logic in templates by using a function in a place where a
+ * node would usually go. The function must either return part of a template or null.
+ *
+ * In cases where a set of nodes are required as output rather than a single node
+ * with children it is possible to just use a list
+ * [node1, node2, node3]
+ *
+ * Usage:
+ *
+ * render(template, substitutions) - take a template and an object mapping
+ * variable names to parameters and return either a DOM node or a list of DOM nodes
+ *
+ * substitute(template, substitutions) - take a template and variable mapping object,
+ * make the variable substitutions and return the substituted template
+ *
+ */
+
+ function is_single_node(template)
+ {
+ return typeof template[0] === "string";
+ }
+
+ function substitute(template, substitutions)
+ {
+ if (typeof template === "function") {
+ var replacement = template(substitutions);
+ if (!replacement) {
+ return null;
+ }
+
+ return substitute(replacement, substitutions);
+ }
+
+ if (is_single_node(template)) {
+ return substitute_single(template, substitutions);
+ }
+
+ return filter(map(template, function(x) {
+ return substitute(x, substitutions);
+ }), function(x) {return x !== null;});
+ }
+
+ function substitute_single(template, substitutions)
+ {
+ var substitution_re = /\$\{([^ }]*)\}/g;
+
+ function do_substitution(input) {
+ var components = input.split(substitution_re);
+ var rv = [];
+ for (var i = 0; i < components.length; i += 2) {
+ rv.push(components[i]);
+ if (components[i + 1]) {
+ rv.push(String(substitutions[components[i + 1]]));
+ }
+ }
+ return rv;
+ }
+
+ function substitute_attrs(attrs, rv)
+ {
+ rv[1] = {};
+ for (var name in template[1]) {
+ if (attrs.hasOwnProperty(name)) {
+ var new_name = do_substitution(name).join("");
+ var new_value = do_substitution(attrs[name]).join("");
+ rv[1][new_name] = new_value;
+ }
+ }
+ }
+
+ function substitute_children(children, rv)
+ {
+ for (var i = 0; i < children.length; i++) {
+ if (children[i] instanceof Object) {
+ var replacement = substitute(children[i], substitutions);
+ if (replacement !== null) {
+ if (is_single_node(replacement)) {
+ rv.push(replacement);
+ } else {
+ extend(rv, replacement);
+ }
+ }
+ } else {
+ extend(rv, do_substitution(String(children[i])));
+ }
+ }
+ return rv;
+ }
+
+ var rv = [];
+ rv.push(do_substitution(String(template[0])).join(""));
+
+ if (template[0] === "{text}") {
+ substitute_children(template.slice(1), rv);
+ } else {
+ substitute_attrs(template[1], rv);
+ substitute_children(template.slice(2), rv);
+ }
+
+ return rv;
+ }
+
+ function make_dom_single(template, doc)
+ {
+ var output_document = doc || document;
+ var element;
+ if (template[0] === "{text}") {
+ element = output_document.createTextNode("");
+ for (var i = 1; i < template.length; i++) {
+ element.data += template[i];
+ }
+ } else {
+ element = output_document.createElementNS(xhtml_ns, template[0]);
+ for (var name in template[1]) {
+ if (template[1].hasOwnProperty(name)) {
+ element.setAttribute(name, template[1][name]);
+ }
+ }
+ for (var i = 2; i < template.length; i++) {
+ if (template[i] instanceof Object) {
+ var sub_element = make_dom(template[i]);
+ element.appendChild(sub_element);
+ } else {
+ var text_node = output_document.createTextNode(template[i]);
+ element.appendChild(text_node);
+ }
+ }
+ }
+
+ return element;
+ }
+
+ function make_dom(template, substitutions, output_document)
+ {
+ if (is_single_node(template)) {
+ return make_dom_single(template, output_document);
+ }
+
+ return map(template, function(x) {
+ return make_dom_single(x, output_document);
+ });
+ }
+
+ function render(template, substitutions, output_document)
+ {
+ return make_dom(substitute(template, substitutions), output_document);
+ }
+
+ /*
+ * Utility functions
+ */
+ function assert(expected_true, function_name, description, error, substitutions)
+ {
+ if (expected_true !== true) {
+ var msg = make_message(function_name, description,
+ error, substitutions);
+ throw new AssertionError(msg);
+ }
+ }
+
+ function AssertionError(message)
+ {
+ if (typeof message == "string") {
+ message = sanitize_unpaired_surrogates(message);
+ }
+ this.message = message;
+ this.stack = get_stack();
+ }
+ expose(AssertionError, "AssertionError");
+
+ AssertionError.prototype = Object.create(Error.prototype);
+
+ const get_stack = function() {
+ var stack = new Error().stack;
+ // 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) {
+ return "(Stack trace unavailable)";
+ }
+
+ var lines = stack.split("\n");
+
+ // Create a pattern to match stack frames originating within testharness.js. These include the
+ // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21').
+ // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript
+ // in case it contains RegExp characters.
+ var script_url = get_script_url();
+ var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js";
+ var re = new RegExp(re_text + ":\\d+:\\d+");
+
+ // Some browsers include a preamble that specifies the type of the error object. Skip this by
+ // advancing until we find the first stack frame originating from testharness.js.
+ var i = 0;
+ while (!re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Then skip the top frames originating from testharness.js to begin the stack at the test code.
+ while (re.test(lines[i]) && i < lines.length) {
+ i++;
+ }
+
+ // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified.
+ if (i >= lines.length) {
+ return stack;
+ }
+
+ return lines.slice(i).join("\n");
+ }
+
+ function OptionalFeatureUnsupportedError(message)
+ {
+ AssertionError.call(this, message);
+ }
+ OptionalFeatureUnsupportedError.prototype = Object.create(AssertionError.prototype);
+ expose(OptionalFeatureUnsupportedError, "OptionalFeatureUnsupportedError");
+
+ function make_message(function_name, description, error, substitutions)
+ {
+ for (var p in substitutions) {
+ if (substitutions.hasOwnProperty(p)) {
+ substitutions[p] = format_value(substitutions[p]);
+ }
+ }
+ var node_form = substitute(["{text}", "${function_name}: ${description}" + error],
+ merge({function_name:function_name,
+ description:(description?description + " ":"")},
+ substitutions));
+ return node_form.slice(1).join("");
+ }
+
+ function filter(array, callable, thisObj) {
+ var rv = [];
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ var pass = callable.call(thisObj, array[i], i, array);
+ if (pass) {
+ rv.push(array[i]);
+ }
+ }
+ }
+ return rv;
+ }
+
+ function map(array, callable, thisObj)
+ {
+ var rv = [];
+ rv.length = array.length;
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ rv[i] = callable.call(thisObj, array[i], i, array);
+ }
+ }
+ return rv;
+ }
+
+ function extend(array, items)
+ {
+ Array.prototype.push.apply(array, items);
+ }
+
+ function forEach(array, callback, thisObj)
+ {
+ for (var i = 0; i < array.length; i++) {
+ if (array.hasOwnProperty(i)) {
+ callback.call(thisObj, array[i], i, array);
+ }
+ }
+ }
+
+ /**
+ * Immediately invoke a "iteratee" function with a series of values in
+ * parallel and invoke a final "done" function when all of the "iteratee"
+ * invocations have signaled completion.
+ *
+ * If all callbacks complete synchronously (or if no callbacks are
+ * specified), the `done_callback` will be invoked synchronously. It is the
+ * responsibility of the caller to ensure asynchronicity in cases where
+ * that is desired.
+ *
+ * @param {array} value Zero or more values to use in the invocation of
+ * `iter_callback`
+ * @param {function} iter_callback A function that will be invoked once for
+ * each of the provided `values`. Two
+ * arguments will be available in each
+ * invocation: the value from `values` and
+ * a function that must be invoked to
+ * signal completion
+ * @param {function} done_callback A function that will be invoked after
+ * all operations initiated by the
+ * `iter_callback` function have signaled
+ * completion
+ */
+ function all_async(values, iter_callback, done_callback)
+ {
+ var remaining = values.length;
+
+ if (remaining === 0) {
+ done_callback();
+ }
+
+ forEach(values,
+ function(element) {
+ var invoked = false;
+ var elDone = function() {
+ if (invoked) {
+ return;
+ }
+
+ invoked = true;
+ remaining -= 1;
+
+ if (remaining === 0) {
+ done_callback();
+ }
+ };
+
+ iter_callback(element, elDone);
+ });
+ }
+
+ function merge(a,b)
+ {
+ var rv = {};
+ var p;
+ for (p in a) {
+ rv[p] = a[p];
+ }
+ for (p in b) {
+ rv[p] = b[p];
+ }
+ return rv;
+ }
+
+ function expose(object, name)
+ {
+ var components = name.split(".");
+ var target = global_scope;
+ for (var i = 0; i < components.length - 1; i++) {
+ if (!(components[i] in target)) {
+ target[components[i]] = {};
+ }
+ target = target[components[i]];
+ }
+ target[components[components.length - 1]] = object;
+ }
+
+ function is_same_origin(w) {
+ try {
+ 'random_prop' in w;
+ return true;
+ } catch (e) {
+ return false;
+ }
+ }
+
+ /** Returns the 'src' URL of the first
+
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/reason.html b/test/fixtures/wpt/streams/transferable/reason.html
new file mode 100644
index 0000000..4251aa8
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/reason.html
@@ -0,0 +1,132 @@
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/resources/create-wasm-module.js b/test/fixtures/wpt/streams/transferable/resources/create-wasm-module.js
new file mode 100644
index 0000000..37064af
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/create-wasm-module.js
@@ -0,0 +1,11 @@
+// There aren't many cloneable types that will cause an error on
+// deserialization. WASM modules have the property that it's an error to
+// deserialize them cross-site, which works for our purposes.
+async function createWasmModule() {
+ // It doesn't matter what the module is, so we use one from another
+ // test.
+ const response =
+ await fetch("/wasm/serialization/module/resources/incrementer.wasm");
+ const ab = await response.arrayBuffer();
+ return WebAssembly.compile(ab);
+}
diff --git a/test/fixtures/wpt/streams/transferable/resources/deserialize-error-frame.html b/test/fixtures/wpt/streams/transferable/resources/deserialize-error-frame.html
new file mode 100644
index 0000000..5ec2fcd
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/deserialize-error-frame.html
@@ -0,0 +1,39 @@
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/resources/echo-iframe.html b/test/fixtures/wpt/streams/transferable/resources/echo-iframe.html
new file mode 100644
index 0000000..68f6850
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/echo-iframe.html
@@ -0,0 +1,7 @@
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/resources/echo-worker.js b/test/fixtures/wpt/streams/transferable/resources/echo-worker.js
new file mode 100644
index 0000000..806c237
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/echo-worker.js
@@ -0,0 +1,2 @@
+// A worker that just transfers back any message that is sent to it.
+onmessage = evt => postMessage(evt.data, [evt.data]);
diff --git a/test/fixtures/wpt/streams/transferable/resources/helpers.js b/test/fixtures/wpt/streams/transferable/resources/helpers.js
new file mode 100644
index 0000000..1250453
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/helpers.js
@@ -0,0 +1,132 @@
+'use strict';
+
+(() => {
+ // Create a ReadableStream that will pass the tests in
+ // testTransferredReadableStream(), below.
+ function createOriginalReadableStream() {
+ return new ReadableStream({
+ start(controller) {
+ controller.enqueue('a');
+ controller.close();
+ }
+ });
+ }
+
+ // Common tests to roughly determine that |rs| is a correctly transferred
+ // version of a stream created by createOriginalReadableStream().
+ function testTransferredReadableStream(rs) {
+ assert_equals(rs.constructor, ReadableStream,
+ 'rs should be a ReadableStream in this realm');
+ assert_true(rs instanceof ReadableStream,
+ 'instanceof check should pass');
+
+ // Perform a brand-check on |rs| in the process of calling getReader().
+ const reader = ReadableStream.prototype.getReader.call(rs);
+
+ return reader.read().then(({value, done}) => {
+ assert_false(done, 'done should be false');
+ assert_equals(value, 'a', 'value should be "a"');
+ return reader.read();
+ }).then(({done}) => {
+ assert_true(done, 'done should be true');
+ });
+ }
+
+ function testMessage(msg) {
+ assert_array_equals(msg.ports, [], 'there should be no ports in the event');
+ return testTransferredReadableStream(msg.data);
+ }
+
+ function testMessageEvent(target) {
+ return new Promise((resolve, reject) => {
+ target.addEventListener('message', ev => {
+ try {
+ resolve(testMessage(ev));
+ } catch (e) {
+ reject(e);
+ }
+ }, {once: true});
+ });
+ }
+
+ function testMessageEventOrErrorMessage(target) {
+ return new Promise((resolve, reject) => {
+ target.addEventListener('message', ev => {
+ if (typeof ev.data === 'string') {
+ // Assume it's an error message and reject with it.
+ reject(ev.data);
+ return;
+ }
+
+ try {
+ resolve(testMessage(ev));
+ } catch (e) {
+ reject(e);
+ }
+ }, {once: true});
+ });
+ }
+
+ function checkTestResults(target) {
+ return new Promise((resolve, reject) => {
+ target.onmessage = msg => {
+ // testharness.js sends us objects which we need to ignore.
+ if (typeof msg.data !== 'string')
+ return;
+
+ if (msg.data === 'OK') {
+ resolve();
+ } else {
+ reject(msg.data);
+ }
+ };
+ });
+ }
+
+ // These tests assume that a transferred ReadableStream will behave the same
+ // regardless of how it was transferred. This enables us to simply transfer the
+ // stream to ourselves.
+ function createTransferredReadableStream(underlyingSource) {
+ const original = new ReadableStream(underlyingSource);
+ const promise = new Promise((resolve, reject) => {
+ addEventListener('message', msg => {
+ const rs = msg.data;
+ if (rs instanceof ReadableStream) {
+ resolve(rs);
+ } else {
+ reject(new Error(`what is this thing: "${rs}"?`));
+ }
+ }, {once: true});
+ });
+ postMessage(original, '*', [original]);
+ return promise;
+ }
+
+ function recordingTransferredReadableStream(underlyingSource, strategy) {
+ const original = recordingReadableStream(underlyingSource, strategy);
+ const promise = new Promise((resolve, reject) => {
+ addEventListener('message', msg => {
+ const rs = msg.data;
+ if (rs instanceof ReadableStream) {
+ rs.events = original.events;
+ rs.eventsWithoutPulls = original.eventsWithoutPulls;
+ rs.controller = original.controller;
+ resolve(rs);
+ } else {
+ reject(new Error(`what is this thing: "${rs}"?`));
+ }
+ }, {once: true});
+ });
+ postMessage(original, '*', [original]);
+ return promise;
+ }
+
+ self.createOriginalReadableStream = createOriginalReadableStream;
+ self.testMessage = testMessage;
+ self.testMessageEvent = testMessageEvent;
+ self.testMessageEventOrErrorMessage = testMessageEventOrErrorMessage;
+ self.checkTestResults = checkTestResults;
+ self.createTransferredReadableStream = createTransferredReadableStream;
+ self.recordingTransferredReadableStream = recordingTransferredReadableStream;
+
+})();
diff --git a/test/fixtures/wpt/streams/transferable/resources/receiving-shared-worker.js b/test/fixtures/wpt/streams/transferable/resources/receiving-shared-worker.js
new file mode 100644
index 0000000..84f779c
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/receiving-shared-worker.js
@@ -0,0 +1,11 @@
+'use strict';
+importScripts('/resources/testharness.js', 'helpers.js');
+
+onconnect = evt => {
+ const port = evt.source;
+ const promise = testMessageEvent(port);
+ port.start();
+ promise
+ .then(() => port.postMessage('OK'))
+ .catch(err => port.postMessage(`BAD: ${err}`));
+};
diff --git a/test/fixtures/wpt/streams/transferable/resources/receiving-worker.js b/test/fixtures/wpt/streams/transferable/resources/receiving-worker.js
new file mode 100644
index 0000000..4ebb9c5
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/receiving-worker.js
@@ -0,0 +1,7 @@
+'use strict';
+importScripts('/resources/testharness.js', 'helpers.js');
+
+const promise = testMessageEvent(self);
+promise
+ .then(() => postMessage('OK'))
+ .catch(err => postMessage(`BAD: ${err}`));
diff --git a/test/fixtures/wpt/streams/transferable/resources/sending-shared-worker.js b/test/fixtures/wpt/streams/transferable/resources/sending-shared-worker.js
new file mode 100644
index 0000000..e579077
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/sending-shared-worker.js
@@ -0,0 +1,12 @@
+'use strict';
+importScripts('helpers.js');
+
+onconnect = msg => {
+ const port = msg.source;
+ const orig = createOriginalReadableStream();
+ try {
+ port.postMessage(orig, [orig]);
+ } catch (e) {
+ port.postMessage(e.message);
+ }
+};
diff --git a/test/fixtures/wpt/streams/transferable/resources/sending-worker.js b/test/fixtures/wpt/streams/transferable/resources/sending-worker.js
new file mode 100644
index 0000000..0b79733
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/sending-worker.js
@@ -0,0 +1,5 @@
+'use strict';
+importScripts('helpers.js');
+
+const orig = createOriginalReadableStream();
+postMessage(orig, [orig]);
diff --git a/test/fixtures/wpt/streams/transferable/resources/service-worker-iframe.html b/test/fixtures/wpt/streams/transferable/resources/service-worker-iframe.html
new file mode 100644
index 0000000..348d067
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/service-worker-iframe.html
@@ -0,0 +1,39 @@
+
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/resources/service-worker.js b/test/fixtures/wpt/streams/transferable/resources/service-worker.js
new file mode 100644
index 0000000..af76b6c
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/resources/service-worker.js
@@ -0,0 +1,30 @@
+'use strict';
+importScripts('/resources/testharness.js', 'helpers.js');
+
+onmessage = msg => {
+ const client = msg.source;
+ if (msg.data === 'SEND') {
+ sendingTest(client);
+ } else {
+ receivingTest(msg, client);
+ }
+};
+
+function sendingTest(client) {
+ const orig = createOriginalReadableStream();
+ try {
+ client.postMessage(orig, [orig]);
+ } catch (e) {
+ client.postMessage(e.message);
+ }
+}
+
+function receivingTest(msg, client) {
+ try {
+ msg.waitUntil(testMessage(msg)
+ .then(() => client.postMessage('OK'))
+ .catch(e => client.postMessage(`BAD: ${e}`)));
+ } catch (e) {
+ client.postMessage(`BAD: ${e}`);
+ }
+}
diff --git a/test/fixtures/wpt/streams/transferable/service-worker.https.html b/test/fixtures/wpt/streams/transferable/service-worker.https.html
new file mode 100644
index 0000000..2ca7f19
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/service-worker.https.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/shared-worker.html b/test/fixtures/wpt/streams/transferable/shared-worker.html
new file mode 100644
index 0000000..cd04154
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/shared-worker.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/transform-stream.html b/test/fixtures/wpt/streams/transferable/transform-stream.html
new file mode 100644
index 0000000..fbfbfe8
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/transform-stream.html
@@ -0,0 +1,104 @@
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/window.html b/test/fixtures/wpt/streams/transferable/window.html
new file mode 100644
index 0000000..beaf548
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/window.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/worker.html b/test/fixtures/wpt/streams/transferable/worker.html
new file mode 100644
index 0000000..c5dc9fc
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/worker.html
@@ -0,0 +1,76 @@
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transferable/writable-stream.html b/test/fixtures/wpt/streams/transferable/writable-stream.html
new file mode 100644
index 0000000..adc6f45
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/writable-stream.html
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/streams/transform-streams/backpressure.any.js b/test/fixtures/wpt/streams/transform-streams/backpressure.any.js
new file mode 100644
index 0000000..64c9d09
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/backpressure.any.js
@@ -0,0 +1,195 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/recording-streams.js
+// META: script=../resources/test-utils.js
+'use strict';
+
+const error1 = new Error('error1 message');
+error1.name = 'error1';
+
+promise_test(() => {
+ const ts = recordingTransformStream();
+ const writer = ts.writable.getWriter();
+ // This call never resolves.
+ writer.write('a');
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(ts.events, [], 'transform should not be called');
+ });
+}, 'backpressure allows no transforms with a default identity transform and no reader');
+
+promise_test(() => {
+ const ts = recordingTransformStream({}, undefined, { highWaterMark: 1 });
+ const writer = ts.writable.getWriter();
+ // This call to write() resolves asynchronously.
+ writer.write('a');
+ // This call to write() waits for backpressure that is never relieved and never calls transform().
+ writer.write('b');
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(ts.events, ['transform', 'a'], 'transform should be called once');
+ });
+}, 'backpressure only allows one transform() with a identity transform with a readable HWM of 1 and no reader');
+
+promise_test(() => {
+ // Without a transform() implementation, recordingTransformStream() never enqueues anything.
+ const ts = recordingTransformStream({
+ transform() {
+ // Discard all chunks. As a result, the readable side is never full enough to exert backpressure and transform()
+ // keeps being called.
+ }
+ }, undefined, { highWaterMark: 1 });
+ const writer = ts.writable.getWriter();
+ const writePromises = [];
+ for (let i = 0; i < 4; ++i) {
+ writePromises.push(writer.write(i));
+ }
+ return Promise.all(writePromises).then(() => {
+ assert_array_equals(ts.events, ['transform', 0, 'transform', 1, 'transform', 2, 'transform', 3],
+ 'all 4 events should be transformed');
+ });
+}, 'transform() should keep being called as long as there is no backpressure');
+
+promise_test(() => {
+ const ts = new TransformStream({}, undefined, { highWaterMark: 1 });
+ const writer = ts.writable.getWriter();
+ const reader = ts.readable.getReader();
+ const events = [];
+ const writerPromises = [
+ writer.write('a').then(() => events.push('a')),
+ writer.write('b').then(() => events.push('b')),
+ writer.close().then(() => events.push('closed'))];
+ return delay(0).then(() => {
+ assert_array_equals(events, ['a'], 'the first write should have resolved');
+ return reader.read();
+ }).then(({ value, done }) => {
+ assert_false(done, 'done should not be true');
+ assert_equals('a', value, 'value should be "a"');
+ return delay(0);
+ }).then(() => {
+ assert_array_equals(events, ['a', 'b', 'closed'], 'both writes and close() should have resolved');
+ return reader.read();
+ }).then(({ value, done }) => {
+ assert_false(done, 'done should still not be true');
+ assert_equals('b', value, 'value should be "b"');
+ return reader.read();
+ }).then(({ done }) => {
+ assert_true(done, 'done should be true');
+ return writerPromises;
+ });
+}, 'writes should resolve as soon as transform completes');
+
+promise_test(() => {
+ const ts = new TransformStream(undefined, undefined, { highWaterMark: 0 });
+ const writer = ts.writable.getWriter();
+ const reader = ts.readable.getReader();
+ const readPromise = reader.read();
+ writer.write('a');
+ return readPromise.then(({ value, done }) => {
+ assert_false(done, 'not done');
+ assert_equals(value, 'a', 'value should be "a"');
+ });
+}, 'calling pull() before the first write() with backpressure should work');
+
+promise_test(() => {
+ let reader;
+ const ts = recordingTransformStream({
+ transform(chunk, controller) {
+ controller.enqueue(chunk);
+ return reader.read();
+ }
+ }, undefined, { highWaterMark: 1 });
+ const writer = ts.writable.getWriter();
+ reader = ts.readable.getReader();
+ return writer.write('a');
+}, 'transform() should be able to read the chunk it just enqueued');
+
+promise_test(() => {
+ let resolveTransform;
+ const transformPromise = new Promise(resolve => {
+ resolveTransform = resolve;
+ });
+ const ts = recordingTransformStream({
+ transform() {
+ return transformPromise;
+ }
+ }, undefined, new CountQueuingStrategy({ highWaterMark: Infinity }));
+ const writer = ts.writable.getWriter();
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
+ return delay(0).then(() => {
+ writer.write('a');
+ assert_array_equals(ts.events, ['transform', 'a']);
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0');
+ return flushAsyncEvents();
+ }).then(() => {
+ assert_equals(writer.desiredSize, 0, 'desiredSize should still be 0');
+ resolveTransform();
+ return delay(0);
+ }).then(() => {
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
+ });
+}, 'blocking transform() should cause backpressure');
+
+promise_test(t => {
+ const ts = new TransformStream();
+ ts.readable.cancel(error1);
+ return promise_rejects_exactly(t, error1, ts.writable.getWriter().closed, 'closed should reject');
+}, 'writer.closed should resolve after readable is canceled during start');
+
+promise_test(t => {
+ const ts = new TransformStream({}, undefined, { highWaterMark: 0 });
+ return delay(0).then(() => {
+ ts.readable.cancel(error1);
+ return promise_rejects_exactly(t, error1, ts.writable.getWriter().closed, 'closed should reject');
+ });
+}, 'writer.closed should resolve after readable is canceled with backpressure');
+
+promise_test(t => {
+ const ts = new TransformStream({}, undefined, { highWaterMark: 1 });
+ return delay(0).then(() => {
+ ts.readable.cancel(error1);
+ return promise_rejects_exactly(t, error1, ts.writable.getWriter().closed, 'closed should reject');
+ });
+}, 'writer.closed should resolve after readable is canceled with no backpressure');
+
+promise_test(() => {
+ const ts = new TransformStream({}, undefined, { highWaterMark: 1 });
+ const writer = ts.writable.getWriter();
+ return delay(0).then(() => {
+ const writePromise = writer.write('a');
+ ts.readable.cancel(error1);
+ return writePromise;
+ });
+}, 'cancelling the readable should cause a pending write to resolve');
+
+promise_test(t => {
+ const rs = new ReadableStream();
+ const ts = new TransformStream();
+ const pipePromise = rs.pipeTo(ts.writable);
+ ts.readable.cancel(error1);
+ return promise_rejects_exactly(t, error1, pipePromise, 'promise returned from pipeTo() should be rejected');
+}, 'cancelling the readable side of a TransformStream should abort an empty pipe');
+
+promise_test(t => {
+ const rs = new ReadableStream();
+ const ts = new TransformStream();
+ const pipePromise = rs.pipeTo(ts.writable);
+ return delay(0).then(() => {
+ ts.readable.cancel(error1);
+ return promise_rejects_exactly(t, error1, pipePromise, 'promise returned from pipeTo() should be rejected');
+ });
+}, 'cancelling the readable side of a TransformStream should abort an empty pipe after startup');
+
+promise_test(t => {
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue('a');
+ controller.enqueue('b');
+ controller.enqueue('c');
+ }
+ });
+ const ts = new TransformStream();
+ const pipePromise = rs.pipeTo(ts.writable);
+ // Allow data to flow into the pipe.
+ return delay(0).then(() => {
+ ts.readable.cancel(error1);
+ return promise_rejects_exactly(t, error1, pipePromise, 'promise returned from pipeTo() should be rejected');
+ });
+}, 'cancelling the readable side of a TransformStream should abort a full pipe');
diff --git a/test/fixtures/wpt/streams/transform-streams/errors.any.js b/test/fixtures/wpt/streams/transform-streams/errors.any.js
new file mode 100644
index 0000000..ba26b32
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/errors.any.js
@@ -0,0 +1,341 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/test-utils.js
+'use strict';
+
+const thrownError = new Error('bad things are happening!');
+thrownError.name = 'error1';
+
+promise_test(t => {
+ const ts = new TransformStream({
+ transform() {
+ throw thrownError;
+ }
+ });
+
+ const reader = ts.readable.getReader();
+
+ const writer = ts.writable.getWriter();
+
+ return Promise.all([
+ promise_rejects_exactly(t, thrownError, writer.write('a'),
+ 'writable\'s write should reject with the thrown error'),
+ promise_rejects_exactly(t, thrownError, reader.read(),
+ 'readable\'s read should reject with the thrown error'),
+ promise_rejects_exactly(t, thrownError, reader.closed,
+ 'readable\'s closed should be rejected with the thrown error'),
+ promise_rejects_exactly(t, thrownError, writer.closed,
+ 'writable\'s closed should be rejected with the thrown error')
+ ]);
+}, 'TransformStream errors thrown in transform put the writable and readable in an errored state');
+
+promise_test(t => {
+ const ts = new TransformStream({
+ transform() {
+ },
+ flush() {
+ throw thrownError;
+ }
+ });
+
+ const reader = ts.readable.getReader();
+
+ const writer = ts.writable.getWriter();
+
+ return Promise.all([
+ writer.write('a'),
+ promise_rejects_exactly(t, thrownError, writer.close(),
+ 'writable\'s close should reject with the thrown error'),
+ promise_rejects_exactly(t, thrownError, reader.read(),
+ 'readable\'s read should reject with the thrown error'),
+ promise_rejects_exactly(t, thrownError, reader.closed,
+ 'readable\'s closed should be rejected with the thrown error'),
+ promise_rejects_exactly(t, thrownError, writer.closed,
+ 'writable\'s closed should be rejected with the thrown error')
+ ]);
+}, 'TransformStream errors thrown in flush put the writable and readable in an errored state');
+
+test(() => {
+ new TransformStream({
+ start(c) {
+ c.enqueue('a');
+ c.error(new Error('generic error'));
+ assert_throws_js(TypeError, () => c.enqueue('b'), 'enqueue() should throw');
+ }
+ });
+}, 'errored TransformStream should not enqueue new chunks');
+
+promise_test(t => {
+ const ts = new TransformStream({
+ start() {
+ return flushAsyncEvents().then(() => {
+ throw thrownError;
+ });
+ },
+ transform: t.unreached_func('transform should not be called'),
+ flush: t.unreached_func('flush should not be called')
+ });
+
+ const writer = ts.writable.getWriter();
+ const reader = ts.readable.getReader();
+ return Promise.all([
+ promise_rejects_exactly(t, thrownError, writer.write('a'), 'writer should reject with thrownError'),
+ promise_rejects_exactly(t, thrownError, writer.close(), 'close() should reject with thrownError'),
+ promise_rejects_exactly(t, thrownError, reader.read(), 'reader should reject with thrownError')
+ ]);
+}, 'TransformStream transformer.start() rejected promise should error the stream');
+
+promise_test(t => {
+ const controllerError = new Error('start failure');
+ controllerError.name = 'controllerError';
+ const ts = new TransformStream({
+ start(c) {
+ return flushAsyncEvents()
+ .then(() => {
+ c.error(controllerError);
+ throw new Error('ignored error');
+ });
+ },
+ transform: t.unreached_func('transform should never be called if start() fails'),
+ flush: t.unreached_func('flush should never be called if start() fails')
+ });
+
+ const writer = ts.writable.getWriter();
+ const reader = ts.readable.getReader();
+ return Promise.all([
+ promise_rejects_exactly(t, controllerError, writer.write('a'), 'writer should reject with controllerError'),
+ promise_rejects_exactly(t, controllerError, writer.close(), 'close should reject with same error'),
+ promise_rejects_exactly(t, controllerError, reader.read(), 'reader should reject with same error')
+ ]);
+}, 'when controller.error is followed by a rejection, the error reason should come from controller.error');
+
+test(() => {
+ assert_throws_js(URIError, () => new TransformStream({
+ start() { throw new URIError('start thrown error'); },
+ transform() {}
+ }), 'constructor should throw');
+}, 'TransformStream constructor should throw when start does');
+
+test(() => {
+ const strategy = {
+ size() { throw new URIError('size thrown error'); }
+ };
+
+ assert_throws_js(URIError, () => new TransformStream({
+ start(c) {
+ c.enqueue('a');
+ },
+ transform() {}
+ }, undefined, strategy), 'constructor should throw the same error strategy.size throws');
+}, 'when strategy.size throws inside start(), the constructor should throw the same error');
+
+test(() => {
+ const controllerError = new URIError('controller.error');
+
+ let controller;
+ const strategy = {
+ size() {
+ controller.error(controllerError);
+ throw new Error('redundant error');
+ }
+ };
+
+ assert_throws_js(URIError, () => new TransformStream({
+ start(c) {
+ controller = c;
+ c.enqueue('a');
+ },
+ transform() {}
+ }, undefined, strategy), 'the first error should be thrown');
+}, 'when strategy.size calls controller.error() then throws, the constructor should throw the first error');
+
+promise_test(t => {
+ const ts = new TransformStream();
+ const writer = ts.writable.getWriter();
+ const closedPromise = writer.closed;
+ return Promise.all([
+ ts.readable.cancel(thrownError),
+ promise_rejects_exactly(t, thrownError, closedPromise, 'closed should throw a TypeError')
+ ]);
+}, 'cancelling the readable side should error the writable');
+
+promise_test(t => {
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ const writer = ts.writable.getWriter();
+ const reader = ts.readable.getReader();
+ const writePromise = writer.write('a');
+ const closePromise = writer.close();
+ controller.error(thrownError);
+ return Promise.all([
+ promise_rejects_exactly(t, thrownError, reader.closed, 'reader.closed should reject'),
+ promise_rejects_exactly(t, thrownError, writePromise, 'writePromise should reject'),
+ promise_rejects_exactly(t, thrownError, closePromise, 'closePromise should reject')]);
+}, 'it should be possible to error the readable between close requested and complete');
+
+promise_test(t => {
+ const ts = new TransformStream({
+ transform(chunk, controller) {
+ controller.enqueue(chunk);
+ controller.terminate();
+ throw thrownError;
+ }
+ }, undefined, { highWaterMark: 1 });
+ const writePromise = ts.writable.getWriter().write('a');
+ const closedPromise = ts.readable.getReader().closed;
+ return Promise.all([
+ promise_rejects_exactly(t, thrownError, writePromise, 'write() should reject'),
+ promise_rejects_exactly(t, thrownError, closedPromise, 'reader.closed should reject')
+ ]);
+}, 'an exception from transform() should error the stream if terminate has been requested but not completed');
+
+promise_test(t => {
+ const ts = new TransformStream();
+ const writer = ts.writable.getWriter();
+ // The microtask following transformer.start() hasn't completed yet, so the abort is queued and not notified to the
+ // TransformStream yet.
+ const abortPromise = writer.abort(thrownError);
+ const cancelPromise = ts.readable.cancel(new Error('cancel reason'));
+ return Promise.all([
+ abortPromise,
+ cancelPromise,
+ promise_rejects_exactly(t, thrownError, writer.closed, 'writer.closed should reject with thrownError')]);
+}, 'abort should set the close reason for the writable when it happens before cancel during start, but cancel should ' +
+ 'still succeed');
+
+promise_test(t => {
+ let resolveTransform;
+ const transformPromise = new Promise(resolve => {
+ resolveTransform = resolve;
+ });
+ const ts = new TransformStream({
+ transform() {
+ return transformPromise;
+ }
+ }, undefined, { highWaterMark: 2 });
+ const writer = ts.writable.getWriter();
+ return delay(0).then(() => {
+ const writePromise = writer.write();
+ const abortPromise = writer.abort(thrownError);
+ const cancelPromise = ts.readable.cancel(new Error('cancel reason'));
+ resolveTransform();
+ return Promise.all([
+ writePromise,
+ abortPromise,
+ cancelPromise,
+ promise_rejects_exactly(t, thrownError, writer.closed, 'writer.closed should reject with thrownError')]);
+ });
+}, 'abort should set the close reason for the writable when it happens before cancel during underlying sink write, ' +
+ 'but cancel should still succeed');
+
+const ignoredError = new Error('ignoredError');
+ignoredError.name = 'ignoredError';
+
+promise_test(t => {
+ const ts = new TransformStream({
+ start(controller) {
+ controller.error(thrownError);
+ controller.error(ignoredError);
+ }
+ });
+ return promise_rejects_exactly(t, thrownError, ts.writable.abort(), 'abort() should reject with thrownError');
+}, 'controller.error() should do nothing the second time it is called');
+
+promise_test(t => {
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ const cancelPromise = ts.readable.cancel(thrownError);
+ controller.error(ignoredError);
+ return Promise.all([
+ cancelPromise,
+ promise_rejects_exactly(t, thrownError, ts.writable.getWriter().closed, 'closed should reject with thrownError')
+ ]);
+}, 'controller.error() should do nothing after readable.cancel()');
+
+promise_test(t => {
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ return ts.writable.abort(thrownError).then(() => {
+ controller.error(ignoredError);
+ return promise_rejects_exactly(t, thrownError, ts.writable.getWriter().closed, 'closed should reject with thrownError');
+ });
+}, 'controller.error() should do nothing after writable.abort() has completed');
+
+promise_test(t => {
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ },
+ transform() {
+ throw thrownError;
+ }
+ }, undefined, { highWaterMark: Infinity });
+ const writer = ts.writable.getWriter();
+ return promise_rejects_exactly(t, thrownError, writer.write(), 'write() should reject').then(() => {
+ controller.error();
+ return promise_rejects_exactly(t, thrownError, writer.closed, 'closed should reject with thrownError');
+ });
+}, 'controller.error() should do nothing after a transformer method has thrown an exception');
+
+promise_test(t => {
+ let controller;
+ let calls = 0;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ },
+ transform() {
+ ++calls;
+ }
+ }, undefined, { highWaterMark: 1 });
+ return delay(0).then(() => {
+ // Create backpressure.
+ controller.enqueue('a');
+ const writer = ts.writable.getWriter();
+ // transform() will not be called until backpressure is relieved.
+ const writePromise = writer.write('b');
+ assert_equals(calls, 0, 'transform() should not have been called');
+ controller.error(thrownError);
+ // Now backpressure has been relieved and the write can proceed.
+ return promise_rejects_exactly(t, thrownError, writePromise, 'write() should reject').then(() => {
+ assert_equals(calls, 0, 'transform() should not be called');
+ });
+ });
+}, 'erroring during write with backpressure should result in the write failing');
+
+promise_test(t => {
+ const ts = new TransformStream({}, undefined, { highWaterMark: 0 });
+ return delay(0).then(() => {
+ const writer = ts.writable.getWriter();
+ // write should start synchronously
+ const writePromise = writer.write(0);
+ // The underlying sink's abort() is not called until the write() completes.
+ const abortPromise = writer.abort(thrownError);
+ // Perform a read to relieve backpressure and permit the write() to complete.
+ const readPromise = ts.readable.getReader().read();
+ return Promise.all([
+ promise_rejects_exactly(t, thrownError, readPromise, 'read() should reject'),
+ promise_rejects_exactly(t, thrownError, writePromise, 'write() should reject'),
+ abortPromise
+ ]);
+ });
+}, 'a write() that was waiting for backpressure should reject if the writable is aborted');
+
+promise_test(t => {
+ const ts = new TransformStream();
+ ts.writable.abort(thrownError);
+ const reader = ts.readable.getReader();
+ return promise_rejects_exactly(t, thrownError, reader.read(), 'read() should reject with thrownError');
+}, 'the readable should be errored with the reason passed to the writable abort() method');
diff --git a/test/fixtures/wpt/streams/transform-streams/flush.any.js b/test/fixtures/wpt/streams/transform-streams/flush.any.js
new file mode 100644
index 0000000..dc40532
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/flush.any.js
@@ -0,0 +1,131 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/test-utils.js
+'use strict';
+
+promise_test(() => {
+ let flushCalled = false;
+ const ts = new TransformStream({
+ transform() { },
+ flush() {
+ flushCalled = true;
+ }
+ });
+
+ return ts.writable.getWriter().close().then(() => {
+ return assert_true(flushCalled, 'closing the writable triggers the transform flush immediately');
+ });
+}, 'TransformStream flush is called immediately when the writable is closed, if no writes are queued');
+
+promise_test(() => {
+ let flushCalled = false;
+ let resolveTransform;
+ const ts = new TransformStream({
+ transform() {
+ return new Promise(resolve => {
+ resolveTransform = resolve;
+ });
+ },
+ flush() {
+ flushCalled = true;
+ return new Promise(() => {}); // never resolves
+ }
+ }, undefined, { highWaterMark: 1 });
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+ writer.close();
+ assert_false(flushCalled, 'closing the writable does not immediately call flush if writes are not finished');
+
+ let rsClosed = false;
+ ts.readable.getReader().closed.then(() => {
+ rsClosed = true;
+ });
+
+ return delay(0).then(() => {
+ assert_false(flushCalled, 'closing the writable does not asynchronously call flush if writes are not finished');
+ resolveTransform();
+ return delay(0);
+ }).then(() => {
+ assert_true(flushCalled, 'flush is eventually called');
+ assert_false(rsClosed, 'if flushPromise does not resolve, the readable does not become closed');
+ });
+}, 'TransformStream flush is called after all queued writes finish, once the writable is closed');
+
+promise_test(() => {
+ let c;
+ const ts = new TransformStream({
+ start(controller) {
+ c = controller;
+ },
+ transform() {
+ },
+ flush() {
+ c.enqueue('x');
+ c.enqueue('y');
+ }
+ });
+
+ const reader = ts.readable.getReader();
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+ writer.close();
+ return reader.read().then(result1 => {
+ assert_equals(result1.value, 'x', 'the first chunk read is the first one enqueued in flush');
+ assert_equals(result1.done, false, 'the first chunk read is the first one enqueued in flush');
+
+ return reader.read().then(result2 => {
+ assert_equals(result2.value, 'y', 'the second chunk read is the second one enqueued in flush');
+ assert_equals(result2.done, false, 'the second chunk read is the second one enqueued in flush');
+ });
+ });
+}, 'TransformStream flush gets a chance to enqueue more into the readable');
+
+promise_test(() => {
+ let c;
+ const ts = new TransformStream({
+ start(controller) {
+ c = controller;
+ },
+ transform() {
+ },
+ flush() {
+ c.enqueue('x');
+ c.enqueue('y');
+ return delay(0);
+ }
+ });
+
+ const reader = ts.readable.getReader();
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+ writer.close();
+
+ return Promise.all([
+ reader.read().then(result1 => {
+ assert_equals(result1.value, 'x', 'the first chunk read is the first one enqueued in flush');
+ assert_equals(result1.done, false, 'the first chunk read is the first one enqueued in flush');
+
+ return reader.read().then(result2 => {
+ assert_equals(result2.value, 'y', 'the second chunk read is the second one enqueued in flush');
+ assert_equals(result2.done, false, 'the second chunk read is the second one enqueued in flush');
+ });
+ }),
+ reader.closed.then(() => {
+ assert_true(true, 'readable reader becomes closed');
+ })
+ ]);
+}, 'TransformStream flush gets a chance to enqueue more into the readable, and can then async close');
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+promise_test(t => {
+ const ts = new TransformStream({
+ flush(controller) {
+ controller.error(error1);
+ }
+ });
+ return promise_rejects_exactly(t, error1, ts.writable.getWriter().close(), 'close() should reject');
+}, 'error() during flush should cause writer.close() to reject');
diff --git a/test/fixtures/wpt/streams/transform-streams/general.any.js b/test/fixtures/wpt/streams/transform-streams/general.any.js
new file mode 100644
index 0000000..d4f2a1d
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/general.any.js
@@ -0,0 +1,437 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/test-utils.js
+// META: script=../resources/rs-utils.js
+'use strict';
+
+test(() => {
+ new TransformStream({ transform() { } });
+}, 'TransformStream can be constructed with a transform function');
+
+test(() => {
+ new TransformStream();
+ new TransformStream({});
+}, 'TransformStream can be constructed with no transform function');
+
+test(() => {
+ const ts = new TransformStream({ transform() { } });
+
+ const writer = ts.writable.getWriter();
+ assert_equals(writer.desiredSize, 1, 'writer.desiredSize should be 1');
+}, 'TransformStream writable starts in the writable state');
+
+promise_test(() => {
+ const ts = new TransformStream();
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+ assert_equals(writer.desiredSize, 0, 'writer.desiredSize should be 0 after write()');
+
+ return ts.readable.getReader().read().then(result => {
+ assert_equals(result.value, 'a',
+ 'result from reading the readable is the same as was written to writable');
+ assert_false(result.done, 'stream should not be done');
+
+ return delay(0).then(() => assert_equals(writer.desiredSize, 1, 'desiredSize should be 1 again'));
+ });
+}, 'Identity TransformStream: can read from readable what is put into writable');
+
+promise_test(() => {
+ let c;
+ const ts = new TransformStream({
+ start(controller) {
+ c = controller;
+ },
+ transform(chunk) {
+ c.enqueue(chunk.toUpperCase());
+ }
+ });
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+
+ return ts.readable.getReader().read().then(result => {
+ assert_equals(result.value, 'A',
+ 'result from reading the readable is the transformation of what was written to writable');
+ assert_false(result.done, 'stream should not be done');
+ });
+}, 'Uppercaser sync TransformStream: can read from readable transformed version of what is put into writable');
+
+promise_test(() => {
+ let c;
+ const ts = new TransformStream({
+ start(controller) {
+ c = controller;
+ },
+ transform(chunk) {
+ c.enqueue(chunk.toUpperCase());
+ c.enqueue(chunk.toUpperCase());
+ }
+ });
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+
+ const reader = ts.readable.getReader();
+
+ return reader.read().then(result1 => {
+ assert_equals(result1.value, 'A',
+ 'the first chunk read is the transformation of the single chunk written');
+ assert_false(result1.done, 'stream should not be done');
+
+ return reader.read().then(result2 => {
+ assert_equals(result2.value, 'A',
+ 'the second chunk read is also the transformation of the single chunk written');
+ assert_false(result2.done, 'stream should not be done');
+ });
+ });
+}, 'Uppercaser-doubler sync TransformStream: can read both chunks put into the readable');
+
+promise_test(() => {
+ let c;
+ const ts = new TransformStream({
+ start(controller) {
+ c = controller;
+ },
+ transform(chunk) {
+ return delay(0).then(() => c.enqueue(chunk.toUpperCase()));
+ }
+ });
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+
+ return ts.readable.getReader().read().then(result => {
+ assert_equals(result.value, 'A',
+ 'result from reading the readable is the transformation of what was written to writable');
+ assert_false(result.done, 'stream should not be done');
+ });
+}, 'Uppercaser async TransformStream: can read from readable transformed version of what is put into writable');
+
+promise_test(() => {
+ let doSecondEnqueue;
+ let returnFromTransform;
+ const ts = new TransformStream({
+ transform(chunk, controller) {
+ delay(0).then(() => controller.enqueue(chunk.toUpperCase()));
+ doSecondEnqueue = () => controller.enqueue(chunk.toUpperCase());
+ return new Promise(resolve => {
+ returnFromTransform = resolve;
+ });
+ }
+ });
+
+ const reader = ts.readable.getReader();
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+
+ return reader.read().then(result1 => {
+ assert_equals(result1.value, 'A',
+ 'the first chunk read is the transformation of the single chunk written');
+ assert_false(result1.done, 'stream should not be done');
+ doSecondEnqueue();
+
+ return reader.read().then(result2 => {
+ assert_equals(result2.value, 'A',
+ 'the second chunk read is also the transformation of the single chunk written');
+ assert_false(result2.done, 'stream should not be done');
+ returnFromTransform();
+ });
+ });
+}, 'Uppercaser-doubler async TransformStream: can read both chunks put into the readable');
+
+promise_test(() => {
+ const ts = new TransformStream({ transform() { } });
+
+ const writer = ts.writable.getWriter();
+ writer.close();
+
+ return Promise.all([writer.closed, ts.readable.getReader().closed]);
+}, 'TransformStream: by default, closing the writable closes the readable (when there are no queued writes)');
+
+promise_test(() => {
+ let transformResolve;
+ const transformPromise = new Promise(resolve => {
+ transformResolve = resolve;
+ });
+ const ts = new TransformStream({
+ transform() {
+ return transformPromise;
+ }
+ }, undefined, { highWaterMark: 1 });
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+ writer.close();
+
+ let rsClosed = false;
+ ts.readable.getReader().closed.then(() => {
+ rsClosed = true;
+ });
+
+ return delay(0).then(() => {
+ assert_equals(rsClosed, false, 'readable is not closed after a tick');
+ transformResolve();
+
+ return writer.closed.then(() => {
+ // TODO: Is this expectation correct?
+ assert_equals(rsClosed, true, 'readable is closed at that point');
+ });
+ });
+}, 'TransformStream: by default, closing the writable waits for transforms to finish before closing both');
+
+promise_test(() => {
+ let c;
+ const ts = new TransformStream({
+ start(controller) {
+ c = controller;
+ },
+ transform() {
+ c.enqueue('x');
+ c.enqueue('y');
+ return delay(0);
+ }
+ });
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+ writer.close();
+
+ const readableChunks = readableStreamToArray(ts.readable);
+
+ return writer.closed.then(() => {
+ return readableChunks.then(chunks => {
+ assert_array_equals(chunks, ['x', 'y'], 'both enqueued chunks can be read from the readable');
+ });
+ });
+}, 'TransformStream: by default, closing the writable closes the readable after sync enqueues and async done');
+
+promise_test(() => {
+ let c;
+ const ts = new TransformStream({
+ start(controller) {
+ c = controller;
+ },
+ transform() {
+ return delay(0)
+ .then(() => c.enqueue('x'))
+ .then(() => c.enqueue('y'))
+ .then(() => delay(0));
+ }
+ });
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+ writer.close();
+
+ const readableChunks = readableStreamToArray(ts.readable);
+
+ return writer.closed.then(() => {
+ return readableChunks.then(chunks => {
+ assert_array_equals(chunks, ['x', 'y'], 'both enqueued chunks can be read from the readable');
+ });
+ });
+}, 'TransformStream: by default, closing the writable closes the readable after async enqueues and async done');
+
+promise_test(() => {
+ let c;
+ const ts = new TransformStream({
+ suffix: '-suffix',
+
+ start(controller) {
+ c = controller;
+ c.enqueue('start' + this.suffix);
+ },
+
+ transform(chunk) {
+ c.enqueue(chunk + this.suffix);
+ },
+
+ flush() {
+ c.enqueue('flushed' + this.suffix);
+ }
+ });
+
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+ writer.close();
+
+ const readableChunks = readableStreamToArray(ts.readable);
+
+ return writer.closed.then(() => {
+ return readableChunks.then(chunks => {
+ assert_array_equals(chunks, ['start-suffix', 'a-suffix', 'flushed-suffix'], 'all enqueued chunks have suffixes');
+ });
+ });
+}, 'Transform stream should call transformer methods as methods');
+
+promise_test(() => {
+ function functionWithOverloads() {}
+ functionWithOverloads.apply = () => assert_unreached('apply() should not be called');
+ functionWithOverloads.call = () => assert_unreached('call() should not be called');
+ const ts = new TransformStream({
+ start: functionWithOverloads,
+ transform: functionWithOverloads,
+ flush: functionWithOverloads
+ });
+ const writer = ts.writable.getWriter();
+ writer.write('a');
+ writer.close();
+
+ return readableStreamToArray(ts.readable);
+}, 'methods should not not have .apply() or .call() called');
+
+promise_test(t => {
+ let startCalled = false;
+ let startDone = false;
+ let transformDone = false;
+ let flushDone = false;
+ const ts = new TransformStream({
+ start() {
+ startCalled = true;
+ return flushAsyncEvents().then(() => {
+ startDone = true;
+ });
+ },
+ transform() {
+ return t.step(() => {
+ assert_true(startDone, 'transform() should not be called until the promise returned from start() has resolved');
+ return flushAsyncEvents().then(() => {
+ transformDone = true;
+ });
+ });
+ },
+ flush() {
+ return t.step(() => {
+ assert_true(transformDone,
+ 'flush() should not be called until the promise returned from transform() has resolved');
+ return flushAsyncEvents().then(() => {
+ flushDone = true;
+ });
+ });
+ }
+ }, undefined, { highWaterMark: 1 });
+
+ assert_true(startCalled, 'start() should be called synchronously');
+
+ const writer = ts.writable.getWriter();
+ const writePromise = writer.write('a');
+ return writer.close().then(() => {
+ assert_true(flushDone, 'promise returned from flush() should have resolved');
+ return writePromise;
+ });
+}, 'TransformStream start, transform, and flush should be strictly ordered');
+
+promise_test(() => {
+ let transformCalled = false;
+ const ts = new TransformStream({
+ transform() {
+ transformCalled = true;
+ }
+ }, undefined, { highWaterMark: Infinity });
+ // transform() is only called synchronously when there is no backpressure and all microtasks have run.
+ return delay(0).then(() => {
+ const writePromise = ts.writable.getWriter().write();
+ assert_true(transformCalled, 'transform() should have been called');
+ return writePromise;
+ });
+}, 'it should be possible to call transform() synchronously');
+
+promise_test(() => {
+ const ts = new TransformStream({}, undefined, { highWaterMark: 0 });
+
+ const writer = ts.writable.getWriter();
+ writer.close();
+
+ return Promise.all([writer.closed, ts.readable.getReader().closed]);
+}, 'closing the writable should close the readable when there are no queued chunks, even with backpressure');
+
+test(() => {
+ new TransformStream({
+ start(controller) {
+ controller.terminate();
+ assert_throws_js(TypeError, () => controller.enqueue(), 'enqueue should throw');
+ }
+ });
+}, 'enqueue() should throw after controller.terminate()');
+
+promise_test(() => {
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ const cancelPromise = ts.readable.cancel();
+ assert_throws_js(TypeError, () => controller.enqueue(), 'enqueue should throw');
+ return cancelPromise;
+}, 'enqueue() should throw after readable.cancel()');
+
+test(() => {
+ new TransformStream({
+ start(controller) {
+ controller.terminate();
+ controller.terminate();
+ }
+ });
+}, 'controller.terminate() should do nothing the second time it is called');
+
+promise_test(t => {
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ const cancelReason = { name: 'cancelReason' };
+ const cancelPromise = ts.readable.cancel(cancelReason);
+ controller.terminate();
+ return Promise.all([
+ cancelPromise,
+ promise_rejects_exactly(t, cancelReason, ts.writable.getWriter().closed, 'closed should reject with cancelReason')
+ ]);
+}, 'terminate() should do nothing after readable.cancel()');
+
+promise_test(() => {
+ let calls = 0;
+ new TransformStream({
+ start() {
+ ++calls;
+ }
+ });
+ return flushAsyncEvents().then(() => {
+ assert_equals(calls, 1, 'start() should have been called exactly once');
+ });
+}, 'start() should not be called twice');
+
+test(() => {
+ assert_throws_js(RangeError, () => new TransformStream({ readableType: 'bytes' }), 'constructor should throw');
+}, 'specifying a defined readableType should throw');
+
+test(() => {
+ assert_throws_js(RangeError, () => new TransformStream({ writableType: 'bytes' }), 'constructor should throw');
+}, 'specifying a defined writableType should throw');
+
+test(() => {
+ class Subclass extends TransformStream {
+ extraFunction() {
+ return true;
+ }
+ }
+ assert_equals(
+ Object.getPrototypeOf(Subclass.prototype), TransformStream.prototype,
+ 'Subclass.prototype\'s prototype should be TransformStream.prototype');
+ assert_equals(Object.getPrototypeOf(Subclass), TransformStream,
+ 'Subclass\'s prototype should be TransformStream');
+ const sub = new Subclass();
+ assert_true(sub instanceof TransformStream,
+ 'Subclass object should be an instance of TransformStream');
+ assert_true(sub instanceof Subclass,
+ 'Subclass object should be an instance of Subclass');
+ const readableGetter = Object.getOwnPropertyDescriptor(
+ TransformStream.prototype, 'readable').get;
+ assert_equals(readableGetter.call(sub), sub.readable,
+ 'Subclass object should pass brand check');
+ assert_true(sub.extraFunction(),
+ 'extraFunction() should be present on Subclass object');
+}, 'Subclassing TransformStream should work');
diff --git a/test/fixtures/wpt/streams/transform-streams/lipfuzz.any.js b/test/fixtures/wpt/streams/transform-streams/lipfuzz.any.js
new file mode 100644
index 0000000..c8c3803
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/lipfuzz.any.js
@@ -0,0 +1,163 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+class LipFuzzTransformer {
+ constructor(substitutions) {
+ this.substitutions = substitutions;
+ this.partialChunk = '';
+ this.lastIndex = undefined;
+ }
+
+ transform(chunk, controller) {
+ chunk = this.partialChunk + chunk;
+ this.partialChunk = '';
+ // lastIndex is the index of the first character after the last substitution.
+ this.lastIndex = 0;
+ chunk = chunk.replace(/\{\{([a-zA-Z0-9_-]+)\}\}/g, this.replaceTag.bind(this));
+ // Regular expression for an incomplete template at the end of a string.
+ const partialAtEndRegexp = /\{(\{([a-zA-Z0-9_-]+(\})?)?)?$/g;
+ // Avoid looking at any characters that have already been substituted.
+ partialAtEndRegexp.lastIndex = this.lastIndex;
+ this.lastIndex = undefined;
+ const match = partialAtEndRegexp.exec(chunk);
+ if (match) {
+ this.partialChunk = chunk.substring(match.index);
+ chunk = chunk.substring(0, match.index);
+ }
+ controller.enqueue(chunk);
+ }
+
+ flush(controller) {
+ if (this.partialChunk.length > 0) {
+ controller.enqueue(this.partialChunk);
+ }
+ }
+
+ replaceTag(match, p1, offset) {
+ let replacement = this.substitutions[p1];
+ if (replacement === undefined) {
+ replacement = '';
+ }
+ this.lastIndex = offset + replacement.length;
+ return replacement;
+ }
+}
+
+const substitutions = {
+ in1: 'out1',
+ in2: 'out2',
+ quine: '{{quine}}',
+ bogusPartial: '{{incompleteResult}'
+};
+
+const cases = [
+ {
+ input: [''],
+ output: ['']
+ },
+ {
+ input: [],
+ output: []
+ },
+ {
+ input: ['{{in1}}'],
+ output: ['out1']
+ },
+ {
+ input: ['z{{in1}}'],
+ output: ['zout1']
+ },
+ {
+ input: ['{{in1}}q'],
+ output: ['out1q']
+ },
+ {
+ input: ['{{in1}}{{in1}'],
+ output: ['out1', '{{in1}']
+ },
+ {
+ input: ['{{in1}}{{in1}', '}'],
+ output: ['out1', 'out1']
+ },
+ {
+ input: ['{{in1', '}}'],
+ output: ['', 'out1']
+ },
+ {
+ input: ['{{', 'in1}}'],
+ output: ['', 'out1']
+ },
+ {
+ input: ['{', '{in1}}'],
+ output: ['', 'out1']
+ },
+ {
+ input: ['{{', 'in1}'],
+ output: ['', '', '{{in1}']
+ },
+ {
+ input: ['{'],
+ output: ['', '{']
+ },
+ {
+ input: ['{', ''],
+ output: ['', '', '{']
+ },
+ {
+ input: ['{', '{', 'i', 'n', '1', '}', '}'],
+ output: ['', '', '', '', '', '', 'out1']
+ },
+ {
+ input: ['{{in1}}{{in2}}{{in1}}'],
+ output: ['out1out2out1']
+ },
+ {
+ input: ['{{wrong}}'],
+ output: ['']
+ },
+ {
+ input: ['{{wron', 'g}}'],
+ output: ['', '']
+ },
+ {
+ input: ['{{quine}}'],
+ output: ['{{quine}}']
+ },
+ {
+ input: ['{{bogusPartial}}'],
+ output: ['{{incompleteResult}']
+ },
+ {
+ input: ['{{bogusPartial}}}'],
+ output: ['{{incompleteResult}}']
+ }
+];
+
+for (const testCase of cases) {
+ const inputChunks = testCase.input;
+ const outputChunks = testCase.output;
+ promise_test(() => {
+ const lft = new TransformStream(new LipFuzzTransformer(substitutions));
+ const writer = lft.writable.getWriter();
+ const promises = [];
+ for (const inputChunk of inputChunks) {
+ promises.push(writer.write(inputChunk));
+ }
+ promises.push(writer.close());
+ const reader = lft.readable.getReader();
+ let readerChain = Promise.resolve();
+ for (const outputChunk of outputChunks) {
+ readerChain = readerChain.then(() => {
+ return reader.read().then(({ value, done }) => {
+ assert_false(done, `done should be false when reading ${outputChunk}`);
+ assert_equals(value, outputChunk, `value should match outputChunk`);
+ });
+ });
+ }
+ readerChain = readerChain.then(() => {
+ return reader.read().then(({ done }) => assert_true(done, `done should be true`));
+ });
+ promises.push(readerChain);
+ return Promise.all(promises);
+ }, `testing "${inputChunks}" (length ${inputChunks.length})`);
+}
diff --git a/test/fixtures/wpt/streams/transform-streams/patched-global.any.js b/test/fixtures/wpt/streams/transform-streams/patched-global.any.js
new file mode 100644
index 0000000..416edf8
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/patched-global.any.js
@@ -0,0 +1,51 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+// Tests which patch the global environment are kept separate to avoid
+// interfering with other tests.
+
+test(t => {
+ // eslint-disable-next-line no-extend-native, accessor-pairs
+ Object.defineProperty(Object.prototype, 'highWaterMark', {
+ set() { throw new Error('highWaterMark setter called'); }
+ });
+
+ // eslint-disable-next-line no-extend-native, accessor-pairs
+ Object.defineProperty(Object.prototype, 'size', {
+ set() { throw new Error('size setter called'); }
+ });
+
+ t.add_cleanup(() => {
+ delete Object.prototype.highWaterMark;
+ delete Object.prototype.size;
+ });
+
+ assert_not_equals(new TransformStream(), null, 'constructor should work');
+}, 'TransformStream constructor should not call setters for highWaterMark or size');
+
+test(t => {
+ const oldReadableStream = ReadableStream;
+ const oldWritableStream = WritableStream;
+ const getReader = ReadableStream.prototype.getReader;
+ const getWriter = WritableStream.prototype.getWriter;
+
+ // Replace ReadableStream and WritableStream with broken versions.
+ ReadableStream = function () {
+ throw new Error('Called the global ReadableStream constructor');
+ };
+ WritableStream = function () {
+ throw new Error('Called the global WritableStream constructor');
+ };
+ t.add_cleanup(() => {
+ ReadableStream = oldReadableStream;
+ WritableStream = oldWritableStream;
+ });
+
+ const ts = new TransformStream();
+
+ // Just to be sure, ensure the readable and writable pass brand checks.
+ assert_not_equals(getReader.call(ts.readable), undefined,
+ 'getReader should work when called on ts.readable');
+ assert_not_equals(getWriter.call(ts.writable), undefined,
+ 'getWriter should work when called on ts.writable');
+}, 'TransformStream should use the original value of ReadableStream and WritableStream');
diff --git a/test/fixtures/wpt/streams/transform-streams/properties.any.js b/test/fixtures/wpt/streams/transform-streams/properties.any.js
new file mode 100644
index 0000000..f2ac482
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/properties.any.js
@@ -0,0 +1,49 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+const transformerMethods = {
+ start: {
+ length: 1,
+ trigger: () => Promise.resolve()
+ },
+ transform: {
+ length: 2,
+ trigger: ts => ts.writable.getWriter().write()
+ },
+ flush: {
+ length: 1,
+ trigger: ts => ts.writable.getWriter().close()
+ }
+};
+
+for (const method in transformerMethods) {
+ const { length, trigger } = transformerMethods[method];
+
+ // Some semantic tests of how transformer methods are called can be found in general.js, as well as in the test files
+ // specific to each method.
+ promise_test(() => {
+ let argCount;
+ const ts = new TransformStream({
+ [method](...args) {
+ argCount = args.length;
+ }
+ }, undefined, { highWaterMark: Infinity });
+ return Promise.resolve(trigger(ts)).then(() => {
+ assert_equals(argCount, length, `${method} should be called with ${length} arguments`);
+ });
+ }, `transformer method ${method} should be called with the right number of arguments`);
+
+ promise_test(() => {
+ let methodWasCalled = false;
+ function Transformer() {}
+ Transformer.prototype = {
+ [method]() {
+ methodWasCalled = true;
+ }
+ };
+ const ts = new TransformStream(new Transformer(), undefined, { highWaterMark: Infinity });
+ return Promise.resolve(trigger(ts)).then(() => {
+ assert_true(methodWasCalled, `${method} should be called`);
+ });
+ }, `transformer method ${method} should be called even when it's located on the prototype chain`);
+}
diff --git a/test/fixtures/wpt/streams/transform-streams/reentrant-strategies.any.js b/test/fixtures/wpt/streams/transform-streams/reentrant-strategies.any.js
new file mode 100644
index 0000000..31e5394
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/reentrant-strategies.any.js
@@ -0,0 +1,319 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/recording-streams.js
+// META: script=../resources/rs-utils.js
+// META: script=../resources/test-utils.js
+'use strict';
+
+// The size() function of readableStrategy can re-entrantly call back into the TransformStream implementation. This
+// makes it risky to cache state across the call to ReadableStreamDefaultControllerEnqueue. These tests attempt to catch
+// such errors. They are separated from the other strategy tests because no real user code should ever do anything like
+// this.
+//
+// There is no such issue with writableStrategy size() because it is never called from within TransformStream
+// algorithms.
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+promise_test(() => {
+ let controller;
+ let calls = 0;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ }, undefined, {
+ size() {
+ ++calls;
+ if (calls < 2) {
+ controller.enqueue('b');
+ }
+ return 1;
+ },
+ highWaterMark: Infinity
+ });
+ const writer = ts.writable.getWriter();
+ return Promise.all([writer.write('a'), writer.close()])
+ .then(() => readableStreamToArray(ts.readable))
+ .then(array => assert_array_equals(array, ['b', 'a'], 'array should contain two chunks'));
+}, 'enqueue() inside size() should work');
+
+promise_test(() => {
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ }, undefined, {
+ size() {
+ // The readable queue is empty.
+ controller.terminate();
+ // The readable state has gone from "readable" to "closed".
+ return 1;
+ // This chunk will be enqueued, but will be impossible to read because the state is already "closed".
+ },
+ highWaterMark: Infinity
+ });
+ const writer = ts.writable.getWriter();
+ return writer.write('a')
+ .then(() => readableStreamToArray(ts.readable))
+ .then(array => assert_array_equals(array, [], 'array should contain no chunks'));
+ // The chunk 'a' is still in readable's queue. readable is closed so 'a' cannot be read. writable's queue is empty and
+ // it is still writable.
+}, 'terminate() inside size() should work');
+
+promise_test(t => {
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ }, undefined, {
+ size() {
+ controller.error(error1);
+ return 1;
+ },
+ highWaterMark: Infinity
+ });
+ const writer = ts.writable.getWriter();
+ return writer.write('a')
+ .then(() => promise_rejects_exactly(t, error1, ts.readable.getReader().read(), 'read() should reject'));
+}, 'error() inside size() should work');
+
+promise_test(() => {
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ }, undefined, {
+ size() {
+ assert_equals(controller.desiredSize, 1, 'desiredSize should be 1');
+ return 1;
+ },
+ highWaterMark: 1
+ });
+ const writer = ts.writable.getWriter();
+ return Promise.all([writer.write('a'), writer.close()])
+ .then(() => readableStreamToArray(ts.readable))
+ .then(array => assert_array_equals(array, ['a'], 'array should contain one chunk'));
+}, 'desiredSize inside size() should work');
+
+promise_test(t => {
+ let cancelPromise;
+ const ts = new TransformStream({}, undefined, {
+ size() {
+ cancelPromise = ts.readable.cancel(error1);
+ return 1;
+ },
+ highWaterMark: Infinity
+ });
+ const writer = ts.writable.getWriter();
+ return writer.write('a')
+ .then(() => {
+ promise_rejects_exactly(t, error1, writer.closed, 'writer.closed should reject');
+ return cancelPromise;
+ });
+}, 'readable cancel() inside size() should work');
+
+promise_test(() => {
+ let controller;
+ let pipeToPromise;
+ const ws = recordingWritableStream();
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ }, undefined, {
+ size() {
+ if (!pipeToPromise) {
+ pipeToPromise = ts.readable.pipeTo(ws);
+ }
+ return 1;
+ },
+ highWaterMark: 1
+ });
+ // Allow promise returned by start() to resolve so that enqueue() will happen synchronously.
+ return delay(0).then(() => {
+ controller.enqueue('a');
+ assert_not_equals(pipeToPromise, undefined);
+
+ // Some pipeTo() implementations need an additional chunk enqueued in order for the first one to be processed. See
+ // https://github.com/whatwg/streams/issues/794 for background.
+ controller.enqueue('a');
+
+ // Give pipeTo() a chance to process the queued chunks.
+ return delay(0);
+ }).then(() => {
+ assert_array_equals(ws.events, ['write', 'a', 'write', 'a'], 'ws should contain two chunks');
+ controller.terminate();
+ return pipeToPromise;
+ }).then(() => {
+ assert_array_equals(ws.events, ['write', 'a', 'write', 'a', 'close'], 'target should have been closed');
+ });
+}, 'pipeTo() inside size() should work');
+
+promise_test(() => {
+ let controller;
+ let readPromise;
+ let calls = 0;
+ let reader;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ }, undefined, {
+ size() {
+ // This is triggered by controller.enqueue(). The queue is empty and there are no pending reads. pull() is called
+ // synchronously, allowing transform() to proceed asynchronously. This results in a second call to enqueue(),
+ // which resolves this pending read() without calling size() again.
+ readPromise = reader.read();
+ ++calls;
+ return 1;
+ },
+ highWaterMark: 0
+ });
+ reader = ts.readable.getReader();
+ const writer = ts.writable.getWriter();
+ let writeResolved = false;
+ const writePromise = writer.write('b').then(() => {
+ writeResolved = true;
+ });
+ return flushAsyncEvents().then(() => {
+ assert_false(writeResolved);
+ controller.enqueue('a');
+ assert_equals(calls, 1, 'size() should have been called once');
+ return delay(0);
+ }).then(() => {
+ assert_true(writeResolved);
+ assert_equals(calls, 1, 'size() should only be called once');
+ return readPromise;
+ }).then(({ value, done }) => {
+ assert_false(done, 'done should be false');
+ // See https://github.com/whatwg/streams/issues/794 for why this chunk is not 'a'.
+ assert_equals(value, 'b', 'chunk should have been read');
+ assert_equals(calls, 1, 'calls should still be 1');
+ return writePromise;
+ });
+}, 'read() inside of size() should work');
+
+promise_test(() => {
+ let writer;
+ let writePromise1;
+ let calls = 0;
+ const ts = new TransformStream({}, undefined, {
+ size() {
+ ++calls;
+ if (calls < 2) {
+ writePromise1 = writer.write('a');
+ }
+ return 1;
+ },
+ highWaterMark: Infinity
+ });
+ writer = ts.writable.getWriter();
+ // Give pull() a chance to be called.
+ return delay(0).then(() => {
+ // This write results in a synchronous call to transform(), enqueue(), and size().
+ const writePromise2 = writer.write('b');
+ assert_equals(calls, 1, 'size() should have been called once');
+ return Promise.all([writePromise1, writePromise2, writer.close()]);
+ }).then(() => {
+ assert_equals(calls, 2, 'size() should have been called twice');
+ return readableStreamToArray(ts.readable);
+ }).then(array => {
+ assert_array_equals(array, ['b', 'a'], 'both chunks should have been enqueued');
+ assert_equals(calls, 2, 'calls should still be 2');
+ });
+}, 'writer.write() inside size() should work');
+
+promise_test(() => {
+ let controller;
+ let writer;
+ let writePromise;
+ let calls = 0;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ }, undefined, {
+ size() {
+ ++calls;
+ if (calls < 2) {
+ writePromise = writer.write('a');
+ }
+ return 1;
+ },
+ highWaterMark: Infinity
+ });
+ writer = ts.writable.getWriter();
+ // Give pull() a chance to be called.
+ return delay(0).then(() => {
+ // This enqueue results in synchronous calls to size(), write(), transform() and enqueue().
+ controller.enqueue('b');
+ assert_equals(calls, 2, 'size() should have been called twice');
+ return Promise.all([writePromise, writer.close()]);
+ }).then(() => {
+ return readableStreamToArray(ts.readable);
+ }).then(array => {
+ // Because one call to enqueue() is nested inside the other, they finish in the opposite order that they were
+ // called, so the chunks end up reverse order.
+ assert_array_equals(array, ['a', 'b'], 'both chunks should have been enqueued');
+ assert_equals(calls, 2, 'calls should still be 2');
+ });
+}, 'synchronous writer.write() inside size() should work');
+
+promise_test(() => {
+ let writer;
+ let closePromise;
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ }, undefined, {
+ size() {
+ closePromise = writer.close();
+ return 1;
+ },
+ highWaterMark: 1
+ });
+ writer = ts.writable.getWriter();
+ const reader = ts.readable.getReader();
+ // Wait for the promise returned by start() to be resolved so that the call to close() will result in a synchronous
+ // call to TransformStreamDefaultSink.
+ return delay(0).then(() => {
+ controller.enqueue('a');
+ return reader.read();
+ }).then(({ value, done }) => {
+ assert_false(done, 'done should be false');
+ assert_equals(value, 'a', 'value should be correct');
+ return reader.read();
+ }).then(({ done }) => {
+ assert_true(done, 'done should be true');
+ return closePromise;
+ });
+}, 'writer.close() inside size() should work');
+
+promise_test(t => {
+ let abortPromise;
+ let controller;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ }
+ }, undefined, {
+ size() {
+ abortPromise = ts.writable.abort(error1);
+ return 1;
+ },
+ highWaterMark: 1
+ });
+ const reader = ts.readable.getReader();
+ // Wait for the promise returned by start() to be resolved so that the call to abort() will result in a synchronous
+ // call to TransformStreamDefaultSink.
+ return delay(0).then(() => {
+ controller.enqueue('a');
+ return Promise.all([promise_rejects_exactly(t, error1, reader.read(), 'read() should reject'), abortPromise]);
+ });
+}, 'writer.abort() inside size() should work');
diff --git a/test/fixtures/wpt/streams/transform-streams/strategies.any.js b/test/fixtures/wpt/streams/transform-streams/strategies.any.js
new file mode 100644
index 0000000..d465d31
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/strategies.any.js
@@ -0,0 +1,150 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/recording-streams.js
+// META: script=../resources/test-utils.js
+'use strict';
+
+// Here we just test that the strategies are correctly passed to the readable and writable sides. We assume that
+// ReadableStream and WritableStream will correctly apply the strategies when they are being used by a TransformStream
+// and so it isn't necessary to repeat their tests here.
+
+test(() => {
+ const ts = new TransformStream({}, { highWaterMark: 17 });
+ assert_equals(ts.writable.getWriter().desiredSize, 17, 'desiredSize should be 17');
+}, 'writableStrategy highWaterMark should work');
+
+promise_test(() => {
+ const ts = recordingTransformStream({}, undefined, { highWaterMark: 9 });
+ const writer = ts.writable.getWriter();
+ for (let i = 0; i < 10; ++i) {
+ writer.write(i);
+ }
+ return delay(0).then(() => {
+ assert_array_equals(ts.events, [
+ 'transform', 0, 'transform', 1, 'transform', 2, 'transform', 3, 'transform', 4,
+ 'transform', 5, 'transform', 6, 'transform', 7, 'transform', 8],
+ 'transform() should have been called 9 times');
+ });
+}, 'readableStrategy highWaterMark should work');
+
+promise_test(t => {
+ let writableSizeCalled = false;
+ let readableSizeCalled = false;
+ let transformCalled = false;
+ const ts = new TransformStream(
+ {
+ transform(chunk, controller) {
+ t.step(() => {
+ transformCalled = true;
+ assert_true(writableSizeCalled, 'writableStrategy.size() should have been called');
+ assert_false(readableSizeCalled, 'readableStrategy.size() should not have been called');
+ controller.enqueue(chunk);
+ assert_true(readableSizeCalled, 'readableStrategy.size() should have been called');
+ });
+ }
+ },
+ {
+ size() {
+ writableSizeCalled = true;
+ return 1;
+ }
+ },
+ {
+ size() {
+ readableSizeCalled = true;
+ return 1;
+ },
+ highWaterMark: Infinity
+ });
+ return ts.writable.getWriter().write().then(() => {
+ assert_true(transformCalled, 'transform() should be called');
+ });
+}, 'writable should have the correct size() function');
+
+test(() => {
+ const ts = new TransformStream();
+ const writer = ts.writable.getWriter();
+ assert_equals(writer.desiredSize, 1, 'default writable HWM is 1');
+ writer.write(undefined);
+ assert_equals(writer.desiredSize, 0, 'default chunk size is 1');
+}, 'default writable strategy should be equivalent to { highWaterMark: 1 }');
+
+promise_test(t => {
+ const ts = new TransformStream({
+ transform(chunk, controller) {
+ return t.step(() => {
+ assert_equals(controller.desiredSize, 0, 'desiredSize should be 0');
+ controller.enqueue(undefined);
+ // The first chunk enqueued is consumed by the pending read().
+ assert_equals(controller.desiredSize, 0, 'desiredSize should still be 0');
+ controller.enqueue(undefined);
+ assert_equals(controller.desiredSize, -1, 'desiredSize should be -1');
+ });
+ }
+ });
+ const writePromise = ts.writable.getWriter().write();
+ return ts.readable.getReader().read().then(() => writePromise);
+}, 'default readable strategy should be equivalent to { highWaterMark: 0 }');
+
+test(() => {
+ assert_throws_js(RangeError, () => new TransformStream(undefined, { highWaterMark: -1 }),
+ 'should throw RangeError for negative writableHighWaterMark');
+ assert_throws_js(RangeError, () => new TransformStream(undefined, undefined, { highWaterMark: -1 }),
+ 'should throw RangeError for negative readableHighWaterMark');
+ assert_throws_js(RangeError, () => new TransformStream(undefined, { highWaterMark: NaN }),
+ 'should throw RangeError for NaN writableHighWaterMark');
+ assert_throws_js(RangeError, () => new TransformStream(undefined, undefined, { highWaterMark: NaN }),
+ 'should throw RangeError for NaN readableHighWaterMark');
+}, 'a RangeError should be thrown for an invalid highWaterMark');
+
+const objectThatConvertsTo42 = {
+ toString() {
+ return '42';
+ }
+};
+
+test(() => {
+ const ts = new TransformStream(undefined, { highWaterMark: objectThatConvertsTo42 });
+ const writer = ts.writable.getWriter();
+ assert_equals(writer.desiredSize, 42, 'writable HWM is 42');
+}, 'writableStrategy highWaterMark should be converted to a number');
+
+test(() => {
+ const ts = new TransformStream({
+ start(controller) {
+ assert_equals(controller.desiredSize, 42, 'desiredSize should be 42');
+ }
+ }, undefined, { highWaterMark: objectThatConvertsTo42 });
+}, 'readableStrategy highWaterMark should be converted to a number');
+
+promise_test(t => {
+ const ts = new TransformStream(undefined, undefined, {
+ size() { return NaN; },
+ highWaterMark: 1
+ });
+ const writer = ts.writable.getWriter();
+ return promise_rejects_js(t, RangeError, writer.write(), 'write should reject');
+}, 'a bad readableStrategy size function should cause writer.write() to reject on an identity transform');
+
+promise_test(t => {
+ const ts = new TransformStream({
+ transform(chunk, controller) {
+ // This assert has the important side-effect of catching the error, so transform() does not throw.
+ assert_throws_js(RangeError, () => controller.enqueue(chunk), 'enqueue should throw');
+ }
+ }, undefined, {
+ size() {
+ return -1;
+ },
+ highWaterMark: 1
+ });
+
+ const writer = ts.writable.getWriter();
+ return writer.write().then(() => {
+ return Promise.all([
+ promise_rejects_js(t, RangeError, writer.ready, 'ready should reject'),
+ promise_rejects_js(t, RangeError, writer.closed, 'closed should reject'),
+ promise_rejects_js(t, RangeError, ts.readable.getReader().closed, 'readable closed should reject')
+ ]);
+ });
+}, 'a bad readableStrategy size function should error the stream on enqueue even when transformer.transform() ' +
+ 'catches the exception');
diff --git a/test/fixtures/wpt/streams/transform-streams/terminate.any.js b/test/fixtures/wpt/streams/transform-streams/terminate.any.js
new file mode 100644
index 0000000..8cb1067
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/terminate.any.js
@@ -0,0 +1,100 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/recording-streams.js
+// META: script=../resources/test-utils.js
+'use strict';
+
+promise_test(t => {
+ const ts = recordingTransformStream({}, undefined, { highWaterMark: 0 });
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(0);
+ }
+ });
+ let pipeToRejected = false;
+ const pipeToPromise = promise_rejects_js(t, TypeError, rs.pipeTo(ts.writable), 'pipeTo should reject').then(() => {
+ pipeToRejected = true;
+ });
+ return delay(0).then(() => {
+ assert_array_equals(ts.events, [], 'transform() should have seen no chunks');
+ assert_false(pipeToRejected, 'pipeTo() should not have rejected yet');
+ ts.controller.terminate();
+ return pipeToPromise;
+ }).then(() => {
+ assert_array_equals(ts.events, [], 'transform() should still have seen no chunks');
+ assert_true(pipeToRejected, 'pipeToRejected must be true');
+ });
+}, 'controller.terminate() should error pipeTo()');
+
+promise_test(t => {
+ const ts = recordingTransformStream({}, undefined, { highWaterMark: 1 });
+ const rs = new ReadableStream({
+ start(controller) {
+ controller.enqueue(0);
+ controller.enqueue(1);
+ }
+ });
+ const pipeToPromise = rs.pipeTo(ts.writable);
+ return delay(0).then(() => {
+ assert_array_equals(ts.events, ['transform', 0], 'transform() should have seen one chunk');
+ ts.controller.terminate();
+ return promise_rejects_js(t, TypeError, pipeToPromise, 'pipeTo() should reject');
+ }).then(() => {
+ assert_array_equals(ts.events, ['transform', 0], 'transform() should still have seen only one chunk');
+ });
+}, 'controller.terminate() should prevent remaining chunks from being processed');
+
+test(() => {
+ new TransformStream({
+ start(controller) {
+ controller.enqueue(0);
+ controller.terminate();
+ assert_throws_js(TypeError, () => controller.enqueue(1), 'enqueue should throw');
+ }
+ });
+}, 'controller.enqueue() should throw after controller.terminate()');
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+promise_test(t => {
+ const ts = new TransformStream({
+ start(controller) {
+ controller.enqueue(0);
+ controller.terminate();
+ controller.error(error1);
+ }
+ });
+ return Promise.all([
+ promise_rejects_js(t, TypeError, ts.writable.abort(), 'abort() should reject with a TypeError'),
+ promise_rejects_exactly(t, error1, ts.readable.cancel(), 'cancel() should reject with error1'),
+ promise_rejects_exactly(t, error1, ts.readable.getReader().closed, 'closed should reject with error1')
+ ]);
+}, 'controller.error() after controller.terminate() with queued chunk should error the readable');
+
+promise_test(t => {
+ const ts = new TransformStream({
+ start(controller) {
+ controller.terminate();
+ controller.error(error1);
+ }
+ });
+ return Promise.all([
+ promise_rejects_js(t, TypeError, ts.writable.abort(), 'abort() should reject with a TypeError'),
+ ts.readable.cancel(),
+ ts.readable.getReader().closed
+ ]);
+}, 'controller.error() after controller.terminate() without queued chunk should do nothing');
+
+promise_test(() => {
+ const ts = new TransformStream({
+ flush(controller) {
+ controller.terminate();
+ }
+ });
+ const writer = ts.writable.getWriter();
+ return Promise.all([
+ writer.close(),
+ writer.closed,
+ ts.readable.getReader().closed
+ ]);
+}, 'controller.terminate() inside flush() should not prevent writer.close() from succeeding');
diff --git a/test/fixtures/wpt/streams/writable-streams/aborting.any.js b/test/fixtures/wpt/streams/writable-streams/aborting.any.js
new file mode 100644
index 0000000..5c053ba
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/aborting.any.js
@@ -0,0 +1,1378 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/test-utils.js
+// META: script=../resources/recording-streams.js
+'use strict';
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+const error2 = new Error('error2');
+error2.name = 'error2';
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write: t.unreached_func('write() should not be called')
+ });
+
+ const writer = ws.getWriter();
+ const writePromise = writer.write('a');
+
+ const readyPromise = writer.ready;
+
+ writer.abort(error1);
+
+ assert_equals(writer.ready, readyPromise, 'the ready promise property should not change');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error1, readyPromise, 'the ready promise should reject with error1'),
+ promise_rejects_exactly(t, error1, writePromise, 'the write() promise should reject with error1')
+ ]);
+}, 'Aborting a WritableStream before it starts should cause the writer\'s unsettled ready promise to reject');
+
+promise_test(t => {
+ const ws = new WritableStream();
+
+ const writer = ws.getWriter();
+ writer.write('a');
+
+ const readyPromise = writer.ready;
+
+ return readyPromise.then(() => {
+ writer.abort(error1);
+
+ assert_not_equals(writer.ready, readyPromise, 'the ready promise property should change');
+ return promise_rejects_exactly(t, error1, writer.ready, 'the ready promise should reject with error1');
+ });
+}, 'Aborting a WritableStream should cause the writer\'s fulfilled ready promise to reset to a rejected one');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+
+ writer.releaseLock();
+
+ return promise_rejects_js(t, TypeError, writer.abort(), 'abort() should reject with a TypeError');
+}, 'abort() on a released writer rejects');
+
+promise_test(t => {
+ const ws = recordingWritableStream();
+
+ return delay(0)
+ .then(() => {
+ const writer = ws.getWriter();
+
+ const abortPromise = writer.abort(error1);
+
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writer.write(1), 'write(1) must reject with error1'),
+ promise_rejects_exactly(t, error1, writer.write(2), 'write(2) must reject with error1'),
+ abortPromise
+ ]);
+ })
+ .then(() => {
+ assert_array_equals(ws.events, ['abort', error1]);
+ });
+}, 'Aborting a WritableStream immediately prevents future writes');
+
+promise_test(t => {
+ const ws = recordingWritableStream();
+ const results = [];
+
+ return delay(0)
+ .then(() => {
+ const writer = ws.getWriter();
+
+ results.push(
+ writer.write(1),
+ promise_rejects_exactly(t, error1, writer.write(2), 'write(2) must reject with error1'),
+ promise_rejects_exactly(t, error1, writer.write(3), 'write(3) must reject with error1')
+ );
+
+ const abortPromise = writer.abort(error1);
+
+ results.push(
+ promise_rejects_exactly(t, error1, writer.write(4), 'write(4) must reject with error1'),
+ promise_rejects_exactly(t, error1, writer.write(5), 'write(5) must reject with error1')
+ );
+
+ return abortPromise;
+ }).then(() => {
+ assert_array_equals(ws.events, ['write', 1, 'abort', error1]);
+
+ return Promise.all(results);
+ });
+}, 'Aborting a WritableStream prevents further writes after any that are in progress');
+
+promise_test(() => {
+ const ws = new WritableStream({
+ abort() {
+ return 'Hello';
+ }
+ });
+ const writer = ws.getWriter();
+
+ return writer.abort('a').then(value => {
+ assert_equals(value, undefined, 'fulfillment value must be undefined');
+ });
+}, 'Fulfillment value of writer.abort() call must be undefined even if the underlying sink returns a non-undefined ' +
+ 'value');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ abort() {
+ throw error1;
+ }
+ });
+ const writer = ws.getWriter();
+
+ return promise_rejects_exactly(t, error1, writer.abort(undefined),
+ 'rejection reason of abortPromise must be the error thrown by abort');
+}, 'WritableStream if sink\'s abort throws, the promise returned by writer.abort() rejects');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ abort() {
+ throw error1;
+ }
+ });
+ const writer = ws.getWriter();
+
+ const abortPromise1 = writer.abort(undefined);
+ const abortPromise2 = writer.abort(undefined);
+
+ assert_equals(abortPromise1, abortPromise2, 'the promises must be the same');
+
+ return promise_rejects_exactly(t, error1, abortPromise1, 'promise must have matching rejection');
+}, 'WritableStream if sink\'s abort throws, the promise returned by multiple writer.abort()s is the same and rejects');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ abort() {
+ throw error1;
+ }
+ });
+
+ return promise_rejects_exactly(t, error1, ws.abort(undefined),
+ 'rejection reason of abortPromise must be the error thrown by abort');
+}, 'WritableStream if sink\'s abort throws, the promise returned by ws.abort() rejects');
+
+promise_test(t => {
+ let resolveWritePromise;
+ const ws = new WritableStream({
+ write() {
+ return new Promise(resolve => {
+ resolveWritePromise = resolve;
+ });
+ },
+ abort() {
+ throw error1;
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ writer.write().catch(() => {});
+ return flushAsyncEvents().then(() => {
+ const abortPromise = writer.abort(undefined);
+
+ resolveWritePromise();
+ return promise_rejects_exactly(t, error1, abortPromise,
+ 'rejection reason of abortPromise must be the error thrown by abort');
+ });
+}, 'WritableStream if sink\'s abort throws, for an abort performed during a write, the promise returned by ' +
+ 'ws.abort() rejects');
+
+promise_test(() => {
+ const ws = recordingWritableStream();
+ const writer = ws.getWriter();
+
+ return writer.abort(error1).then(() => {
+ assert_array_equals(ws.events, ['abort', error1]);
+ });
+}, 'Aborting a WritableStream passes through the given reason');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+
+ const abortPromise = writer.abort(error1);
+
+ const events = [];
+ writer.ready.catch(() => {
+ events.push('ready');
+ });
+ writer.closed.catch(() => {
+ events.push('closed');
+ });
+
+ return Promise.all([
+ abortPromise,
+ promise_rejects_exactly(t, error1, writer.write(), 'writing should reject with error1'),
+ promise_rejects_exactly(t, error1, writer.close(), 'closing should reject with error1'),
+ promise_rejects_exactly(t, error1, writer.ready, 'ready should reject with error1'),
+ promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with error1')
+ ]).then(() => {
+ assert_array_equals(['ready', 'closed'], events, 'ready should reject before closed');
+ });
+}, 'Aborting a WritableStream puts it in an errored state with the error passed to abort()');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+
+ const writePromise = promise_rejects_exactly(t, error1, writer.write('a'),
+ 'writing should reject with error1');
+
+ writer.abort(error1);
+
+ return writePromise;
+}, 'Aborting a WritableStream causes any outstanding write() promises to be rejected with the reason supplied');
+
+promise_test(t => {
+ const ws = recordingWritableStream();
+ const writer = ws.getWriter();
+
+ const closePromise = writer.close();
+ const abortPromise = writer.abort(error1);
+
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with error1'),
+ promise_rejects_exactly(t, error1, closePromise, 'close() should reject with error1'),
+ abortPromise
+ ]).then(() => {
+ assert_array_equals(ws.events, ['abort', error1]);
+ });
+}, 'Closing but then immediately aborting a WritableStream causes the stream to error');
+
+promise_test(() => {
+ let resolveClose;
+ const ws = new WritableStream({
+ close() {
+ return new Promise(resolve => {
+ resolveClose = resolve;
+ });
+ }
+ });
+ const writer = ws.getWriter();
+
+ const closePromise = writer.close();
+
+ return delay(0).then(() => {
+ const abortPromise = writer.abort(error1);
+ resolveClose();
+ return Promise.all([
+ writer.closed,
+ abortPromise,
+ closePromise
+ ]);
+ });
+}, 'Closing a WritableStream and aborting it while it closes causes the stream to ignore the abort attempt');
+
+promise_test(() => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+
+ writer.close();
+
+ return delay(0).then(() => writer.abort());
+}, 'Aborting a WritableStream after it is closed is a no-op');
+
+promise_test(t => {
+ // Testing that per https://github.com/whatwg/streams/issues/620#issuecomment-263483953 the fallback to close was
+ // removed.
+
+ // Cannot use recordingWritableStream since it always has an abort
+ let closeCalled = false;
+ const ws = new WritableStream({
+ close() {
+ closeCalled = true;
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ writer.abort(error1);
+
+ return promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with error1').then(() => {
+ assert_false(closeCalled, 'close must not have been called');
+ });
+}, 'WritableStream should NOT call underlying sink\'s close if no abort is supplied (historical)');
+
+promise_test(() => {
+ let thenCalled = false;
+ const ws = new WritableStream({
+ abort() {
+ return {
+ then(onFulfilled) {
+ thenCalled = true;
+ onFulfilled();
+ }
+ };
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.abort().then(() => assert_true(thenCalled, 'then() should be called'));
+}, 'returning a thenable from abort() should work');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write() {
+ return flushAsyncEvents();
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const writePromise = writer.write('a');
+ writer.abort(error1);
+ let closedRejected = false;
+ return Promise.all([
+ writePromise.then(() => assert_false(closedRejected, '.closed should not resolve before write()')),
+ promise_rejects_exactly(t, error1, writer.closed, '.closed should reject').then(() => {
+ closedRejected = true;
+ })
+ ]);
+ });
+}, '.closed should not resolve before fulfilled write()');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write() {
+ return Promise.reject(error1);
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const writePromise = writer.write('a');
+ const abortPromise = writer.abort(error2);
+ let closedRejected = false;
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writePromise, 'write() should reject')
+ .then(() => assert_false(closedRejected, '.closed should not resolve before write()')),
+ promise_rejects_exactly(t, error2, writer.closed, '.closed should reject')
+ .then(() => {
+ closedRejected = true;
+ }),
+ abortPromise
+ ]);
+ });
+}, '.closed should not resolve before rejected write(); write() error should not overwrite abort() error');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write() {
+ return flushAsyncEvents();
+ }
+ }, new CountQueuingStrategy({ highWaterMark: 4 }));
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const settlementOrder = [];
+ return Promise.all([
+ writer.write('1').then(() => settlementOrder.push(1)),
+ promise_rejects_exactly(t, error1, writer.write('2'), 'first queued write should be rejected')
+ .then(() => settlementOrder.push(2)),
+ promise_rejects_exactly(t, error1, writer.write('3'), 'second queued write should be rejected')
+ .then(() => settlementOrder.push(3)),
+ writer.abort(error1)
+ ]).then(() => assert_array_equals([1, 2, 3], settlementOrder, 'writes should be satisfied in order'));
+ });
+}, 'writes should be satisfied in order when aborting');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write() {
+ return Promise.reject(error1);
+ }
+ }, new CountQueuingStrategy({ highWaterMark: 4 }));
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const settlementOrder = [];
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writer.write('1'), 'in-flight write should be rejected')
+ .then(() => settlementOrder.push(1)),
+ promise_rejects_exactly(t, error2, writer.write('2'), 'first queued write should be rejected')
+ .then(() => settlementOrder.push(2)),
+ promise_rejects_exactly(t, error2, writer.write('3'), 'second queued write should be rejected')
+ .then(() => settlementOrder.push(3)),
+ writer.abort(error2)
+ ]).then(() => assert_array_equals([1, 2, 3], settlementOrder, 'writes should be satisfied in order'));
+ });
+}, 'writes should be satisfied in order after rejected write when aborting');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write() {
+ return Promise.reject(error1);
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writer.write('a'), 'writer.write() should reject with error from underlying write()'),
+ promise_rejects_exactly(t, error2, writer.close(),
+ 'writer.close() should reject with error from underlying write()'),
+ writer.abort(error2)
+ ]);
+ });
+}, 'close() should reject with abort reason why abort() is first error');
+
+promise_test(() => {
+ let resolveWrite;
+ const ws = recordingWritableStream({
+ write() {
+ return new Promise(resolve => {
+ resolveWrite = resolve;
+ });
+ }
+ });
+
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ writer.write('a');
+ const abortPromise = writer.abort('b');
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(ws.events, ['write', 'a'], 'abort should not be called while write is in-flight');
+ resolveWrite();
+ return abortPromise.then(() => {
+ assert_array_equals(ws.events, ['write', 'a', 'abort', 'b'], 'abort should be called after the write finishes');
+ });
+ });
+ });
+}, 'underlying abort() should not be called until underlying write() completes');
+
+promise_test(() => {
+ let resolveClose;
+ const ws = recordingWritableStream({
+ close() {
+ return new Promise(resolve => {
+ resolveClose = resolve;
+ });
+ }
+ });
+
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ writer.close();
+ const abortPromise = writer.abort();
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(ws.events, ['close'], 'abort should not be called while close is in-flight');
+ resolveClose();
+ return abortPromise.then(() => {
+ assert_array_equals(ws.events, ['close'], 'abort should not be called');
+ });
+ });
+ });
+}, 'underlying abort() should not be called if underlying close() has started');
+
+promise_test(t => {
+ let rejectClose;
+ let abortCalled = false;
+ const ws = new WritableStream({
+ close() {
+ return new Promise((resolve, reject) => {
+ rejectClose = reject;
+ });
+ },
+ abort() {
+ abortCalled = true;
+ }
+ });
+
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const closePromise = writer.close();
+ const abortPromise = writer.abort();
+ return flushAsyncEvents().then(() => {
+ assert_false(abortCalled, 'underlying abort should not be called while close is in-flight');
+ rejectClose(error1);
+ return promise_rejects_exactly(t, error1, abortPromise, 'abort should reject with the same reason').then(() => {
+ return promise_rejects_exactly(t, error1, closePromise, 'close should reject with the same reason');
+ }).then(() => {
+ assert_false(abortCalled, 'underlying abort should not be called after close completes');
+ });
+ });
+ });
+}, 'if underlying close() has started and then rejects, the abort() and close() promises should reject with the ' +
+ 'underlying close rejection reason');
+
+promise_test(t => {
+ let resolveWrite;
+ const ws = recordingWritableStream({
+ write() {
+ return new Promise(resolve => {
+ resolveWrite = resolve;
+ });
+ }
+ });
+
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ writer.write('a');
+ const closePromise = writer.close();
+ const abortPromise = writer.abort(error1);
+
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(ws.events, ['write', 'a'], 'abort should not be called while write is in-flight');
+ resolveWrite();
+ return abortPromise.then(() => {
+ assert_array_equals(ws.events, ['write', 'a', 'abort', error1], 'abort should be called after write completes');
+ return promise_rejects_exactly(t, error1, closePromise, 'promise returned by close() should be rejected');
+ });
+ });
+ });
+}, 'an abort() that happens during a write() should trigger the underlying abort() even with a close() queued');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write() {
+ return new Promise(() => {});
+ }
+ });
+
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ writer.write('a');
+ writer.abort(error1);
+ writer.releaseLock();
+ const writer2 = ws.getWriter();
+ return promise_rejects_exactly(t, error1, writer2.ready,
+ 'ready of the second writer should be rejected with error1');
+ });
+}, 'if a writer is created for a stream with a pending abort, its ready should be rejected with the abort error');
+
+promise_test(() => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const closePromise = writer.close();
+ const abortPromise = writer.abort();
+ const events = [];
+ return Promise.all([
+ closePromise.then(() => { events.push('close'); }),
+ abortPromise.then(() => { events.push('abort'); })
+ ]).then(() => {
+ assert_array_equals(events, ['close', 'abort']);
+ });
+ });
+}, 'writer close() promise should resolve before abort() promise');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write(chunk, controller) {
+ controller.error(error1);
+ return new Promise(() => {});
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ writer.write('a');
+ return promise_rejects_exactly(t, error1, writer.ready, 'writer.ready should reject');
+ });
+}, 'writer.ready should reject on controller error without waiting for underlying write');
+
+promise_test(t => {
+ let rejectWrite;
+ const ws = new WritableStream({
+ write() {
+ return new Promise((resolve, reject) => {
+ rejectWrite = reject;
+ });
+ }
+ });
+
+ let writePromise;
+ let abortPromise;
+
+ const events = [];
+
+ const writer = ws.getWriter();
+
+ writer.closed.catch(() => {
+ events.push('closed');
+ });
+
+ // Wait for ws to start
+ return flushAsyncEvents().then(() => {
+ writePromise = writer.write('a');
+ writePromise.catch(() => {
+ events.push('writePromise');
+ });
+
+ abortPromise = writer.abort(error1);
+ abortPromise.then(() => {
+ events.push('abortPromise');
+ });
+
+ const writePromise2 = writer.write('a');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writePromise2, 'writePromise2 must reject with the error from abort'),
+ promise_rejects_exactly(t, error1, writer.ready, 'writer.ready must reject with the error from abort'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(events, [], 'writePromise, abortPromise and writer.closed must not be rejected yet');
+
+ rejectWrite(error2);
+
+ return Promise.all([
+ promise_rejects_exactly(t, error2, writePromise,
+ 'writePromise must reject with the error returned from the sink\'s write method'),
+ abortPromise,
+ promise_rejects_exactly(t, error1, writer.closed,
+ 'writer.closed must reject with the error from abort'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(events, ['writePromise', 'abortPromise', 'closed'],
+ 'writePromise, abortPromise and writer.closed must settle');
+
+ const writePromise3 = writer.write('a');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writePromise3,
+ 'writePromise3 must reject with the error from abort'),
+ promise_rejects_exactly(t, error1, writer.ready,
+ 'writer.ready must be still rejected with the error indicating abort')
+ ]);
+ }).then(() => {
+ writer.releaseLock();
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, writer.ready,
+ 'writer.ready must be rejected with an error indicating release'),
+ promise_rejects_js(t, TypeError, writer.closed,
+ 'writer.closed must be rejected with an error indicating release')
+ ]);
+ });
+}, 'writer.abort() while there is an in-flight write, and then finish the write with rejection');
+
+promise_test(t => {
+ let resolveWrite;
+ let controller;
+ const ws = new WritableStream({
+ write(chunk, c) {
+ controller = c;
+ return new Promise(resolve => {
+ resolveWrite = resolve;
+ });
+ }
+ });
+
+ let writePromise;
+ let abortPromise;
+
+ const events = [];
+
+ const writer = ws.getWriter();
+
+ writer.closed.catch(() => {
+ events.push('closed');
+ });
+
+ // Wait for ws to start
+ return flushAsyncEvents().then(() => {
+ writePromise = writer.write('a');
+ writePromise.then(() => {
+ events.push('writePromise');
+ });
+
+ abortPromise = writer.abort(error1);
+ abortPromise.then(() => {
+ events.push('abortPromise');
+ });
+
+ const writePromise2 = writer.write('a');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writePromise2, 'writePromise2 must reject with the error from abort'),
+ promise_rejects_exactly(t, error1, writer.ready, 'writer.ready must reject with the error from abort'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(events, [], 'writePromise, abortPromise and writer.closed must not be fulfilled/rejected yet');
+
+ // This error is too late to change anything. abort() has already changed the stream state to 'erroring'.
+ controller.error(error2);
+
+ const writePromise3 = writer.write('a');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writePromise3,
+ 'writePromise3 must reject with the error from abort'),
+ promise_rejects_exactly(t, error1, writer.ready,
+ 'writer.ready must be still rejected with the error indicating abort'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(
+ events, [],
+ 'writePromise, abortPromise and writer.closed must not be fulfilled/rejected yet even after ' +
+ 'controller.error() call');
+
+ resolveWrite();
+
+ return Promise.all([
+ writePromise,
+ abortPromise,
+ promise_rejects_exactly(t, error1, writer.closed,
+ 'writer.closed must reject with the error from abort'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(events, ['writePromise', 'abortPromise', 'closed'],
+ 'writePromise, abortPromise and writer.closed must settle');
+
+ const writePromise4 = writer.write('a');
+
+ return Promise.all([
+ writePromise,
+ promise_rejects_exactly(t, error1, writePromise4,
+ 'writePromise4 must reject with the error from abort'),
+ promise_rejects_exactly(t, error1, writer.ready,
+ 'writer.ready must be still rejected with the error indicating abort')
+ ]);
+ }).then(() => {
+ writer.releaseLock();
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, writer.ready,
+ 'writer.ready must be rejected with an error indicating release'),
+ promise_rejects_js(t, TypeError, writer.closed,
+ 'writer.closed must be rejected with an error indicating release')
+ ]);
+ });
+}, 'writer.abort(), controller.error() while there is an in-flight write, and then finish the write');
+
+promise_test(t => {
+ let resolveClose;
+ let controller;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ },
+ close() {
+ return new Promise(resolve => {
+ resolveClose = resolve;
+ });
+ }
+ });
+
+ let closePromise;
+ let abortPromise;
+
+ const events = [];
+
+ const writer = ws.getWriter();
+
+ writer.closed.then(() => {
+ events.push('closed');
+ });
+
+ // Wait for ws to start
+ return flushAsyncEvents().then(() => {
+ closePromise = writer.close();
+ closePromise.then(() => {
+ events.push('closePromise');
+ });
+
+ abortPromise = writer.abort(error1);
+ abortPromise.then(() => {
+ events.push('abortPromise');
+ });
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, writer.close(),
+ 'writer.close() must reject with an error indicating already closing'),
+ promise_rejects_exactly(t, error1, writer.ready, 'writer.ready must reject with the error from abort'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(events, [], 'closePromise, abortPromise and writer.closed must not be fulfilled/rejected yet');
+
+ controller.error(error2);
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, writer.close(),
+ 'writer.close() must reject with an error indicating already closing'),
+ promise_rejects_exactly(t, error1, writer.ready,
+ 'writer.ready must be still rejected with the error indicating abort'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(
+ events, [],
+ 'closePromise, abortPromise and writer.closed must not be fulfilled/rejected yet even after ' +
+ 'controller.error() call');
+
+ resolveClose();
+
+ return Promise.all([
+ closePromise,
+ abortPromise,
+ writer.closed,
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'],
+ 'closedPromise, abortPromise and writer.closed must fulfill');
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, writer.close(),
+ 'writer.close() must reject with an error indicating already closing'),
+ promise_rejects_exactly(t, error1, writer.ready,
+ 'writer.ready must be still rejected with the error indicating abort')
+ ]);
+ }).then(() => {
+ writer.releaseLock();
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, writer.close(),
+ 'writer.close() must reject with an error indicating release'),
+ promise_rejects_js(t, TypeError, writer.ready,
+ 'writer.ready must be rejected with an error indicating release'),
+ promise_rejects_js(t, TypeError, writer.closed,
+ 'writer.closed must be rejected with an error indicating release')
+ ]);
+ });
+}, 'writer.abort(), controller.error() while there is an in-flight close, and then finish the close');
+
+promise_test(t => {
+ let resolveWrite;
+ let controller;
+ const ws = recordingWritableStream({
+ write(chunk, c) {
+ controller = c;
+ return new Promise(resolve => {
+ resolveWrite = resolve;
+ });
+ }
+ });
+
+ let writePromise;
+ let abortPromise;
+
+ const events = [];
+
+ const writer = ws.getWriter();
+
+ writer.closed.catch(() => {
+ events.push('closed');
+ });
+
+ // Wait for ws to start
+ return flushAsyncEvents().then(() => {
+ writePromise = writer.write('a');
+ writePromise.then(() => {
+ events.push('writePromise');
+ });
+
+ controller.error(error2);
+
+ const writePromise2 = writer.write('a');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error2, writePromise2,
+ 'writePromise2 must reject with the error passed to the controller\'s error method'),
+ promise_rejects_exactly(t, error2, writer.ready,
+ 'writer.ready must reject with the error passed to the controller\'s error method'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(events, [], 'writePromise and writer.closed must not be fulfilled/rejected yet');
+
+ abortPromise = writer.abort(error1);
+ abortPromise.catch(() => {
+ events.push('abortPromise');
+ });
+
+ const writePromise3 = writer.write('a');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error2, writePromise3,
+ 'writePromise3 must reject with the error passed to the controller\'s error method'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(
+ events, [],
+ 'writePromise and writer.closed must not be fulfilled/rejected yet even after writer.abort()');
+
+ resolveWrite();
+
+ return Promise.all([
+ promise_rejects_exactly(t, error2, abortPromise,
+ 'abort() must reject with the error passed to the controller\'s error method'),
+ promise_rejects_exactly(t, error2, writer.closed,
+ 'writer.closed must reject with the error passed to the controller\'s error method'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(events, ['writePromise', 'abortPromise', 'closed'],
+ 'writePromise, abortPromise and writer.closed must fulfill/reject');
+ assert_array_equals(ws.events, ['write', 'a'], 'sink abort() should not be called');
+
+ const writePromise4 = writer.write('a');
+
+ return Promise.all([
+ writePromise,
+ promise_rejects_exactly(t, error2, writePromise4,
+ 'writePromise4 must reject with the error passed to the controller\'s error method'),
+ promise_rejects_exactly(t, error2, writer.ready,
+ 'writer.ready must be still rejected with the error passed to the controller\'s error method')
+ ]);
+ }).then(() => {
+ writer.releaseLock();
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, writer.ready,
+ 'writer.ready must be rejected with an error indicating release'),
+ promise_rejects_js(t, TypeError, writer.closed,
+ 'writer.closed must be rejected with an error indicating release')
+ ]);
+ });
+}, 'controller.error(), writer.abort() while there is an in-flight write, and then finish the write');
+
+promise_test(t => {
+ let resolveClose;
+ let controller;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ },
+ close() {
+ return new Promise(resolve => {
+ resolveClose = resolve;
+ });
+ }
+ });
+
+ let closePromise;
+ let abortPromise;
+
+ const events = [];
+
+ const writer = ws.getWriter();
+
+ writer.closed.then(() => {
+ events.push('closed');
+ });
+
+ // Wait for ws to start
+ return flushAsyncEvents().then(() => {
+ closePromise = writer.close();
+ closePromise.then(() => {
+ events.push('closePromise');
+ });
+
+ controller.error(error2);
+
+ return flushAsyncEvents();
+ }).then(() => {
+ assert_array_equals(events, [], 'closePromise must not be fulfilled/rejected yet');
+
+ abortPromise = writer.abort(error1);
+ abortPromise.then(() => {
+ events.push('abortPromise');
+ });
+
+ return Promise.all([
+ promise_rejects_exactly(t, error2, writer.ready,
+ 'writer.ready must reject with the error passed to the controller\'s error method'),
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(
+ events, [],
+ 'closePromise and writer.closed must not be fulfilled/rejected yet even after writer.abort()');
+
+ resolveClose();
+
+ return Promise.all([
+ closePromise,
+ promise_rejects_exactly(t, error2, writer.ready,
+ 'writer.ready must be still rejected with the error passed to the controller\'s error method'),
+ writer.closed,
+ flushAsyncEvents()
+ ]);
+ }).then(() => {
+ assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'],
+ 'abortPromise, closePromise and writer.closed must fulfill/reject');
+ }).then(() => {
+ writer.releaseLock();
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, writer.ready,
+ 'writer.ready must be rejected with an error indicating release'),
+ promise_rejects_js(t, TypeError, writer.closed,
+ 'writer.closed must be rejected with an error indicating release')
+ ]);
+ });
+}, 'controller.error(), writer.abort() while there is an in-flight close, and then finish the close');
+
+promise_test(t => {
+ let resolveWrite;
+ const ws = new WritableStream({
+ write() {
+ return new Promise(resolve => {
+ resolveWrite = resolve;
+ });
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const writePromise = writer.write('a');
+ const closed = writer.closed;
+ const abortPromise = writer.abort();
+ writer.releaseLock();
+ resolveWrite();
+ return Promise.all([
+ writePromise,
+ abortPromise,
+ promise_rejects_js(t, TypeError, closed, 'closed should reject')]);
+ });
+}, 'releaseLock() while aborting should reject the original closed promise');
+
+// TODO(ricea): Consider removing this test if it is no longer useful.
+promise_test(t => {
+ let resolveWrite;
+ let resolveAbort;
+ let resolveAbortStarted;
+ const abortStarted = new Promise(resolve => {
+ resolveAbortStarted = resolve;
+ });
+ const ws = new WritableStream({
+ write() {
+ return new Promise(resolve => {
+ resolveWrite = resolve;
+ });
+ },
+ abort() {
+ resolveAbortStarted();
+ return new Promise(resolve => {
+ resolveAbort = resolve;
+ });
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const writePromise = writer.write('a');
+ const closed = writer.closed;
+ const abortPromise = writer.abort();
+ resolveWrite();
+ return abortStarted.then(() => {
+ writer.releaseLock();
+ assert_equals(writer.closed, closed, 'closed promise should not have changed');
+ resolveAbort();
+ return Promise.all([
+ writePromise,
+ abortPromise,
+ promise_rejects_js(t, TypeError, closed, 'closed should reject')]);
+ });
+ });
+}, 'releaseLock() during delayed async abort() should reject the writer.closed promise');
+
+promise_test(() => {
+ let resolveStart;
+ const ws = recordingWritableStream({
+ start() {
+ return new Promise(resolve => {
+ resolveStart = resolve;
+ });
+ }
+ });
+ const abortPromise = ws.abort('done');
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(ws.events, [], 'abort() should not be called during start()');
+ resolveStart();
+ return abortPromise.then(() => {
+ assert_array_equals(ws.events, ['abort', 'done'], 'abort() should be called after start() is done');
+ });
+ });
+}, 'sink abort() should not be called until sink start() is done');
+
+promise_test(() => {
+ let resolveStart;
+ let controller;
+ const ws = recordingWritableStream({
+ start(c) {
+ controller = c;
+ return new Promise(resolve => {
+ resolveStart = resolve;
+ });
+ }
+ });
+ const abortPromise = ws.abort('done');
+ controller.error(error1);
+ resolveStart();
+ return abortPromise.then(() =>
+ assert_array_equals(ws.events, ['abort', 'done'],
+ 'abort() should still be called if start() errors the controller'));
+}, 'if start attempts to error the controller after abort() has been called, then it should lose');
+
+promise_test(() => {
+ const ws = recordingWritableStream({
+ start() {
+ return Promise.reject(error1);
+ }
+ });
+ return ws.abort('done').then(() =>
+ assert_array_equals(ws.events, ['abort', 'done'], 'abort() should still be called if start() rejects'));
+}, 'stream abort() promise should still resolve if sink start() rejects');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+ const writerReady1 = writer.ready;
+ writer.abort(error1);
+ const writerReady2 = writer.ready;
+ assert_not_equals(writerReady1, writerReady2, 'abort() should replace the ready promise with a rejected one');
+ return Promise.all([writerReady1,
+ promise_rejects_exactly(t, error1, writerReady2, 'writerReady2 should reject')]);
+}, 'writer abort() during sink start() should replace the writer.ready promise synchronously');
+
+promise_test(t => {
+ const events = [];
+ const ws = recordingWritableStream();
+ const writer = ws.getWriter();
+ const writePromise1 = writer.write(1);
+ const abortPromise = writer.abort(error1);
+ const writePromise2 = writer.write(2);
+ const closePromise = writer.close();
+ writePromise1.catch(() => events.push('write1'));
+ abortPromise.then(() => events.push('abort'));
+ writePromise2.catch(() => events.push('write2'));
+ closePromise.catch(() => events.push('close'));
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writePromise1, 'first write() should reject'),
+ abortPromise,
+ promise_rejects_exactly(t, error1, writePromise2, 'second write() should reject'),
+ promise_rejects_exactly(t, error1, closePromise, 'close() should reject')
+ ])
+ .then(() => {
+ assert_array_equals(events, ['write2', 'write1', 'abort', 'close'],
+ 'promises should resolve in the standard order');
+ assert_array_equals(ws.events, ['abort', error1], 'underlying sink write() should not be called');
+ });
+}, 'promises returned from other writer methods should be rejected when writer abort() happens during sink start()');
+
+promise_test(t => {
+ let writeReject;
+ let controller;
+ const ws = new WritableStream({
+ write(chunk, c) {
+ controller = c;
+ return new Promise((resolve, reject) => {
+ writeReject = reject;
+ });
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const writePromise = writer.write('a');
+ const abortPromise = writer.abort();
+ controller.error(error1);
+ writeReject(error2);
+ return Promise.all([
+ promise_rejects_exactly(t, error2, writePromise, 'write() should reject with error2'),
+ abortPromise
+ ]);
+ });
+}, 'abort() should succeed despite rejection from write');
+
+promise_test(t => {
+ let closeReject;
+ let controller;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ },
+ close() {
+ return new Promise((resolve, reject) => {
+ closeReject = reject;
+ });
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const closePromise = writer.close();
+ const abortPromise = writer.abort();
+ controller.error(error1);
+ closeReject(error2);
+ return Promise.all([
+ promise_rejects_exactly(t, error2, closePromise, 'close() should reject with error2'),
+ promise_rejects_exactly(t, error2, abortPromise, 'abort() should reject with error2')
+ ]);
+ });
+}, 'abort() should be rejected with the rejection returned from close()');
+
+promise_test(t => {
+ let rejectWrite;
+ const ws = recordingWritableStream({
+ write() {
+ return new Promise((resolve, reject) => {
+ rejectWrite = reject;
+ });
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const writePromise = writer.write('1');
+ const abortPromise = writer.abort(error2);
+ rejectWrite(error1);
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writePromise, 'write should reject'),
+ abortPromise,
+ promise_rejects_exactly(t, error2, writer.closed, 'closed should reject with error2')
+ ]);
+ }).then(() => {
+ assert_array_equals(ws.events, ['write', '1', 'abort', error2], 'abort sink method should be called');
+ });
+}, 'a rejecting sink.write() should not prevent sink.abort() from being called');
+
+promise_test(() => {
+ const ws = recordingWritableStream({
+ start() {
+ return Promise.reject(error1);
+ }
+ });
+ return ws.abort(error2)
+ .then(() => {
+ assert_array_equals(ws.events, ['abort', error2]);
+ });
+}, 'when start errors after stream abort(), underlying sink abort() should be called anyway');
+
+promise_test(() => {
+ const ws = new WritableStream();
+ const abortPromise1 = ws.abort();
+ const abortPromise2 = ws.abort();
+ assert_equals(abortPromise1, abortPromise2, 'the promises must be the same');
+
+ return abortPromise1.then(
+ v => assert_equals(v, undefined, 'abort() should fulfill with undefined'));
+}, 'when calling abort() twice on the same stream, both should give the same promise that fulfills with undefined');
+
+promise_test(() => {
+ const ws = new WritableStream();
+ const abortPromise1 = ws.abort();
+
+ return abortPromise1.then(v1 => {
+ assert_equals(v1, undefined, 'first abort() should fulfill with undefined');
+
+ const abortPromise2 = ws.abort();
+ assert_not_equals(abortPromise2, abortPromise1, 'because we waited, the second promise should be a new promise');
+
+ return abortPromise2.then(v2 => {
+ assert_equals(v2, undefined, 'second abort() should fulfill with undefined');
+ });
+ });
+}, 'when calling abort() twice on the same stream, but sequentially so so there\'s no pending abort the second time, ' +
+ 'both should fulfill with undefined');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ start(c) {
+ c.error(error1);
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return promise_rejects_exactly(t, error1, writer.closed, 'writer.closed should reject').then(() => {
+ return writer.abort().then(
+ v => assert_equals(v, undefined, 'abort() should fulfill with undefined'));
+ });
+}, 'calling abort() on an errored stream should fulfill with undefined');
+
+promise_test(t => {
+ let controller;
+ let resolveWrite;
+ const ws = recordingWritableStream({
+ start(c) {
+ controller = c;
+ },
+ write() {
+ return new Promise(resolve => {
+ resolveWrite = resolve;
+ });
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const writePromise = writer.write('chunk');
+ controller.error(error1);
+ const abortPromise = writer.abort(error2);
+ resolveWrite();
+ return Promise.all([
+ writePromise,
+ promise_rejects_exactly(t, error1, abortPromise, 'abort() should reject')
+ ]).then(() => {
+ assert_array_equals(ws.events, ['write', 'chunk'], 'sink abort() should not be called');
+ });
+ });
+}, 'sink abort() should not be called if stream was erroring due to controller.error() before abort() was called');
+
+promise_test(t => {
+ let resolveWrite;
+ let size = 1;
+ const ws = recordingWritableStream({
+ write() {
+ return new Promise(resolve => {
+ resolveWrite = resolve;
+ });
+ }
+ }, {
+ size() {
+ return size;
+ },
+ highWaterMark: 1
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const writePromise1 = writer.write('chunk1');
+ size = NaN;
+ const writePromise2 = writer.write('chunk2');
+ const abortPromise = writer.abort(error2);
+ resolveWrite();
+ return Promise.all([
+ writePromise1,
+ promise_rejects_js(t, RangeError, writePromise2, 'second write() should reject'),
+ promise_rejects_js(t, RangeError, abortPromise, 'abort() should reject')
+ ]).then(() => {
+ assert_array_equals(ws.events, ['write', 'chunk1'], 'sink abort() should not be called');
+ });
+ });
+}, 'sink abort() should not be called if stream was erroring due to bad strategy before abort() was called');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ return ws.abort().then(() => {
+ const writer = ws.getWriter();
+ return writer.closed.then(t.unreached_func('closed promise should not fulfill'),
+ e => assert_equals(e, undefined, 'e should be undefined'));
+ });
+}, 'abort with no arguments should set the stored error to undefined');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ return ws.abort(undefined).then(() => {
+ const writer = ws.getWriter();
+ return writer.closed.then(t.unreached_func('closed promise should not fulfill'),
+ e => assert_equals(e, undefined, 'e should be undefined'));
+ });
+}, 'abort with an undefined argument should set the stored error to undefined');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ return ws.abort('string argument').then(() => {
+ const writer = ws.getWriter();
+ return writer.closed.then(t.unreached_func('closed promise should not fulfill'),
+ e => assert_equals(e, 'string argument', 'e should be \'string argument\''));
+ });
+}, 'abort with a string argument should set the stored error to that argument');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+ return promise_rejects_js(t, TypeError, ws.abort(), 'abort should reject')
+ .then(() => writer.ready);
+}, 'abort on a locked stream should reject');
diff --git a/test/fixtures/wpt/streams/writable-streams/bad-strategies.any.js b/test/fixtures/wpt/streams/writable-streams/bad-strategies.any.js
new file mode 100644
index 0000000..b180bae
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/bad-strategies.any.js
@@ -0,0 +1,95 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+const error1 = new Error('a unique string');
+error1.name = 'error1';
+
+test(() => {
+ assert_throws_exactly(error1, () => {
+ new WritableStream({}, {
+ get size() {
+ throw error1;
+ },
+ highWaterMark: 5
+ });
+ }, 'construction should re-throw the error');
+}, 'Writable stream: throwing strategy.size getter');
+
+test(() => {
+ assert_throws_js(TypeError, () => {
+ new WritableStream({}, { size: 'a string' });
+ });
+}, 'reject any non-function value for strategy.size');
+
+test(() => {
+ assert_throws_exactly(error1, () => {
+ new WritableStream({}, {
+ size() {
+ return 1;
+ },
+ get highWaterMark() {
+ throw error1;
+ }
+ });
+ }, 'construction should re-throw the error');
+}, 'Writable stream: throwing strategy.highWaterMark getter');
+
+test(() => {
+
+ for (const highWaterMark of [-1, -Infinity, NaN, 'foo', {}]) {
+ assert_throws_js(RangeError, () => {
+ new WritableStream({}, {
+ size() {
+ return 1;
+ },
+ highWaterMark
+ });
+ }, `construction should throw a RangeError for ${highWaterMark}`);
+ }
+}, 'Writable stream: invalid strategy.highWaterMark');
+
+promise_test(t => {
+ const ws = new WritableStream({}, {
+ size() {
+ throw error1;
+ },
+ highWaterMark: 5
+ });
+
+ const writer = ws.getWriter();
+
+ const p1 = promise_rejects_exactly(t, error1, writer.write('a'), 'write should reject with the thrown error');
+
+ const p2 = promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with the thrown error');
+
+ return Promise.all([p1, p2]);
+}, 'Writable stream: throwing strategy.size method');
+
+promise_test(() => {
+ const sizes = [NaN, -Infinity, Infinity, -1];
+ return Promise.all(sizes.map(size => {
+ const ws = new WritableStream({}, {
+ size() {
+ return size;
+ },
+ highWaterMark: 5
+ });
+
+ const writer = ws.getWriter();
+
+ return writer.write('a').then(() => assert_unreached('write must reject'), writeE => {
+ assert_equals(writeE.name, 'RangeError', `write must reject with a RangeError for ${size}`);
+
+ return writer.closed.then(() => assert_unreached('write must reject'), closedE => {
+ assert_equals(closedE, writeE, `closed should reject with the same error as write`);
+ });
+ });
+ }));
+}, 'Writable stream: invalid strategy.size return value');
+
+test(() => {
+ assert_throws_js(TypeError, () => new WritableStream(undefined, {
+ size: 'not a function',
+ highWaterMark: NaN
+ }), 'WritableStream constructor should throw a TypeError');
+}, 'Writable stream: invalid size beats invalid highWaterMark');
diff --git a/test/fixtures/wpt/streams/writable-streams/bad-underlying-sinks.any.js b/test/fixtures/wpt/streams/writable-streams/bad-underlying-sinks.any.js
new file mode 100644
index 0000000..0bfc036
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/bad-underlying-sinks.any.js
@@ -0,0 +1,204 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/test-utils.js
+// META: script=../resources/recording-streams.js
+'use strict';
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+test(() => {
+ assert_throws_exactly(error1, () => {
+ new WritableStream({
+ get start() {
+ throw error1;
+ }
+ });
+ }, 'constructor should throw same error as throwing start getter');
+
+ assert_throws_exactly(error1, () => {
+ new WritableStream({
+ start() {
+ throw error1;
+ }
+ });
+ }, 'constructor should throw same error as throwing start method');
+
+ assert_throws_js(TypeError, () => {
+ new WritableStream({
+ start: 'not a function or undefined'
+ });
+ }, 'constructor should throw TypeError when passed a non-function start property');
+
+ assert_throws_js(TypeError, () => {
+ new WritableStream({
+ start: { apply() {} }
+ });
+ }, 'constructor should throw TypeError when passed a non-function start property with an .apply method');
+}, 'start: errors in start cause WritableStream constructor to throw');
+
+promise_test(t => {
+
+ const ws = recordingWritableStream({
+ close() {
+ throw error1;
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return promise_rejects_exactly(t, error1, writer.close(), 'close() promise must reject with the thrown error')
+ .then(() => promise_rejects_exactly(t, error1, writer.ready, 'ready promise must reject with the thrown error'))
+ .then(() => promise_rejects_exactly(t, error1, writer.closed, 'closed promise must reject with the thrown error'))
+ .then(() => {
+ assert_array_equals(ws.events, ['close']);
+ });
+
+}, 'close: throwing method should cause writer close() and ready to reject');
+
+promise_test(t => {
+
+ const ws = recordingWritableStream({
+ close() {
+ return Promise.reject(error1);
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return promise_rejects_exactly(t, error1, writer.close(), 'close() promise must reject with the same error')
+ .then(() => promise_rejects_exactly(t, error1, writer.ready, 'ready promise must reject with the same error'))
+ .then(() => assert_array_equals(ws.events, ['close']));
+
+}, 'close: returning a rejected promise should cause writer close() and ready to reject');
+
+test(() => {
+ assert_throws_exactly(error1, () => new WritableStream({
+ get close() {
+ throw error1;
+ }
+ }), 'constructor should throw');
+}, 'close: throwing getter should cause constructor to throw');
+
+test(() => {
+ assert_throws_exactly(error1, () => new WritableStream({
+ get write() {
+ throw error1;
+ }
+ }), 'constructor should throw');
+}, 'write: throwing getter should cause write() and closed to reject');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write() {
+ throw error1;
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return promise_rejects_exactly(t, error1, writer.write('a'), 'write should reject with the thrown error')
+ .then(() => promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with the thrown error'));
+}, 'write: throwing method should cause write() and closed to reject');
+
+promise_test(t => {
+
+ let rejectSinkWritePromise;
+ const ws = recordingWritableStream({
+ write() {
+ return new Promise((r, reject) => {
+ rejectSinkWritePromise = reject;
+ });
+ }
+ });
+
+ return flushAsyncEvents().then(() => {
+ const writer = ws.getWriter();
+ const writePromise = writer.write('a');
+ rejectSinkWritePromise(error1);
+
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writePromise, 'writer write must reject with the same error'),
+ promise_rejects_exactly(t, error1, writer.ready, 'ready promise must reject with the same error')
+ ]);
+ })
+ .then(() => {
+ assert_array_equals(ws.events, ['write', 'a']);
+ });
+
+}, 'write: returning a promise that becomes rejected after the writer write() should cause writer write() and ready ' +
+ 'to reject');
+
+promise_test(t => {
+
+ const ws = recordingWritableStream({
+ write() {
+ if (ws.events.length === 2) {
+ return delay(0);
+ }
+
+ return Promise.reject(error1);
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ // Do not wait for this; we want to test the ready promise when the stream is "full" (desiredSize = 0), but if we wait
+ // then the stream will transition back to "empty" (desiredSize = 1)
+ writer.write('a');
+ const readyPromise = writer.ready;
+
+ return promise_rejects_exactly(t, error1, writer.write('b'), 'second write must reject with the same error').then(() => {
+ assert_equals(writer.ready, readyPromise,
+ 'the ready promise must not change, since the queue was full after the first write, so the pending one simply ' +
+ 'transitioned');
+ return promise_rejects_exactly(t, error1, writer.ready, 'ready promise must reject with the same error');
+ })
+ .then(() => assert_array_equals(ws.events, ['write', 'a', 'write', 'b']));
+
+}, 'write: returning a rejected promise (second write) should cause writer write() and ready to reject');
+
+test(() => {
+ assert_throws_js(TypeError, () => new WritableStream({
+ start: 'test'
+ }), 'constructor should throw');
+}, 'start: non-function start method');
+
+test(() => {
+ assert_throws_js(TypeError, () => new WritableStream({
+ write: 'test'
+ }), 'constructor should throw');
+}, 'write: non-function write method');
+
+test(() => {
+ assert_throws_js(TypeError, () => new WritableStream({
+ close: 'test'
+ }), 'constructor should throw');
+}, 'close: non-function close method');
+
+test(() => {
+ assert_throws_js(TypeError, () => new WritableStream({
+ abort: { apply() {} }
+ }), 'constructor should throw');
+}, 'abort: non-function abort method with .apply');
+
+test(() => {
+ assert_throws_exactly(error1, () => new WritableStream({
+ get abort() {
+ throw error1;
+ }
+ }), 'constructor should throw');
+}, 'abort: throwing getter should cause abort() and closed to reject');
+
+promise_test(t => {
+ const abortReason = new Error('different string');
+ const ws = new WritableStream({
+ abort() {
+ throw error1;
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return promise_rejects_exactly(t, error1, writer.abort(abortReason), 'abort should reject with the thrown error')
+ .then(() => promise_rejects_exactly(t, abortReason, writer.closed, 'closed should reject with abortReason'));
+}, 'abort: throwing method should cause abort() and closed to reject');
diff --git a/test/fixtures/wpt/streams/writable-streams/byte-length-queuing-strategy.any.js b/test/fixtures/wpt/streams/writable-streams/byte-length-queuing-strategy.any.js
new file mode 100644
index 0000000..9a61dd7
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/byte-length-queuing-strategy.any.js
@@ -0,0 +1,28 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+promise_test(t => {
+ let isDone = false;
+ const ws = new WritableStream(
+ {
+ write() {
+ return new Promise(resolve => {
+ t.step_timeout(() => {
+ isDone = true;
+ resolve();
+ }, 200);
+ });
+ },
+
+ close() {
+ assert_true(isDone, 'close is only called once the promise has been resolved');
+ }
+ },
+ new ByteLengthQueuingStrategy({ highWaterMark: 1024 * 16 })
+ );
+
+ const writer = ws.getWriter();
+ writer.write({ byteLength: 1024 });
+
+ return writer.close();
+}, 'Closing a writable stream with in-flight writes below the high water mark delays the close call properly');
diff --git a/test/fixtures/wpt/streams/writable-streams/close.any.js b/test/fixtures/wpt/streams/writable-streams/close.any.js
new file mode 100644
index 0000000..cf997ed
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/close.any.js
@@ -0,0 +1,470 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/test-utils.js
+// META: script=../resources/recording-streams.js
+'use strict';
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+const error2 = new Error('error2');
+error2.name = 'error2';
+
+promise_test(() => {
+ const ws = new WritableStream({
+ close() {
+ return 'Hello';
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ const closePromise = writer.close();
+ return closePromise.then(value => assert_equals(value, undefined, 'fulfillment value must be undefined'));
+}, 'fulfillment value of writer.close() call must be undefined even if the underlying sink returns a non-undefined ' +
+ 'value');
+
+promise_test(() => {
+ let controller;
+ let resolveClose;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ },
+ close() {
+ return new Promise(resolve => {
+ resolveClose = resolve;
+ });
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ const closePromise = writer.close();
+ return flushAsyncEvents().then(() => {
+ controller.error(error1);
+ return flushAsyncEvents();
+ }).then(() => {
+ resolveClose();
+ return Promise.all([
+ closePromise,
+ writer.closed,
+ flushAsyncEvents().then(() => writer.closed)]);
+ });
+}, 'when sink calls error asynchronously while sink close is in-flight, the stream should not become errored');
+
+promise_test(() => {
+ let controller;
+ const passedError = new Error('error me');
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ },
+ close() {
+ controller.error(passedError);
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return writer.close().then(() => writer.closed);
+}, 'when sink calls error synchronously while closing, the stream should not become errored');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ close() {
+ throw error1;
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return Promise.all([
+ writer.write('y'),
+ promise_rejects_exactly(t, error1, writer.close(), 'close() must reject with the error'),
+ promise_rejects_exactly(t, error1, writer.closed, 'closed must reject with the error')
+ ]);
+}, 'when the sink throws during close, and the close is requested while a write is still in-flight, the stream should ' +
+ 'become errored during the close');
+
+promise_test(() => {
+ const ws = new WritableStream({
+ write(chunk, controller) {
+ controller.error(error1);
+ return new Promise(() => {});
+ }
+ });
+
+ const writer = ws.getWriter();
+ writer.write('a');
+
+ return delay(0).then(() => {
+ writer.releaseLock();
+ });
+}, 'releaseLock on a stream with a pending write in which the stream has been errored');
+
+promise_test(() => {
+ let controller;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ },
+ close() {
+ controller.error(error1);
+ return new Promise(() => {});
+ }
+ });
+
+ const writer = ws.getWriter();
+ writer.close();
+
+ return delay(0).then(() => {
+ writer.releaseLock();
+ });
+}, 'releaseLock on a stream with a pending close in which controller.error() was called');
+
+promise_test(() => {
+ const ws = recordingWritableStream();
+
+ const writer = ws.getWriter();
+
+ return writer.ready.then(() => {
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
+
+ writer.close();
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be still 1');
+
+ return writer.ready.then(v => {
+ assert_equals(v, undefined, 'ready promise should be fulfilled with undefined');
+ assert_array_equals(ws.events, ['close'], 'write and abort should not be called');
+ });
+ });
+}, 'when close is called on a WritableStream in writable state, ready should return a fulfilled promise');
+
+promise_test(() => {
+ const ws = recordingWritableStream({
+ write() {
+ return new Promise(() => {});
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return writer.ready.then(() => {
+ writer.write('a');
+
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0');
+
+ let calledClose = false;
+ return Promise.all([
+ writer.ready.then(v => {
+ assert_equals(v, undefined, 'ready promise should be fulfilled with undefined');
+ assert_true(calledClose, 'ready should not be fulfilled before writer.close() is called');
+ assert_array_equals(ws.events, ['write', 'a'], 'sink abort() should not be called');
+ }),
+ flushAsyncEvents().then(() => {
+ writer.close();
+ calledClose = true;
+ })
+ ]);
+ });
+}, 'when close is called on a WritableStream in waiting state, ready promise should be fulfilled');
+
+promise_test(() => {
+ let asyncCloseFinished = false;
+ const ws = recordingWritableStream({
+ close() {
+ return flushAsyncEvents().then(() => {
+ asyncCloseFinished = true;
+ });
+ }
+ });
+
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ writer.write('a');
+
+ writer.close();
+
+ return writer.ready.then(v => {
+ assert_false(asyncCloseFinished, 'ready promise should be fulfilled before async close completes');
+ assert_equals(v, undefined, 'ready promise should be fulfilled with undefined');
+ assert_array_equals(ws.events, ['write', 'a', 'close'], 'sink abort() should not be called');
+ });
+ });
+}, 'when close is called on a WritableStream in waiting state, ready should be fulfilled immediately even if close ' +
+ 'takes a long time');
+
+promise_test(t => {
+ const rejection = { name: 'letter' };
+ const ws = new WritableStream({
+ close() {
+ return {
+ then(onFulfilled, onRejected) { onRejected(rejection); }
+ };
+ }
+ });
+ return promise_rejects_exactly(t, rejection, ws.getWriter().close(), 'close() should return a rejection');
+}, 'returning a thenable from close() should work');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const closePromise = writer.close();
+ const closedPromise = writer.closed;
+ writer.releaseLock();
+ return Promise.all([
+ closePromise,
+ promise_rejects_js(t, TypeError, closedPromise, '.closed promise should be rejected')
+ ]);
+ });
+}, 'releaseLock() should not change the result of sync close()');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ close() {
+ return flushAsyncEvents();
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const closePromise = writer.close();
+ const closedPromise = writer.closed;
+ writer.releaseLock();
+ return Promise.all([
+ closePromise,
+ promise_rejects_js(t, TypeError, closedPromise, '.closed promise should be rejected')
+ ]);
+ });
+}, 'releaseLock() should not change the result of async close()');
+
+promise_test(() => {
+ let resolveClose;
+ const ws = new WritableStream({
+ close() {
+ const promise = new Promise(resolve => {
+ resolveClose = resolve;
+ });
+ return promise;
+ }
+ });
+ const writer = ws.getWriter();
+ const closePromise = writer.close();
+ writer.releaseLock();
+ return delay(0).then(() => {
+ resolveClose();
+ return closePromise.then(() => {
+ assert_equals(ws.getWriter().desiredSize, 0, 'desiredSize should be 0');
+ });
+ });
+}, 'close() should set state to CLOSED even if writer has detached');
+
+promise_test(() => {
+ let resolveClose;
+ const ws = new WritableStream({
+ close() {
+ const promise = new Promise(resolve => {
+ resolveClose = resolve;
+ });
+ return promise;
+ }
+ });
+ const writer = ws.getWriter();
+ writer.close();
+ writer.releaseLock();
+ return delay(0).then(() => {
+ const abortingWriter = ws.getWriter();
+ const abortPromise = abortingWriter.abort();
+ abortingWriter.releaseLock();
+ resolveClose();
+ return abortPromise;
+ });
+}, 'the promise returned by async abort during close should resolve');
+
+// Though the order in which the promises are fulfilled or rejected is arbitrary, we're checking it for
+// interoperability. We can change the order as long as we file bugs on all implementers to update to the latest tests
+// to keep them interoperable.
+
+promise_test(() => {
+ const ws = new WritableStream({});
+
+ const writer = ws.getWriter();
+
+ const closePromise = writer.close();
+
+ const events = [];
+ return Promise.all([
+ closePromise.then(() => {
+ events.push('closePromise');
+ }),
+ writer.closed.then(() => {
+ events.push('closed');
+ })
+ ]).then(() => {
+ assert_array_equals(events, ['closePromise', 'closed'],
+ 'promises must fulfill/reject in the expected order');
+ });
+}, 'promises must fulfill/reject in the expected order on closure');
+
+promise_test(() => {
+ const ws = new WritableStream({});
+
+ // Wait until the WritableStream starts so that the close() call gets processed. Otherwise, abort() will be
+ // processed without waiting for completion of the close().
+ return delay(0).then(() => {
+ const writer = ws.getWriter();
+
+ const closePromise = writer.close();
+ const abortPromise = writer.abort(error1);
+
+ const events = [];
+ return Promise.all([
+ closePromise.then(() => {
+ events.push('closePromise');
+ }),
+ abortPromise.then(() => {
+ events.push('abortPromise');
+ }),
+ writer.closed.then(() => {
+ events.push('closed');
+ })
+ ]).then(() => {
+ assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'],
+ 'promises must fulfill/reject in the expected order');
+ });
+ });
+}, 'promises must fulfill/reject in the expected order on aborted closure');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ close() {
+ return Promise.reject(error1);
+ }
+ });
+
+ // Wait until the WritableStream starts so that the close() call gets processed.
+ return delay(0).then(() => {
+ const writer = ws.getWriter();
+
+ const closePromise = writer.close();
+ const abortPromise = writer.abort(error2);
+
+ const events = [];
+ closePromise.catch(() => events.push('closePromise'));
+ abortPromise.catch(() => events.push('abortPromise'));
+ writer.closed.catch(() => events.push('closed'));
+ return Promise.all([
+ promise_rejects_exactly(t, error1, closePromise,
+ 'closePromise must reject with the error returned from the sink\'s close method'),
+ promise_rejects_exactly(t, error1, abortPromise,
+ 'abortPromise must reject with the error returned from the sink\'s close method'),
+ promise_rejects_exactly(t, error2, writer.closed,
+ 'writer.closed must reject with error2')
+ ]).then(() => {
+ assert_array_equals(events, ['closePromise', 'abortPromise', 'closed'],
+ 'promises must fulfill/reject in the expected order');
+ });
+ });
+}, 'promises must fulfill/reject in the expected order on aborted and errored closure');
+
+promise_test(t => {
+ let resolveWrite;
+ let controller;
+ const ws = new WritableStream({
+ write(chunk, c) {
+ controller = c;
+ return new Promise(resolve => {
+ resolveWrite = resolve;
+ });
+ }
+ });
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ const writePromise = writer.write('c');
+ controller.error(error1);
+ const closePromise = writer.close();
+ let closeRejected = false;
+ closePromise.catch(() => {
+ closeRejected = true;
+ });
+ return flushAsyncEvents().then(() => {
+ assert_false(closeRejected);
+ resolveWrite();
+ return Promise.all([
+ writePromise,
+ promise_rejects_exactly(t, error1, closePromise, 'close() should reject')
+ ]).then(() => {
+ assert_true(closeRejected);
+ });
+ });
+ });
+}, 'close() should not reject until no sink methods are in flight');
+
+promise_test(() => {
+ const ws = new WritableStream();
+ const writer1 = ws.getWriter();
+ return writer1.close().then(() => {
+ writer1.releaseLock();
+ const writer2 = ws.getWriter();
+ const ready = writer2.ready;
+ assert_equals(ready.constructor, Promise);
+ return ready;
+ });
+}, 'ready promise should be initialised as fulfilled for a writer on a closed stream');
+
+promise_test(() => {
+ const ws = new WritableStream();
+ ws.close();
+ const writer = ws.getWriter();
+ return writer.closed;
+}, 'close() on a writable stream should work');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ ws.getWriter();
+ return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
+}, 'close() on a locked stream should reject');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ start(controller) {
+ controller.error(error1);
+ }
+ });
+ return promise_rejects_exactly(t, error1, ws.close(), 'close should reject with error1');
+}, 'close() on an erroring stream should reject');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ start(controller) {
+ controller.error(error1);
+ }
+ });
+ const writer = ws.getWriter();
+ return promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with the error').then(() => {
+ writer.releaseLock();
+ return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
+ });
+}, 'close() on an errored stream should reject');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+ return writer.close().then(() => {
+ return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
+ });
+}, 'close() on an closed stream should reject');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ close() {
+ return new Promise(() => {});
+ }
+ });
+
+ const writer = ws.getWriter();
+ writer.close();
+ writer.releaseLock();
+
+ return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
+}, 'close() on a stream with a pending close should reject');
diff --git a/test/fixtures/wpt/streams/writable-streams/constructor.any.js b/test/fixtures/wpt/streams/writable-streams/constructor.any.js
new file mode 100644
index 0000000..75eed2a
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/constructor.any.js
@@ -0,0 +1,155 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+const error2 = new Error('error2');
+error2.name = 'error2';
+
+promise_test(() => {
+ let controller;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ // Now error the stream after its construction.
+ controller.error(error1);
+
+ const writer = ws.getWriter();
+
+ assert_equals(writer.desiredSize, null, 'desiredSize should be null');
+ return writer.closed.catch(r => {
+ assert_equals(r, error1, 'ws should be errored by the passed error');
+ });
+}, 'controller argument should be passed to start method');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write(chunk, controller) {
+ controller.error(error1);
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return Promise.all([
+ writer.write('a'),
+ promise_rejects_exactly(t, error1, writer.closed, 'controller.error() in write() should error the stream')
+ ]);
+}, 'controller argument should be passed to write method');
+
+// Older versions of the standard had the controller argument passed to close(). It wasn't useful, and so has been
+// removed. This test remains to identify implementations that haven't been updated.
+promise_test(t => {
+ const ws = new WritableStream({
+ close(...args) {
+ t.step(() => {
+ assert_array_equals(args, [], 'no arguments should be passed to close');
+ });
+ }
+ });
+
+ return ws.getWriter().close();
+}, 'controller argument should not be passed to close method');
+
+promise_test(() => {
+ const ws = new WritableStream({}, {
+ highWaterMark: 1000,
+ size() { return 1; }
+ });
+
+ const writer = ws.getWriter();
+
+ assert_equals(writer.desiredSize, 1000, 'desiredSize should be 1000');
+ return writer.ready.then(v => {
+ assert_equals(v, undefined, 'ready promise should fulfill with undefined');
+ });
+}, 'highWaterMark should be reflected to desiredSize');
+
+promise_test(() => {
+ const ws = new WritableStream({}, {
+ highWaterMark: Infinity,
+ size() { return 0; }
+ });
+
+ const writer = ws.getWriter();
+
+ assert_equals(writer.desiredSize, Infinity, 'desiredSize should be Infinity');
+
+ return writer.ready;
+}, 'WritableStream should be writable and ready should fulfill immediately if the strategy does not apply ' +
+ 'backpressure');
+
+test(() => {
+ new WritableStream();
+}, 'WritableStream should be constructible with no arguments');
+
+test(() => {
+ const underlyingSink = { get start() { throw error1; } };
+ const queuingStrategy = { highWaterMark: 0, get size() { throw error2; } };
+
+ // underlyingSink is converted in prose in the method body, whereas queuingStrategy is done at the IDL layer.
+ // So the queuingStrategy exception should be encountered first.
+ assert_throws_exactly(error2, () => new WritableStream(underlyingSink, queuingStrategy));
+}, 'underlyingSink argument should be converted after queuingStrategy argument');
+
+test(() => {
+ const ws = new WritableStream({});
+
+ const writer = ws.getWriter();
+
+ assert_equals(typeof writer.write, 'function', 'writer should have a write method');
+ assert_equals(typeof writer.abort, 'function', 'writer should have an abort method');
+ assert_equals(typeof writer.close, 'function', 'writer should have a close method');
+
+ assert_equals(writer.desiredSize, 1, 'desiredSize should start at 1');
+
+ assert_not_equals(typeof writer.ready, 'undefined', 'writer should have a ready property');
+ assert_equals(typeof writer.ready.then, 'function', 'ready property should be thenable');
+ assert_not_equals(typeof writer.closed, 'undefined', 'writer should have a closed property');
+ assert_equals(typeof writer.closed.then, 'function', 'closed property should be thenable');
+}, 'WritableStream instances should have standard methods and properties');
+
+test(() => {
+ let WritableStreamDefaultController;
+ new WritableStream({
+ start(c) {
+ WritableStreamDefaultController = c.constructor;
+ }
+ });
+
+ assert_throws_js(TypeError, () => new WritableStreamDefaultController({}),
+ 'constructor should throw a TypeError exception');
+}, 'WritableStreamDefaultController constructor should throw');
+
+test(() => {
+ let WritableStreamDefaultController;
+ const stream = new WritableStream({
+ start(c) {
+ WritableStreamDefaultController = c.constructor;
+ }
+ });
+
+ assert_throws_js(TypeError, () => new WritableStreamDefaultController(stream),
+ 'constructor should throw a TypeError exception');
+}, 'WritableStreamDefaultController constructor should throw when passed an initialised WritableStream');
+
+test(() => {
+ const stream = new WritableStream();
+ const writer = stream.getWriter();
+ const WritableStreamDefaultWriter = writer.constructor;
+ writer.releaseLock();
+ assert_throws_js(TypeError, () => new WritableStreamDefaultWriter({}),
+ 'constructor should throw a TypeError exception');
+}, 'WritableStreamDefaultWriter should throw unless passed a WritableStream');
+
+test(() => {
+ const stream = new WritableStream();
+ const writer = stream.getWriter();
+ const WritableStreamDefaultWriter = writer.constructor;
+ assert_throws_js(TypeError, () => new WritableStreamDefaultWriter(stream),
+ 'constructor should throw a TypeError exception');
+}, 'WritableStreamDefaultWriter constructor should throw when stream argument is locked');
diff --git a/test/fixtures/wpt/streams/writable-streams/count-queuing-strategy.any.js b/test/fixtures/wpt/streams/writable-streams/count-queuing-strategy.any.js
new file mode 100644
index 0000000..30edb3e
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/count-queuing-strategy.any.js
@@ -0,0 +1,124 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+test(() => {
+ new WritableStream({}, new CountQueuingStrategy({ highWaterMark: 4 }));
+}, 'Can construct a writable stream with a valid CountQueuingStrategy');
+
+promise_test(() => {
+ const dones = Object.create(null);
+
+ const ws = new WritableStream(
+ {
+ write(chunk) {
+ return new Promise(resolve => {
+ dones[chunk] = resolve;
+ });
+ }
+ },
+ new CountQueuingStrategy({ highWaterMark: 0 })
+ );
+
+ const writer = ws.getWriter();
+ let writePromiseB;
+ let writePromiseC;
+
+ return Promise.resolve().then(() => {
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be initially 0');
+
+ const writePromiseA = writer.write('a');
+ assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 1st write()');
+
+ writePromiseB = writer.write('b');
+ assert_equals(writer.desiredSize, -2, 'desiredSize should be -2 after 2nd write()');
+
+ dones.a();
+ return writePromiseA;
+ }).then(() => {
+ assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after completing 1st write()');
+
+ dones.b();
+ return writePromiseB;
+ }).then(() => {
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after completing 2nd write()');
+
+ writePromiseC = writer.write('c');
+ assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 3rd write()');
+
+ dones.c();
+ return writePromiseC;
+ }).then(() => {
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after completing 3rd write()');
+ });
+}, 'Correctly governs the value of a WritableStream\'s state property (HWM = 0)');
+
+promise_test(() => {
+ const dones = Object.create(null);
+
+ const ws = new WritableStream(
+ {
+ write(chunk) {
+ return new Promise(resolve => {
+ dones[chunk] = resolve;
+ });
+ }
+ },
+ new CountQueuingStrategy({ highWaterMark: 4 })
+ );
+
+ const writer = ws.getWriter();
+ let writePromiseB;
+ let writePromiseC;
+ let writePromiseD;
+
+ return Promise.resolve().then(() => {
+ assert_equals(writer.desiredSize, 4, 'desiredSize should be initially 4');
+
+ const writePromiseA = writer.write('a');
+ assert_equals(writer.desiredSize, 3, 'desiredSize should be 3 after 1st write()');
+
+ writePromiseB = writer.write('b');
+ assert_equals(writer.desiredSize, 2, 'desiredSize should be 2 after 2nd write()');
+
+ writePromiseC = writer.write('c');
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1 after 3rd write()');
+
+ writePromiseD = writer.write('d');
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after 4th write()');
+
+ writer.write('e');
+ assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 5th write()');
+
+ writer.write('f');
+ assert_equals(writer.desiredSize, -2, 'desiredSize should be -2 after 6th write()');
+
+ writer.write('g');
+ assert_equals(writer.desiredSize, -3, 'desiredSize should be -3 after 7th write()');
+
+ dones.a();
+ return writePromiseA;
+ }).then(() => {
+ assert_equals(writer.desiredSize, -2, 'desiredSize should be -2 after completing 1st write()');
+
+ dones.b();
+ return writePromiseB;
+ }).then(() => {
+ assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after completing 2nd write()');
+
+ dones.c();
+ return writePromiseC;
+ }).then(() => {
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after completing 3rd write()');
+
+ writer.write('h');
+ assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 8th write()');
+
+ dones.d();
+ return writePromiseD;
+ }).then(() => {
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after completing 4th write()');
+
+ writer.write('i');
+ assert_equals(writer.desiredSize, -1, 'desiredSize should be -1 after 9th write()');
+ });
+}, 'Correctly governs the value of a WritableStream\'s state property (HWM = 4)');
diff --git a/test/fixtures/wpt/streams/writable-streams/error.any.js b/test/fixtures/wpt/streams/writable-streams/error.any.js
new file mode 100644
index 0000000..be986fc
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/error.any.js
@@ -0,0 +1,64 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+const error2 = new Error('error2');
+error2.name = 'error2';
+
+promise_test(t => {
+ const ws = new WritableStream({
+ start(controller) {
+ controller.error(error1);
+ }
+ });
+ return promise_rejects_exactly(t, error1, ws.getWriter().closed, 'stream should be errored');
+}, 'controller.error() should error the stream');
+
+test(() => {
+ let controller;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ ws.abort();
+ controller.error(error1);
+}, 'controller.error() on erroring stream should not throw');
+
+promise_test(t => {
+ let controller;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ controller.error(error1);
+ controller.error(error2);
+ return promise_rejects_exactly(t, error1, ws.getWriter().closed, 'first controller.error() should win');
+}, 'surplus calls to controller.error() should be a no-op');
+
+promise_test(() => {
+ let controller;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ return ws.abort().then(() => {
+ controller.error(error1);
+ });
+}, 'controller.error() on errored stream should not throw');
+
+promise_test(() => {
+ let controller;
+ const ws = new WritableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ return ws.getWriter().close().then(() => {
+ controller.error(error1);
+ });
+}, 'controller.error() on closed stream should not throw');
diff --git a/test/fixtures/wpt/streams/writable-streams/floating-point-total-queue-size.any.js b/test/fixtures/wpt/streams/writable-streams/floating-point-total-queue-size.any.js
new file mode 100644
index 0000000..8e77ba0
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/floating-point-total-queue-size.any.js
@@ -0,0 +1,87 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+// Due to the limitations of floating-point precision, the calculation of desiredSize sometimes gives different answers
+// than adding up the items in the queue would. It is important that implementations give the same result in these edge
+// cases so that developers do not come to depend on non-standard behaviour. See
+// https://github.com/whatwg/streams/issues/582 and linked issues for further discussion.
+
+promise_test(() => {
+ const writer = setupTestStream();
+
+ const writePromises = [
+ writer.write(2),
+ writer.write(Number.MAX_SAFE_INTEGER)
+ ];
+
+ assert_equals(writer.desiredSize, 0 - 2 - Number.MAX_SAFE_INTEGER,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (after writing two chunks)');
+
+ return Promise.all(writePromises).then(() => {
+ assert_equals(writer.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative');
+ });
+}, 'Floating point arithmetic must manifest near NUMBER.MAX_SAFE_INTEGER (total ends up positive)');
+
+promise_test(() => {
+ const writer = setupTestStream();
+
+ const writePromises = [
+ writer.write(1e-16),
+ writer.write(1)
+ ];
+
+ assert_equals(writer.desiredSize, 0 - 1e-16 - 1,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (after writing two chunks)');
+
+ return Promise.all(writePromises).then(() => {
+ assert_equals(writer.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative');
+ });
+}, 'Floating point arithmetic must manifest near 0 (total ends up positive, but clamped)');
+
+promise_test(() => {
+ const writer = setupTestStream();
+
+ const writePromises = [
+ writer.write(1e-16),
+ writer.write(1),
+ writer.write(2e-16)
+ ];
+
+ assert_equals(writer.desiredSize, 0 - 1e-16 - 1 - 2e-16,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (after writing three chunks)');
+
+ return Promise.all(writePromises).then(() => {
+ assert_equals(writer.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16 + 1 + 2e-16,
+ 'desiredSize must be calculated using floating-point arithmetic (after the three chunks have finished writing)');
+ });
+}, 'Floating point arithmetic must manifest near 0 (total ends up positive, and not clamped)');
+
+promise_test(() => {
+ const writer = setupTestStream();
+
+ const writePromises = [
+ writer.write(2e-16),
+ writer.write(1)
+ ];
+
+ assert_equals(writer.desiredSize, 0 - 2e-16 - 1,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (after writing two chunks)');
+
+ return Promise.all(writePromises).then(() => {
+ assert_equals(writer.desiredSize, 0 - 2e-16 - 1 + 2e-16 + 1,
+ 'desiredSize must be calculated using floating-point arithmetic (after the two chunks have finished writing)');
+ });
+}, 'Floating point arithmetic must manifest near 0 (total ends up zero)');
+
+function setupTestStream() {
+ const strategy = {
+ size(x) {
+ return x;
+ },
+ highWaterMark: 0
+ };
+
+ const ws = new WritableStream({}, strategy);
+
+ return ws.getWriter();
+}
diff --git a/test/fixtures/wpt/streams/writable-streams/general.any.js b/test/fixtures/wpt/streams/writable-streams/general.any.js
new file mode 100644
index 0000000..fdd10b2
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/general.any.js
@@ -0,0 +1,277 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+test(() => {
+ const ws = new WritableStream({});
+ const writer = ws.getWriter();
+ writer.releaseLock();
+
+ assert_throws_js(TypeError, () => writer.desiredSize, 'desiredSize should throw a TypeError');
+}, 'desiredSize on a released writer');
+
+test(() => {
+ const ws = new WritableStream({});
+
+ const writer = ws.getWriter();
+
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
+}, 'desiredSize initial value');
+
+promise_test(() => {
+ const ws = new WritableStream({});
+
+ const writer = ws.getWriter();
+
+ writer.close();
+
+ return writer.closed.then(() => {
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0');
+ });
+}, 'desiredSize on a writer for a closed stream');
+
+test(() => {
+ const ws = new WritableStream({
+ start(c) {
+ c.error();
+ }
+ });
+
+ const writer = ws.getWriter();
+ assert_equals(writer.desiredSize, null, 'desiredSize should be null');
+}, 'desiredSize on a writer for an errored stream');
+
+test(() => {
+ const ws = new WritableStream({});
+
+ const writer = ws.getWriter();
+ writer.close();
+ writer.releaseLock();
+
+ ws.getWriter();
+}, 'ws.getWriter() on a closing WritableStream');
+
+promise_test(() => {
+ const ws = new WritableStream({});
+
+ const writer = ws.getWriter();
+ return writer.close().then(() => {
+ writer.releaseLock();
+
+ ws.getWriter();
+ });
+}, 'ws.getWriter() on a closed WritableStream');
+
+test(() => {
+ const ws = new WritableStream({});
+
+ const writer = ws.getWriter();
+ writer.abort();
+ writer.releaseLock();
+
+ ws.getWriter();
+}, 'ws.getWriter() on an aborted WritableStream');
+
+promise_test(() => {
+ const ws = new WritableStream({
+ start(c) {
+ c.error();
+ }
+ });
+
+ const writer = ws.getWriter();
+ return writer.closed.then(
+ v => assert_unreached('writer.closed fulfilled unexpectedly with: ' + v),
+ () => {
+ writer.releaseLock();
+
+ ws.getWriter();
+ }
+ );
+}, 'ws.getWriter() on an errored WritableStream');
+
+promise_test(() => {
+ const ws = new WritableStream({});
+
+ const writer = ws.getWriter();
+ writer.releaseLock();
+
+ return writer.closed.then(
+ v => assert_unreached('writer.closed fulfilled unexpectedly with: ' + v),
+ closedRejection => {
+ assert_equals(closedRejection.name, 'TypeError', 'closed promise should reject with a TypeError');
+ return writer.ready.then(
+ v => assert_unreached('writer.ready fulfilled unexpectedly with: ' + v),
+ readyRejection => assert_equals(readyRejection, closedRejection,
+ 'ready promise should reject with the same error')
+ );
+ }
+ );
+}, 'closed and ready on a released writer');
+
+promise_test(t => {
+ let thisObject = null;
+ // Calls to Sink methods after the first are implicitly ignored. Only the first value that is passed to the resolver
+ // is used.
+ class Sink {
+ start() {
+ // Called twice
+ t.step(() => {
+ assert_equals(this, thisObject, 'start should be called as a method');
+ });
+ }
+
+ write() {
+ t.step(() => {
+ assert_equals(this, thisObject, 'write should be called as a method');
+ });
+ }
+
+ close() {
+ t.step(() => {
+ assert_equals(this, thisObject, 'close should be called as a method');
+ });
+ }
+
+ abort() {
+ t.step(() => {
+ assert_equals(this, thisObject, 'abort should be called as a method');
+ });
+ }
+ }
+
+ const theSink = new Sink();
+ thisObject = theSink;
+ const ws = new WritableStream(theSink);
+
+ const writer = ws.getWriter();
+
+ writer.write('a');
+ const closePromise = writer.close();
+
+ const ws2 = new WritableStream(theSink);
+ const writer2 = ws2.getWriter();
+ const abortPromise = writer2.abort();
+
+ return Promise.all([
+ closePromise,
+ abortPromise
+ ]);
+}, 'WritableStream should call underlying sink methods as methods');
+
+promise_test(t => {
+ function functionWithOverloads() {}
+ functionWithOverloads.apply = t.unreached_func('apply() should not be called');
+ functionWithOverloads.call = t.unreached_func('call() should not be called');
+ const underlyingSink = {
+ start: functionWithOverloads,
+ write: functionWithOverloads,
+ close: functionWithOverloads,
+ abort: functionWithOverloads
+ };
+ // Test start(), write(), close().
+ const ws1 = new WritableStream(underlyingSink);
+ const writer1 = ws1.getWriter();
+ writer1.write('a');
+ writer1.close();
+
+ // Test abort().
+ const abortError = new Error();
+ abortError.name = 'abort error';
+
+ const ws2 = new WritableStream(underlyingSink);
+ const writer2 = ws2.getWriter();
+ writer2.abort(abortError);
+
+ // Test abort() with a close underlying sink method present. (Historical; see
+ // https://github.com/whatwg/streams/issues/620#issuecomment-263483953 for what used to be
+ // tested here. But more coverage can't hurt.)
+ const ws3 = new WritableStream({
+ start: functionWithOverloads,
+ write: functionWithOverloads,
+ close: functionWithOverloads
+ });
+ const writer3 = ws3.getWriter();
+ writer3.abort(abortError);
+
+ return writer1.closed
+ .then(() => promise_rejects_exactly(t, abortError, writer2.closed, 'writer2.closed should be rejected'))
+ .then(() => promise_rejects_exactly(t, abortError, writer3.closed, 'writer3.closed should be rejected'));
+}, 'methods should not not have .apply() or .call() called');
+
+promise_test(() => {
+ const strategy = {
+ size() {
+ if (this !== undefined) {
+ throw new Error('size called as a method');
+ }
+ return 1;
+ }
+ };
+
+ const ws = new WritableStream({}, strategy);
+ const writer = ws.getWriter();
+ return writer.write('a');
+}, 'WritableStream\'s strategy.size should not be called as a method');
+
+promise_test(() => {
+ const ws = new WritableStream();
+ const writer1 = ws.getWriter();
+ assert_equals(undefined, writer1.releaseLock(), 'releaseLock() should return undefined');
+ const writer2 = ws.getWriter();
+ assert_equals(undefined, writer1.releaseLock(), 'no-op releaseLock() should return undefined');
+ // Calling releaseLock() on writer1 should not interfere with writer2. If it did, then the ready promise would be
+ // rejected.
+ return writer2.ready;
+}, 'redundant releaseLock() is no-op');
+
+promise_test(() => {
+ const events = [];
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ // Force the ready promise back to a pending state.
+ const writerPromise = writer.write('dummy');
+ const readyPromise = writer.ready.catch(() => events.push('ready'));
+ const closedPromise = writer.closed.catch(() => events.push('closed'));
+ writer.releaseLock();
+ return Promise.all([readyPromise, closedPromise]).then(() => {
+ assert_array_equals(events, ['ready', 'closed'], 'ready promise should fire before closed promise');
+ // Stop the writer promise hanging around after the test has finished.
+ return Promise.all([
+ writerPromise,
+ ws.abort()
+ ]);
+ });
+ });
+}, 'ready promise should fire before closed on releaseLock');
+
+test(() => {
+ class Subclass extends WritableStream {
+ extraFunction() {
+ return true;
+ }
+ }
+ assert_equals(
+ Object.getPrototypeOf(Subclass.prototype), WritableStream.prototype,
+ 'Subclass.prototype\'s prototype should be WritableStream.prototype');
+ assert_equals(Object.getPrototypeOf(Subclass), WritableStream,
+ 'Subclass\'s prototype should be WritableStream');
+ const sub = new Subclass();
+ assert_true(sub instanceof WritableStream,
+ 'Subclass object should be an instance of WritableStream');
+ assert_true(sub instanceof Subclass,
+ 'Subclass object should be an instance of Subclass');
+ const lockedGetter = Object.getOwnPropertyDescriptor(
+ WritableStream.prototype, 'locked').get;
+ assert_equals(lockedGetter.call(sub), sub.locked,
+ 'Subclass object should pass brand check');
+ assert_true(sub.extraFunction(),
+ 'extraFunction() should be present on Subclass object');
+}, 'Subclassing WritableStream should work');
+
+test(() => {
+ const ws = new WritableStream();
+ assert_false(ws.locked, 'stream should not be locked');
+ ws.getWriter();
+ assert_true(ws.locked, 'stream should be locked');
+}, 'the locked getter should return true if the stream has a writer');
diff --git a/test/fixtures/wpt/streams/writable-streams/properties.any.js b/test/fixtures/wpt/streams/writable-streams/properties.any.js
new file mode 100644
index 0000000..0f7f876
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/properties.any.js
@@ -0,0 +1,53 @@
+// META: global=window,worker,jsshell
+'use strict';
+
+const sinkMethods = {
+ start: {
+ length: 1,
+ trigger: () => Promise.resolve()
+ },
+ write: {
+ length: 2,
+ trigger: writer => writer.write()
+ },
+ close: {
+ length: 0,
+ trigger: writer => writer.close()
+ },
+ abort: {
+ length: 1,
+ trigger: writer => writer.abort()
+ }
+};
+
+for (const method in sinkMethods) {
+ const { length, trigger } = sinkMethods[method];
+
+ // Some semantic tests of how sink methods are called can be found in general.js, as well as in the test files
+ // specific to each method.
+ promise_test(() => {
+ let argCount;
+ const ws = new WritableStream({
+ [method](...args) {
+ argCount = args.length;
+ }
+ });
+ return Promise.resolve(trigger(ws.getWriter())).then(() => {
+ assert_equals(argCount, length, `${method} should be called with ${length} arguments`);
+ });
+ }, `sink method ${method} should be called with the right number of arguments`);
+
+ promise_test(() => {
+ let methodWasCalled = false;
+ function Sink() {}
+ Sink.prototype = {
+ [method]() {
+ methodWasCalled = true;
+ }
+ };
+ const ws = new WritableStream(new Sink());
+ return Promise.resolve(trigger(ws.getWriter())).then(() => {
+ assert_true(methodWasCalled, `${method} should be called`);
+ });
+ }, `sink method ${method} should be called even when it's located on the prototype chain`);
+}
diff --git a/test/fixtures/wpt/streams/writable-streams/reentrant-strategy.any.js b/test/fixtures/wpt/streams/writable-streams/reentrant-strategy.any.js
new file mode 100644
index 0000000..afde413
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/reentrant-strategy.any.js
@@ -0,0 +1,174 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/test-utils.js
+// META: script=../resources/recording-streams.js
+'use strict';
+
+// These tests exercise the pathological case of calling WritableStream* methods from within the strategy.size()
+// callback. This is not something any real code should ever do. Failures here indicate subtle deviations from the
+// standard that may affect real, non-pathological code.
+
+const error1 = { name: 'error1' };
+
+promise_test(() => {
+ let writer;
+ const strategy = {
+ size(chunk) {
+ if (chunk > 0) {
+ writer.write(chunk - 1);
+ }
+ return chunk;
+ }
+ };
+
+ const ws = recordingWritableStream({}, strategy);
+ writer = ws.getWriter();
+ return writer.write(2)
+ .then(() => {
+ assert_array_equals(ws.events, ['write', 0, 'write', 1, 'write', 2], 'writes should appear in order');
+ });
+}, 'writes should be written in the standard order');
+
+promise_test(() => {
+ let writer;
+ const events = [];
+ const strategy = {
+ size(chunk) {
+ events.push('size', chunk);
+ if (chunk > 0) {
+ writer.write(chunk - 1)
+ .then(() => events.push('writer.write done', chunk - 1));
+ }
+ return chunk;
+ }
+ };
+ const ws = new WritableStream({
+ write(chunk) {
+ events.push('sink.write', chunk);
+ }
+ }, strategy);
+ writer = ws.getWriter();
+ return writer.write(2)
+ .then(() => events.push('writer.write done', 2))
+ .then(() => flushAsyncEvents())
+ .then(() => {
+ assert_array_equals(events, ['size', 2, 'size', 1, 'size', 0,
+ 'sink.write', 0, 'sink.write', 1, 'writer.write done', 0,
+ 'sink.write', 2, 'writer.write done', 1,
+ 'writer.write done', 2],
+ 'events should happen in standard order');
+ });
+}, 'writer.write() promises should resolve in the standard order');
+
+promise_test(t => {
+ let controller;
+ const strategy = {
+ size() {
+ controller.error(error1);
+ return 1;
+ }
+ };
+ const ws = recordingWritableStream({
+ start(c) {
+ controller = c;
+ }
+ }, strategy);
+ const resolved = [];
+ const writer = ws.getWriter();
+ const readyPromise1 = writer.ready.then(() => resolved.push('ready1'));
+ const writePromise = promise_rejects_exactly(t, error1, writer.write(),
+ 'write() should reject with the error')
+ .then(() => resolved.push('write'));
+ const readyPromise2 = promise_rejects_exactly(t, error1, writer.ready, 'ready should reject with error1')
+ .then(() => resolved.push('ready2'));
+ const closedPromise = promise_rejects_exactly(t, error1, writer.closed, 'closed should reject with error1')
+ .then(() => resolved.push('closed'));
+ return Promise.all([readyPromise1, writePromise, readyPromise2, closedPromise])
+ .then(() => {
+ assert_array_equals(resolved, ['ready1', 'write', 'ready2', 'closed'],
+ 'promises should resolve in standard order');
+ assert_array_equals(ws.events, [], 'underlying sink write should not be called');
+ });
+}, 'controller.error() should work when called from within strategy.size()');
+
+promise_test(t => {
+ let writer;
+ const strategy = {
+ size() {
+ writer.close();
+ return 1;
+ }
+ };
+
+ const ws = recordingWritableStream({}, strategy);
+ writer = ws.getWriter();
+ return promise_rejects_js(t, TypeError, writer.write('a'), 'write() promise should reject')
+ .then(() => {
+ assert_array_equals(ws.events, ['close'], 'sink.write() should not be called');
+ });
+}, 'close() should work when called from within strategy.size()');
+
+promise_test(t => {
+ let writer;
+ const strategy = {
+ size() {
+ writer.abort(error1);
+ return 1;
+ }
+ };
+
+ const ws = recordingWritableStream({}, strategy);
+ writer = ws.getWriter();
+ return promise_rejects_exactly(t, error1, writer.write('a'), 'write() promise should reject')
+ .then(() => {
+ assert_array_equals(ws.events, ['abort', error1], 'sink.write() should not be called');
+ });
+}, 'abort() should work when called from within strategy.size()');
+
+promise_test(t => {
+ let writer;
+ const strategy = {
+ size() {
+ writer.releaseLock();
+ return 1;
+ }
+ };
+
+ const ws = recordingWritableStream({}, strategy);
+ writer = ws.getWriter();
+ const writePromise = promise_rejects_js(t, TypeError, writer.write('a'), 'write() promise should reject');
+ const readyPromise = promise_rejects_js(t, TypeError, writer.ready, 'ready promise should reject');
+ const closedPromise = promise_rejects_js(t, TypeError, writer.closed, 'closed promise should reject');
+ return Promise.all([writePromise, readyPromise, closedPromise])
+ .then(() => {
+ assert_array_equals(ws.events, [], 'sink.write() should not be called');
+ });
+}, 'releaseLock() should abort the write() when called within strategy.size()');
+
+promise_test(t => {
+ let writer1;
+ let ws;
+ let writePromise2;
+ let closePromise;
+ let closedPromise2;
+ const strategy = {
+ size(chunk) {
+ if (chunk > 0) {
+ writer1.releaseLock();
+ const writer2 = ws.getWriter();
+ writePromise2 = writer2.write(0);
+ closePromise = writer2.close();
+ closedPromise2 = writer2.closed;
+ }
+ return 1;
+ }
+ };
+ ws = recordingWritableStream({}, strategy);
+ writer1 = ws.getWriter();
+ const writePromise1 = promise_rejects_js(t, TypeError, writer1.write(1), 'write() promise should reject');
+ const readyPromise = promise_rejects_js(t, TypeError, writer1.ready, 'ready promise should reject');
+ const closedPromise1 = promise_rejects_js(t, TypeError, writer1.closed, 'closed promise should reject');
+ return Promise.all([writePromise1, readyPromise, closedPromise1, writePromise2, closePromise, closedPromise2])
+ .then(() => {
+ assert_array_equals(ws.events, ['write', 0, 'close'], 'sink.write() should only be called once');
+ });
+}, 'original reader should error when new reader is created within strategy.size()');
diff --git a/test/fixtures/wpt/streams/writable-streams/start.any.js b/test/fixtures/wpt/streams/writable-streams/start.any.js
new file mode 100644
index 0000000..02b5f2a
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/start.any.js
@@ -0,0 +1,163 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/test-utils.js
+// META: script=../resources/recording-streams.js
+'use strict';
+
+const error1 = { name: 'error1' };
+
+promise_test(() => {
+ let resolveStartPromise;
+ const ws = recordingWritableStream({
+ start() {
+ return new Promise(resolve => {
+ resolveStartPromise = resolve;
+ });
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
+ writer.write('a');
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after writer.write()');
+
+ // Wait and verify that write isn't called.
+ return flushAsyncEvents()
+ .then(() => {
+ assert_array_equals(ws.events, [], 'write should not be called until start promise resolves');
+ resolveStartPromise();
+ return writer.ready;
+ })
+ .then(() => assert_array_equals(ws.events, ['write', 'a'],
+ 'write should not be called until start promise resolves'));
+}, 'underlying sink\'s write should not be called until start finishes');
+
+promise_test(() => {
+ let resolveStartPromise;
+ const ws = recordingWritableStream({
+ start() {
+ return new Promise(resolve => {
+ resolveStartPromise = resolve;
+ });
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ writer.close();
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
+
+ // Wait and verify that write isn't called.
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(ws.events, [], 'close should not be called until start promise resolves');
+ resolveStartPromise();
+ return writer.closed;
+ });
+}, 'underlying sink\'s close should not be called until start finishes');
+
+test(() => {
+ const passedError = new Error('horrible things');
+
+ let writeCalled = false;
+ let closeCalled = false;
+ assert_throws_exactly(passedError, () => {
+ // recordingWritableStream cannot be used here because the exception in the
+ // constructor prevents assigning the object to a variable.
+ new WritableStream({
+ start() {
+ throw passedError;
+ },
+ write() {
+ writeCalled = true;
+ },
+ close() {
+ closeCalled = true;
+ }
+ });
+ }, 'constructor should throw passedError');
+ assert_false(writeCalled, 'write should not be called');
+ assert_false(closeCalled, 'close should not be called');
+}, 'underlying sink\'s write or close should not be called if start throws');
+
+promise_test(() => {
+ const ws = recordingWritableStream({
+ start() {
+ return Promise.reject();
+ }
+ });
+
+ // Wait and verify that write or close aren't called.
+ return flushAsyncEvents()
+ .then(() => assert_array_equals(ws.events, [], 'write and close should not be called'));
+}, 'underlying sink\'s write or close should not be invoked if the promise returned by start is rejected');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ start() {
+ return {
+ then(onFulfilled, onRejected) { onRejected(error1); }
+ };
+ }
+ });
+ return promise_rejects_exactly(t, error1, ws.getWriter().closed, 'closed promise should be rejected');
+}, 'returning a thenable from start() should work');
+
+promise_test(t => {
+ const ws = recordingWritableStream({
+ start(controller) {
+ controller.error(error1);
+ }
+ });
+ return promise_rejects_exactly(t, error1, ws.getWriter().write('a'), 'write() should reject with the error')
+ .then(() => {
+ assert_array_equals(ws.events, [], 'sink write() should not have been called');
+ });
+}, 'controller.error() during start should cause writes to fail');
+
+promise_test(t => {
+ let controller;
+ let resolveStart;
+ const ws = recordingWritableStream({
+ start(c) {
+ controller = c;
+ return new Promise(resolve => {
+ resolveStart = resolve;
+ });
+ }
+ });
+ const writer = ws.getWriter();
+ const writePromise = writer.write('a');
+ const closePromise = writer.close();
+ controller.error(error1);
+ resolveStart();
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writePromise, 'write() should fail'),
+ promise_rejects_exactly(t, error1, closePromise, 'close() should fail')
+ ]).then(() => {
+ assert_array_equals(ws.events, [], 'sink write() and close() should not have been called');
+ });
+}, 'controller.error() during async start should cause existing writes to fail');
+
+promise_test(t => {
+ const events = [];
+ const promises = [];
+ function catchAndRecord(promise, name) {
+ promises.push(promise.then(t.unreached_func(`promise ${name} should not resolve`),
+ () => {
+ events.push(name);
+ }));
+ }
+ const ws = new WritableStream({
+ start() {
+ return Promise.reject();
+ }
+ }, { highWaterMark: 0 });
+ const writer = ws.getWriter();
+ catchAndRecord(writer.ready, 'ready');
+ catchAndRecord(writer.closed, 'closed');
+ catchAndRecord(writer.write(), 'write');
+ return Promise.all(promises)
+ .then(() => {
+ assert_array_equals(events, ['ready', 'write', 'closed'], 'promises should reject in standard order');
+ });
+}, 'when start() rejects, writer promises should reject in standard order');
diff --git a/test/fixtures/wpt/streams/writable-streams/write.any.js b/test/fixtures/wpt/streams/writable-streams/write.any.js
new file mode 100644
index 0000000..e3defa8
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/write.any.js
@@ -0,0 +1,284 @@
+// META: global=window,worker,jsshell
+// META: script=../resources/test-utils.js
+// META: script=../resources/recording-streams.js
+'use strict';
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+const error2 = new Error('error2');
+error2.name = 'error2';
+
+function writeArrayToStream(array, writableStreamWriter) {
+ array.forEach(chunk => writableStreamWriter.write(chunk));
+ return writableStreamWriter.close();
+}
+
+promise_test(() => {
+ let storage;
+ const ws = new WritableStream({
+ start() {
+ storage = [];
+ },
+
+ write(chunk) {
+ return delay(0).then(() => storage.push(chunk));
+ },
+
+ close() {
+ return delay(0);
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ const input = [1, 2, 3, 4, 5];
+ return writeArrayToStream(input, writer)
+ .then(() => assert_array_equals(storage, input, 'correct data should be relayed to underlying sink'));
+}, 'WritableStream should complete asynchronous writes before close resolves');
+
+promise_test(() => {
+ const ws = recordingWritableStream();
+
+ const writer = ws.getWriter();
+
+ const input = [1, 2, 3, 4, 5];
+ return writeArrayToStream(input, writer)
+ .then(() => assert_array_equals(ws.events, ['write', 1, 'write', 2, 'write', 3, 'write', 4, 'write', 5, 'close'],
+ 'correct data should be relayed to underlying sink'));
+}, 'WritableStream should complete synchronous writes before close resolves');
+
+promise_test(() => {
+ const ws = new WritableStream({
+ write() {
+ return 'Hello';
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ const writePromise = writer.write('a');
+ return writePromise
+ .then(value => assert_equals(value, undefined, 'fulfillment value must be undefined'));
+}, 'fulfillment value of ws.write() call should be undefined even if the underlying sink returns a non-undefined ' +
+ 'value');
+
+promise_test(() => {
+ let resolveSinkWritePromise;
+ const ws = new WritableStream({
+ write() {
+ return new Promise(resolve => {
+ resolveSinkWritePromise = resolve;
+ });
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
+
+ return writer.ready.then(() => {
+ const writePromise = writer.write('a');
+ let writePromiseResolved = false;
+ assert_not_equals(resolveSinkWritePromise, undefined, 'resolveSinkWritePromise should not be undefined');
+
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0 after writer.write()');
+
+ return Promise.all([
+ writePromise.then(value => {
+ writePromiseResolved = true;
+ assert_equals(resolveSinkWritePromise, undefined, 'sinkWritePromise should be fulfilled before writePromise');
+
+ assert_equals(value, undefined, 'writePromise should be fulfilled with undefined');
+ }),
+ writer.ready.then(value => {
+ assert_equals(resolveSinkWritePromise, undefined, 'sinkWritePromise should be fulfilled before writer.ready');
+ assert_true(writePromiseResolved, 'writePromise should be fulfilled before writer.ready');
+
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1 again');
+
+ assert_equals(value, undefined, 'writePromise should be fulfilled with undefined');
+ }),
+ flushAsyncEvents().then(() => {
+ resolveSinkWritePromise();
+ resolveSinkWritePromise = undefined;
+ })
+ ]);
+ });
+}, 'WritableStream should transition to waiting until write is acknowledged');
+
+promise_test(t => {
+ let sinkWritePromiseRejectors = [];
+ const ws = new WritableStream({
+ write() {
+ const sinkWritePromise = new Promise((r, reject) => sinkWritePromiseRejectors.push(reject));
+ return sinkWritePromise;
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ assert_equals(writer.desiredSize, 1, 'desiredSize should be 1');
+
+ return writer.ready.then(() => {
+ const writePromise = writer.write('a');
+ assert_equals(sinkWritePromiseRejectors.length, 1, 'there should be 1 rejector');
+ assert_equals(writer.desiredSize, 0, 'desiredSize should be 0');
+
+ const writePromise2 = writer.write('b');
+ assert_equals(sinkWritePromiseRejectors.length, 1, 'there should be still 1 rejector');
+ assert_equals(writer.desiredSize, -1, 'desiredSize should be -1');
+
+ const closedPromise = writer.close();
+
+ assert_equals(writer.desiredSize, -1, 'desiredSize should still be -1');
+
+ return Promise.all([
+ promise_rejects_exactly(t, error1, closedPromise,
+ 'closedPromise should reject with the error returned from the sink\'s write method')
+ .then(() => assert_equals(sinkWritePromiseRejectors.length, 0,
+ 'sinkWritePromise should reject before closedPromise')),
+ promise_rejects_exactly(t, error1, writePromise,
+ 'writePromise should reject with the error returned from the sink\'s write method')
+ .then(() => assert_equals(sinkWritePromiseRejectors.length, 0,
+ 'sinkWritePromise should reject before writePromise')),
+ promise_rejects_exactly(t, error1, writePromise2,
+ 'writePromise2 should reject with the error returned from the sink\'s write method')
+ .then(() => assert_equals(sinkWritePromiseRejectors.length, 0,
+ 'sinkWritePromise should reject before writePromise2')),
+ flushAsyncEvents().then(() => {
+ sinkWritePromiseRejectors[0](error1);
+ sinkWritePromiseRejectors = [];
+ })
+ ]);
+ });
+}, 'when write returns a rejected promise, queued writes and close should be cleared');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write() {
+ throw error1;
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return promise_rejects_exactly(t, error1, writer.write('a'),
+ 'write() should reject with the error returned from the sink\'s write method')
+ .then(() => promise_rejects_js(t, TypeError, writer.close(), 'close() should be rejected'));
+}, 'when sink\'s write throws an error, the stream should become errored and the promise should reject');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ write(chunk, controller) {
+ controller.error(error1);
+ throw error2;
+ }
+ });
+
+ const writer = ws.getWriter();
+
+ return promise_rejects_exactly(t, error2, writer.write('a'),
+ 'write() should reject with the error returned from the sink\'s write method ')
+ .then(() => {
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writer.ready,
+ 'writer.ready must reject with the error passed to the controller'),
+ promise_rejects_exactly(t, error1, writer.closed,
+ 'writer.closed must reject with the error passed to the controller')
+ ]);
+ });
+}, 'writer.write(), ready and closed reject with the error passed to controller.error() made before sink.write' +
+ ' rejection');
+
+promise_test(() => {
+ const numberOfWrites = 1000;
+
+ let resolveFirstWritePromise;
+ let writeCount = 0;
+ const ws = new WritableStream({
+ write() {
+ ++writeCount;
+ if (!resolveFirstWritePromise) {
+ return new Promise(resolve => {
+ resolveFirstWritePromise = resolve;
+ });
+ }
+ return Promise.resolve();
+ }
+ });
+
+ const writer = ws.getWriter();
+ return writer.ready.then(() => {
+ for (let i = 1; i < numberOfWrites; ++i) {
+ writer.write('a');
+ }
+ const writePromise = writer.write('a');
+
+ assert_equals(writeCount, 1, 'should have called sink\'s write once');
+
+ resolveFirstWritePromise();
+
+ return writePromise
+ .then(() =>
+ assert_equals(writeCount, numberOfWrites, `should have called sink's write ${numberOfWrites} times`));
+ });
+}, 'a large queue of writes should be processed completely');
+
+promise_test(() => {
+ const stream = recordingWritableStream();
+ const w = stream.getWriter();
+ const WritableStreamDefaultWriter = w.constructor;
+ w.releaseLock();
+ const writer = new WritableStreamDefaultWriter(stream);
+ return writer.ready.then(() => {
+ writer.write('a');
+ assert_array_equals(stream.events, ['write', 'a'], 'write() should be passed to sink');
+ });
+}, 'WritableStreamDefaultWriter should work when manually constructed');
+
+promise_test(() => {
+ let thenCalled = false;
+ const ws = new WritableStream({
+ write() {
+ return {
+ then(onFulfilled) {
+ thenCalled = true;
+ onFulfilled();
+ }
+ };
+ }
+ });
+ return ws.getWriter().write('a').then(() => assert_true(thenCalled, 'thenCalled should be true'));
+}, 'returning a thenable from write() should work');
+
+promise_test(() => {
+ const stream = new WritableStream();
+ const writer = stream.getWriter();
+ const WritableStreamDefaultWriter = writer.constructor;
+ assert_throws_js(TypeError, () => new WritableStreamDefaultWriter(stream),
+ 'should not be able to construct on locked stream');
+ // If stream.[[writer]] no longer points to |writer| then the closed Promise
+ // won't work properly.
+ return Promise.all([writer.close(), writer.closed]);
+}, 'failing DefaultWriter constructor should not release an existing writer');
+
+promise_test(t => {
+ const ws = new WritableStream({
+ start() {
+ return Promise.reject(error1);
+ }
+ }, { highWaterMark: 0 });
+ const writer = ws.getWriter();
+ return Promise.all([
+ promise_rejects_exactly(t, error1, writer.ready, 'ready should be rejected'),
+ promise_rejects_exactly(t, error1, writer.write(), 'write() should be rejected')
+ ]);
+}, 'write() on a stream with HWM 0 should not cause the ready Promise to resolve');
+
+promise_test(t => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+ writer.releaseLock();
+ return promise_rejects_js(t, TypeError, writer.write(), 'write should reject');
+}, 'writing to a released writer should reject the returned promise');
diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json
new file mode 100644
index 0000000..8321737
--- /dev/null
+++ b/test/fixtures/wpt/versions.json
@@ -0,0 +1,18 @@
+{
+ "common": {
+ "commit": "d758aedae2d5a96604a07e915795ba00d2b4fe57",
+ "path": "common"
+ },
+ "interfaces": {
+ "commit": "4c7a0a83813006cad4d15f28e65ce66cd499db30",
+ "path": "interfaces"
+ },
+ "resources": {
+ "commit": "4235130a746794f27e70aa25ba24195d96aacd95",
+ "path": "resources"
+ },
+ "streams": {
+ "commit": "7e94a4bcb5bd6808e08ed8db46fa63751543db52",
+ "path": "streams"
+ }
+}
\ No newline at end of file
diff --git a/test/wpt/status/streams.json b/test/wpt/status/streams.json
new file mode 100644
index 0000000..b2419b3
--- /dev/null
+++ b/test/wpt/status/streams.json
@@ -0,0 +1,14 @@
+{
+ "piping/pipe-through.any.js": {
+ "fail": "Node does not perform a brand check for AbortSignal.prototype.aborted"
+ },
+ "queuing-strategies-size-function-per-global.window.js": {
+ "skip": "test requires an |