diff --git a/test/common/wpt.js b/test/common/wpt.js
index 9558b9a4e8d9178..50f83d1183db888 100644
--- a/test/common/wpt.js
+++ b/test/common/wpt.js
@@ -453,12 +453,19 @@ class WPTRunner {
this.scriptsModifier = modifier;
}
- fullInitScript(hasSubsetScript, locationSearchString) {
+ fullInitScript(hasSubsetScript, locationSearchString, metaTitle, relativePath) {
let { initScript } = this;
- if (hasSubsetScript || locationSearchString) {
+
+ if (hasSubsetScript || locationSearchString || !metaTitle) {
initScript = `${initScript}\n\n//===\nglobalThis.location ||= {};`;
}
+ if (metaTitle) {
+ initScript = `${initScript}\n\n//===\nglobalThis.META_TITLE = "${metaTitle}";`;
+ } else {
+ initScript = `${initScript}\n\n//===\nglobalThis.location.pathname = "/${relativePath}";`;
+ }
+
if (locationSearchString) {
initScript = `${initScript}\n\n//===\nglobalThis.location.search = "${locationSearchString}";`;
}
@@ -554,6 +561,7 @@ class WPTRunner {
const harnessPath = fixtures.path('wpt', 'resources', 'testharness.js');
const scriptsToRun = [];
let hasSubsetScript = false;
+ let needsGc = false;
// Scripts specified with the `// META: script=` header
if (meta.script) {
@@ -561,6 +569,9 @@ class WPTRunner {
if (script === '/common/subset-tests.js' || script === '/common/subset-tests-by-key.js') {
hasSubsetScript = true;
}
+ if (script === '/common/gc.js') {
+ needsGc = true;
+ }
const obj = {
filename: this.resource.toRealFilePath(relativePath, script),
code: this.resource.read(relativePath, script, false),
@@ -592,12 +603,13 @@ class WPTRunner {
testRelativePath: relativePath,
wptRunner: __filename,
wptPath: this.path,
- initScript: this.fullInitScript(hasSubsetScript, variant),
+ initScript: this.fullInitScript(hasSubsetScript, variant, meta.title, relativePath),
harness: {
code: fs.readFileSync(harnessPath, 'utf8'),
filename: harnessPath,
},
scriptsToRun,
+ needsGc,
},
});
this.workers.set(testFileName, worker);
@@ -749,11 +761,7 @@ class WPTRunner {
*/
resultCallback(filename, test, reportResult) {
const status = this.getTestStatus(test.status);
- const title = this.getTestTitle(filename);
- if (/^Untitled( \d+)?$/.test(test.name)) {
- test.name = `${title}${test.name.slice(8)}`;
- }
- console.log(`---- ${title} ----`);
+ console.log(`---- ${test.name} ----`);
if (status !== kPass) {
this.fail(filename, test, status, reportResult);
} else {
diff --git a/test/common/wpt/worker.js b/test/common/wpt/worker.js
index 34368ab5c5beffa..14d3d887aad5eb5 100644
--- a/test/common/wpt/worker.js
+++ b/test/common/wpt/worker.js
@@ -1,22 +1,29 @@
'use strict';
-const { runInThisContext } = require('vm');
+const { runInNewContext, runInThisContext } = require('vm');
+const { setFlagsFromString } = require('v8');
const { parentPort, workerData } = require('worker_threads');
const { ResourceLoader } = require(workerData.wptRunner);
const resource = new ResourceLoader(workerData.wptPath);
-global.self = global;
-global.GLOBAL = {
+if (workerData.needsGc) {
+ // See https://github.com/nodejs/node/issues/16595#issuecomment-340288680
+ setFlagsFromString('--expose-gc');
+ globalThis.gc = runInNewContext('gc');
+}
+
+globalThis.self = global;
+globalThis.GLOBAL = {
isWindow() { return false; },
isShadowRealm() { return false; },
};
-global.require = require;
+globalThis.require = require;
// This is a mock, because at the moment fetch is not implemented
// in Node.js, but some tests and harness depend on this to pull
// resources.
-global.fetch = function fetch(file) {
+globalThis.fetch = function fetch(file) {
return resource.read(workerData.testRelativePath, file, true);
};
diff --git a/test/fixtures/wpt/FileAPI/Blob-methods-from-detached-frame.html b/test/fixtures/wpt/FileAPI/Blob-methods-from-detached-frame.html
new file mode 100644
index 000000000000000..37efd5ed2016b76
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/Blob-methods-from-detached-frame.html
@@ -0,0 +1,59 @@
+
+
+
Blob methods from detached frame work as expected
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/FileAPI/BlobURL/cross-partition.tentative.https.html b/test/fixtures/wpt/FileAPI/BlobURL/cross-partition.tentative.https.html
new file mode 100644
index 000000000000000..c75ce07d054eb79
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/BlobURL/cross-partition.tentative.https.html
@@ -0,0 +1,276 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js
index 6c34d7e34b93f91..d16f760caeeb2d5 100644
--- a/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js
+++ b/test/fixtures/wpt/FileAPI/blob/Blob-constructor.any.js
@@ -311,7 +311,16 @@ test_blob(function() {
desc: "Passing a Float64Array as element of the blobParts array should work."
});
-
+test_blob(function() {
+ return new Blob([
+ new BigInt64Array([BigInt("0x5353415053534150")]),
+ new BigUint64Array([BigInt("0x5353415053534150")])
+ ]);
+}, {
+ expected: "PASSPASSPASSPASS",
+ type: "",
+ desc: "Passing BigInt typed arrays as elements of the blobParts array should work."
+});
var t_ports = async_test("Passing a FrozenArray as the blobParts array should work (FrozenArray).");
t_ports.step(function() {
diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-in-worker.worker.js b/test/fixtures/wpt/FileAPI/blob/Blob-in-worker.worker.js
index a67060e7b85effa..a0ca84551dd7cc3 100644
--- a/test/fixtures/wpt/FileAPI/blob/Blob-in-worker.worker.js
+++ b/test/fixtures/wpt/FileAPI/blob/Blob-in-worker.worker.js
@@ -1,14 +1,9 @@
importScripts("/resources/testharness.js");
-async_test(function() {
- var data = "TEST";
- var blob = new Blob([data], {type: "text/plain"});
- var reader = new FileReader();
- reader.onload = this.step_func_done(function() {
- assert_equals(reader.result, data);
- });
- reader.onerror = this.unreached_func("Unexpected error event");
- reader.readAsText(blob);
-}, "Create Blob in Worker");
+promise_test(async () => {
+ const data = "TEST";
+ const blob = new Blob([data], {type: "text/plain"});
+ assert_equals(await blob.text(), data);
+}, 'Create Blob in Worker');
done();
diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-stream-byob-crash.html b/test/fixtures/wpt/FileAPI/blob/Blob-stream-byob-crash.html
new file mode 100644
index 000000000000000..5992ed1396ca907
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/blob/Blob-stream-byob-crash.html
@@ -0,0 +1,11 @@
+
+
diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-stream-sync-xhr-crash.html b/test/fixtures/wpt/FileAPI/blob/Blob-stream-sync-xhr-crash.html
new file mode 100644
index 000000000000000..fe54fb615b1579a
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/blob/Blob-stream-sync-xhr-crash.html
@@ -0,0 +1,13 @@
+
+
diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-stream.any.js b/test/fixtures/wpt/FileAPI/blob/Blob-stream.any.js
index 792b6639c35a265..87710a171a97524 100644
--- a/test/fixtures/wpt/FileAPI/blob/Blob-stream.any.js
+++ b/test/fixtures/wpt/FileAPI/blob/Blob-stream.any.js
@@ -1,24 +1,26 @@
// META: title=Blob Stream
// META: script=../support/Blob.js
-// META: script=../../streams/resources/test-utils.js
+// META: script=/common/gc.js
'use strict';
// Helper function that triggers garbage collection while reading a chunk
// if perform_gc is true.
async function read_and_gc(reader, perform_gc) {
- const read_promise = reader.read();
- if (perform_gc)
- garbageCollect();
+ // Passing Uint8Array for byte streams; non-byte streams will simply ignore it
+ const read_promise = reader.read(new Uint8Array(64));
+ if (perform_gc) {
+ await garbageCollect();
+ }
return read_promise;
}
// Takes in a ReadableStream and reads from it until it is done, returning
// an array that contains the results of each read operation. If perform_gc
// is true, garbage collection is triggered while reading every chunk.
-async function read_all_chunks(stream, perform_gc = false) {
+async function read_all_chunks(stream, { perform_gc = false, mode } = {}) {
assert_true(stream instanceof ReadableStream);
assert_true('getReader' in stream);
- const reader = stream.getReader();
+ const reader = stream.getReader({ mode });
assert_true('read' in reader);
let read_value = await read_and_gc(reader, perform_gc);
@@ -65,8 +67,17 @@ promise_test(async() => {
let blob = new Blob([typed_arr]);
const stream = blob.stream();
blob = null;
- garbageCollect();
- const chunks = await read_all_chunks(stream, /*perform_gc=*/true);
+ await garbageCollect();
+ const chunks = await read_all_chunks(stream, { perform_gc: true });
assert_array_equals(chunks, input_arr);
}, "Blob.stream() garbage collection of blob shouldn't break stream" +
"consumption")
+
+promise_test(async () => {
+ const input_arr = [8, 241, 48, 123, 151];
+ const typed_arr = new Uint8Array(input_arr);
+ let blob = new Blob([typed_arr]);
+ const stream = blob.stream();
+ const chunks = await read_all_chunks(stream, { mode: "byob" });
+ assert_array_equals(chunks, input_arr);
+}, "Reading Blob.stream() with BYOB reader")
diff --git a/test/fixtures/wpt/FileAPI/fileReader.any.js b/test/fixtures/wpt/FileAPI/fileReader.any.js
new file mode 100644
index 000000000000000..2876dcb4ce314e3
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/fileReader.any.js
@@ -0,0 +1,59 @@
+// META: title=FileReader States
+
+'use strict';
+
+test(function () {
+ assert_true(
+ "FileReader" in globalThis,
+ "globalThis should have a FileReader property.",
+ );
+}, "FileReader interface object");
+
+test(function () {
+ var fileReader = new FileReader();
+ assert_true(fileReader instanceof FileReader);
+}, "no-argument FileReader constructor");
+
+var t_abort = async_test("FileReader States -- abort");
+t_abort.step(function () {
+ var fileReader = new FileReader();
+ assert_equals(fileReader.readyState, 0);
+ assert_equals(fileReader.readyState, FileReader.EMPTY);
+
+ var blob = new Blob();
+ fileReader.readAsArrayBuffer(blob);
+ assert_equals(fileReader.readyState, 1);
+ assert_equals(fileReader.readyState, FileReader.LOADING);
+
+ fileReader.onabort = this.step_func(function (e) {
+ assert_equals(fileReader.readyState, 2);
+ assert_equals(fileReader.readyState, FileReader.DONE);
+ t_abort.done();
+ });
+ fileReader.abort();
+ fileReader.onabort = this.unreached_func("abort event should fire sync");
+});
+
+var t_event = async_test("FileReader States -- events");
+t_event.step(function () {
+ var fileReader = new FileReader();
+
+ var blob = new Blob();
+ fileReader.readAsArrayBuffer(blob);
+
+ fileReader.onloadstart = this.step_func(function (e) {
+ assert_equals(fileReader.readyState, 1);
+ assert_equals(fileReader.readyState, FileReader.LOADING);
+ });
+
+ fileReader.onprogress = this.step_func(function (e) {
+ assert_equals(fileReader.readyState, 1);
+ assert_equals(fileReader.readyState, FileReader.LOADING);
+ });
+
+ fileReader.onloadend = this.step_func(function (e) {
+ assert_equals(fileReader.readyState, 2);
+ assert_equals(fileReader.readyState, FileReader.DONE);
+ t_event.done();
+ });
+});
diff --git a/test/fixtures/wpt/FileAPI/idlharness.any.js b/test/fixtures/wpt/FileAPI/idlharness.any.js
new file mode 100644
index 000000000000000..1744242b9f3ff17
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/idlharness.any.js
@@ -0,0 +1,19 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+// META: timeout=long
+
+'use strict';
+
+// https://w3c.github.io/FileAPI/
+
+idl_test(
+ ['FileAPI'],
+ ['dom', 'html', 'url'],
+ idl_array => {
+ idl_array.add_objects({
+ Blob: ['new Blob(["TEST"])'],
+ File: ['new File(["myFileBits"], "myFileName")'],
+ FileReader: ['new FileReader()']
+ });
+ }
+);
diff --git a/test/fixtures/wpt/FileAPI/idlharness.html b/test/fixtures/wpt/FileAPI/idlharness.html
index 5e0a43f80df3f85..45e8684f0027b2c 100644
--- a/test/fixtures/wpt/FileAPI/idlharness.html
+++ b/test/fixtures/wpt/FileAPI/idlharness.html
@@ -2,7 +2,7 @@
- File API automated IDL tests
+ File API automated IDL tests (requiring dom)
@@ -27,10 +27,7 @@
File API automated IDL tests
['dom', 'html', 'url'],
idl_array => {
idl_array.add_objects({
- Blob: ['new Blob(["TEST"])'],
- File: ['new File(["myFileBits"], "myFileName")'],
- FileList: ['document.querySelector("#fileChooser").files'],
- FileReader: ['new FileReader()']
+ FileList: ['document.querySelector("#fileChooser").files']
});
}
);
diff --git a/test/fixtures/wpt/FileAPI/idlharness.worker.js b/test/fixtures/wpt/FileAPI/idlharness.worker.js
index 786b7e4199fb45f..002aaed40a562e9 100644
--- a/test/fixtures/wpt/FileAPI/idlharness.worker.js
+++ b/test/fixtures/wpt/FileAPI/idlharness.worker.js
@@ -10,9 +10,6 @@ idl_test(
['dom', 'html', 'url'],
idl_array => {
idl_array.add_objects({
- Blob: ['new Blob(["TEST"])'],
- File: ['new File(["myFileBits"], "myFileName")'],
- FileReader: ['new FileReader()'],
FileReaderSync: ['new FileReaderSync()']
});
}
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.any.js
new file mode 100644
index 000000000000000..5b69f7ed9821ace
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.any.js
@@ -0,0 +1,81 @@
+// META: title=FileAPI Test: Blob Determining Encoding
+
+var t = async_test("Blob Determing Encoding with encoding argument");
+t.step(function() {
+ // string 'hello'
+ var data = [0xFE,0xFF,0x00,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16BE.")
+ }, reader);
+
+ reader.readAsText(blob, "UTF-16BE");
+});
+
+var t = async_test("Blob Determing Encoding with type attribute");
+t.step(function() {
+ var data = [0xFE,0xFF,0x00,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F];
+ var blob = new Blob([new Uint8Array(data)], {type:"text/plain;charset=UTF-16BE"});
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16BE.")
+ }, reader);
+
+ reader.readAsText(blob);
+});
+
+
+var t = async_test("Blob Determing Encoding with UTF-8 BOM");
+t.step(function() {
+ var data = [0xEF,0xBB,0xBF,0x68,0x65,0x6C,0x6C,0xC3,0xB6];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hellö", "The FileReader should read the blob with UTF-8.");
+ }, reader);
+
+ reader.readAsText(blob);
+});
+
+var t = async_test("Blob Determing Encoding without anything implying charset.");
+t.step(function() {
+ var data = [0x68,0x65,0x6C,0x6C,0xC3,0xB6];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hellö", "The FileReader should read the blob by default with UTF-8.");
+ }, reader);
+
+ reader.readAsText(blob);
+});
+
+var t = async_test("Blob Determing Encoding with UTF-16BE BOM");
+t.step(function() {
+ var data = [0xFE,0xFF,0x00,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16BE.");
+ }, reader);
+
+ reader.readAsText(blob);
+});
+
+var t = async_test("Blob Determing Encoding with UTF-16LE BOM");
+t.step(function() {
+ var data = [0xFF,0xFE,0x68,0x00,0x65,0x00,0x6C,0x00,0x6C,0x00,0x6F,0x00];
+ var blob = new Blob([new Uint8Array(data)]);
+ var reader = new FileReader();
+
+ reader.onloadend = t.step_func_done (function(event) {
+ assert_equals(this.result, "hello", "The FileReader should read the ArrayBuffer through UTF-16LE.");
+ }, reader);
+
+ reader.readAsText(blob);
+});
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js
new file mode 100644
index 000000000000000..fc71c6434812e22
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js
@@ -0,0 +1,17 @@
+// META: title=FileReader event handler attributes
+
+var attributes = [
+ "onloadstart",
+ "onprogress",
+ "onload",
+ "onabort",
+ "onerror",
+ "onloadend",
+];
+attributes.forEach(function(a) {
+ test(function() {
+ var reader = new FileReader();
+ assert_equals(reader[a], null,
+ "event handler attribute should initially be null");
+ }, "FileReader." + a + ": initial value");
+});
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js
new file mode 100644
index 000000000000000..4b19c69b425188a
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js
@@ -0,0 +1,81 @@
+// META: title=FileReader: starting new reads while one is in progress
+
+test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ reader.readAsText(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+ assert_throws_dom("InvalidStateError", function () {
+ reader.readAsText(blob_2)
+ })
+}, 'test FileReader InvalidStateError exception for readAsText');
+
+test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ reader.readAsDataURL(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+ assert_throws_dom("InvalidStateError", function () {
+ reader.readAsDataURL(blob_2)
+ })
+}, 'test FileReader InvalidStateError exception for readAsDataURL');
+
+test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ reader.readAsArrayBuffer(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+ assert_throws_dom("InvalidStateError", function () {
+ reader.readAsArrayBuffer(blob_2)
+ })
+}, 'test FileReader InvalidStateError exception for readAsArrayBuffer');
+
+async_test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ var triggered = false;
+ reader.onloadstart = this.step_func_done(function() {
+ assert_false(triggered, "Only one loadstart event should be dispatched");
+ triggered = true;
+ assert_equals(reader.readyState, FileReader.LOADING,
+ "readyState must be LOADING")
+ assert_throws_dom("InvalidStateError", function () {
+ reader.readAsArrayBuffer(blob_2)
+ })
+ });
+ reader.readAsArrayBuffer(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+}, 'test FileReader InvalidStateError exception in onloadstart event for readAsArrayBuffer');
+
+async_test(function() {
+ var blob_1 = new Blob(['TEST000000001'])
+ var blob_2 = new Blob(['TEST000000002'])
+ var reader = new FileReader();
+ reader.onloadend = this.step_func_done(function() {
+ assert_equals(reader.readyState, FileReader.DONE,
+ "readyState must be DONE")
+ reader.readAsArrayBuffer(blob_2)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+ });
+ reader.readAsArrayBuffer(blob_1)
+ assert_equals(reader.readyState, FileReader.LOADING, "readyState Must be LOADING")
+}, 'test FileReader no InvalidStateError exception in loadend event handler for readAsArrayBuffer');
+
+async_test(function() {
+ var blob_1 = new Blob([new Uint8Array(0x414141)]);
+ var blob_2 = new Blob(['TEST000000002']);
+ var reader = new FileReader();
+ reader.onloadstart = this.step_func(function() {
+ reader.abort();
+ reader.onloadstart = null;
+ reader.onloadend = this.step_func_done(function() {
+ assert_equals('TEST000000002', reader.result);
+ });
+ reader.readAsText(blob_2);
+ });
+ reader.readAsText(blob_1);
+}, 'test abort and restart in onloadstart event for readAsText');
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.any.js
new file mode 100644
index 000000000000000..c778ae55bb573bc
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.any.js
@@ -0,0 +1,38 @@
+// META: title=FileAPI Test: filereader_abort
+
+ test(function() {
+ var readerNoRead = new FileReader();
+ readerNoRead.abort();
+ assert_equals(readerNoRead.readyState, readerNoRead.EMPTY);
+ assert_equals(readerNoRead.result, null);
+ }, "Aborting before read");
+
+ promise_test(t => {
+ var blob = new Blob(["TEST THE ABORT METHOD"]);
+ var readerAbort = new FileReader();
+
+ var eventWatcher = new EventWatcher(t, readerAbort,
+ ['abort', 'loadstart', 'loadend', 'error', 'load']);
+
+ // EventWatcher doesn't let us inspect the state after the abort event,
+ // so add an extra event handler for that.
+ readerAbort.addEventListener('abort', t.step_func(e => {
+ assert_equals(readerAbort.readyState, readerAbort.DONE);
+ }));
+
+ readerAbort.readAsText(blob);
+ return eventWatcher.wait_for('loadstart')
+ .then(() => {
+ assert_equals(readerAbort.readyState, readerAbort.LOADING);
+ // 'abort' and 'loadend' events are dispatched synchronously, so
+ // call wait_for before calling abort.
+ var nextEvent = eventWatcher.wait_for(['abort', 'loadend']);
+ readerAbort.abort();
+ return nextEvent;
+ })
+ .then(() => {
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=24401
+ assert_equals(readerAbort.result, null);
+ assert_equals(readerAbort.readyState, readerAbort.DONE);
+ });
+ }, "Aborting after read");
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.any.js
new file mode 100644
index 000000000000000..9845962090132e6
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.any.js
@@ -0,0 +1,19 @@
+// META: title=FileAPI Test: filereader_error
+
+ async_test(function() {
+ var blob = new Blob(["TEST THE ERROR ATTRIBUTE AND ERROR EVENT"]);
+ var reader = new FileReader();
+ assert_equals(reader.error, null, "The error is null when no error occurred");
+
+ reader.onload = this.step_func(function(evt) {
+ assert_unreached("Should not dispatch the load event");
+ });
+
+ reader.onloadend = this.step_func(function(evt) {
+ assert_equals(reader.result, null, "The result is null");
+ this.done();
+ });
+
+ reader.readAsText(blob);
+ reader.abort();
+ });
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js
new file mode 100644
index 000000000000000..d06e3170782b7ca
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js
@@ -0,0 +1,23 @@
+// META: title=FileAPI Test: filereader_readAsArrayBuffer
+
+ async_test(function() {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function(evt) {
+ assert_equals(reader.result.byteLength, 4, "The byteLength is 4");
+ assert_true(reader.result instanceof ArrayBuffer, "The result is instanceof ArrayBuffer");
+ assert_equals(reader.readyState, reader.DONE);
+ this.done();
+ });
+
+ reader.onloadstart = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.onprogress = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.readAsArrayBuffer(blob);
+ });
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js
new file mode 100644
index 000000000000000..e69ff15e75b5903
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js
@@ -0,0 +1,23 @@
+// META: title=FileAPI Test: filereader_readAsBinaryString
+
+async_test(t => {
+ const blob = new Blob(["σ"]);
+ const reader = new FileReader();
+
+ reader.onload = t.step_func_done(() => {
+ assert_equals(typeof reader.result, "string", "The result is string");
+ assert_equals(reader.result.length, 2, "The result length is 2");
+ assert_equals(reader.result, "\xcf\x83", "The result is \xcf\x83");
+ assert_equals(reader.readyState, reader.DONE);
+ });
+
+ reader.onloadstart = t.step_func(() => {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.onprogress = t.step_func(() => {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.readAsBinaryString(blob);
+});
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.any.js
new file mode 100644
index 000000000000000..d6812121295beed
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.any.js
@@ -0,0 +1,42 @@
+// META: title=FileAPI Test: FileReader.readAsDataURL
+
+async_test(function(testCase) {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.DONE);
+ testCase.done();
+ });
+ reader.onloadstart = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+ reader.onprogress = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.readAsDataURL(blob);
+}, 'FileReader readyState during readAsDataURL');
+
+async_test(function(testCase) {
+ var blob = new Blob(["TEST"], { type: 'text/plain' });
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function() {
+ assert_equals(reader.result, "data:text/plain;base64,VEVTVA==");
+ testCase.done();
+ });
+ reader.readAsDataURL(blob);
+}, 'readAsDataURL result for Blob with specified MIME type');
+
+async_test(function(testCase) {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function() {
+ assert_equals(reader.result,
+ "data:application/octet-stream;base64,VEVTVA==");
+ testCase.done();
+ });
+ reader.readAsDataURL(blob);
+}, 'readAsDataURL result for Blob with unspecified MIME type');
\ No newline at end of file
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.any.js
new file mode 100644
index 000000000000000..4d0fa113931d342
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.any.js
@@ -0,0 +1,36 @@
+// META: title=FileAPI Test: filereader_readAsText
+
+ async_test(function() {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+
+ reader.onload = this.step_func(function(evt) {
+ assert_equals(typeof reader.result, "string", "The result is typeof string");
+ assert_equals(reader.result, "TEST", "The result is TEST");
+ this.done();
+ });
+
+ reader.onloadstart = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING, "The readyState");
+ });
+
+ reader.onprogress = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.readAsText(blob);
+ }, "readAsText should correctly read UTF-8.");
+
+ async_test(function() {
+ var blob = new Blob(["TEST"]);
+ var reader = new FileReader();
+ var reader_UTF16 = new FileReader();
+ reader_UTF16.onload = this.step_func(function(evt) {
+ // "TEST" in UTF-8 is 0x54 0x45 0x53 0x54.
+ // Decoded as utf-16 (little-endian), we get 0x4554 0x5453.
+ assert_equals(reader_UTF16.readyState, reader.DONE, "The readyState");
+ assert_equals(reader_UTF16.result, "\u4554\u5453", "The result is not TEST");
+ this.done();
+ });
+ reader_UTF16.readAsText(blob, "UTF-16");
+ }, "readAsText should correctly read UTF-16.");
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.any.js
new file mode 100644
index 000000000000000..3cb36ab999653bf
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.any.js
@@ -0,0 +1,19 @@
+// META: title=FileAPI Test: filereader_readystate
+
+ async_test(function() {
+ var blob = new Blob(["THIS TEST THE READYSTATE WHEN READ BLOB"]);
+ var reader = new FileReader();
+
+ assert_equals(reader.readyState, reader.EMPTY);
+
+ reader.onloadstart = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.LOADING);
+ });
+
+ reader.onloadend = this.step_func(function(evt) {
+ assert_equals(reader.readyState, reader.DONE);
+ this.done();
+ });
+
+ reader.readAsDataURL(blob);
+ });
diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.any.js
new file mode 100644
index 000000000000000..28c068bb349c3de
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.any.js
@@ -0,0 +1,82 @@
+// META: title=FileAPI Test: filereader_result
+
+ var blob, blob2;
+ setup(function() {
+ blob = new Blob(["This test the result attribute"]);
+ blob2 = new Blob(["This is a second blob"]);
+ });
+
+ async_test(function() {
+ var readText = new FileReader();
+ assert_equals(readText.result, null);
+
+ readText.onloadend = this.step_func(function(evt) {
+ assert_equals(typeof readText.result, "string", "The result type is string");
+ assert_equals(readText.result, "This test the result attribute", "The result is correct");
+ this.done();
+ });
+
+ readText.readAsText(blob);
+ }, "readAsText");
+
+ async_test(function() {
+ var readDataURL = new FileReader();
+ assert_equals(readDataURL.result, null);
+
+ readDataURL.onloadend = this.step_func(function(evt) {
+ assert_equals(typeof readDataURL.result, "string", "The result type is string");
+ assert_true(readDataURL.result.indexOf("VGhpcyB0ZXN0IHRoZSByZXN1bHQgYXR0cmlidXRl") != -1, "return the right base64 string");
+ this.done();
+ });
+
+ readDataURL.readAsDataURL(blob);
+ }, "readAsDataURL");
+
+ async_test(function() {
+ var readArrayBuffer = new FileReader();
+ assert_equals(readArrayBuffer.result, null);
+
+ readArrayBuffer.onloadend = this.step_func(function(evt) {
+ assert_true(readArrayBuffer.result instanceof ArrayBuffer, "The result is instanceof ArrayBuffer");
+ this.done();
+ });
+
+ readArrayBuffer.readAsArrayBuffer(blob);
+ }, "readAsArrayBuffer");
+
+ async_test(function() {
+ var readBinaryString = new FileReader();
+ assert_equals(readBinaryString.result, null);
+
+ readBinaryString.onloadend = this.step_func(function(evt) {
+ assert_equals(typeof readBinaryString.result, "string", "The result type is string");
+ assert_equals(readBinaryString.result, "This test the result attribute", "The result is correct");
+ this.done();
+ });
+
+ readBinaryString.readAsBinaryString(blob);
+ }, "readAsBinaryString");
+
+
+ for (let event of ['loadstart', 'progress']) {
+ for (let method of ['readAsText', 'readAsDataURL', 'readAsArrayBuffer', 'readAsBinaryString']) {
+ promise_test(async function(t) {
+ var reader = new FileReader();
+ assert_equals(reader.result, null, 'result is null before read');
+
+ var eventWatcher = new EventWatcher(t, reader,
+ [event, 'loadend']);
+
+ reader[method](blob);
+ assert_equals(reader.result, null, 'result is null after first read call');
+ await eventWatcher.wait_for(event);
+ assert_equals(reader.result, null, 'result is null during event');
+ await eventWatcher.wait_for('loadend');
+ assert_not_equals(reader.result, null);
+ reader[method](blob);
+ assert_equals(reader.result, null, 'result is null after second read call');
+ await eventWatcher.wait_for(event);
+ assert_equals(reader.result, null, 'result is null during second read event');
+ }, 'result is null during "' + event + '" event for ' + method);
+ }
+ }
diff --git a/test/fixtures/wpt/FileAPI/support/Blob.js b/test/fixtures/wpt/FileAPI/support/Blob.js
index 04069acd3ccbe71..2c2497468589189 100644
--- a/test/fixtures/wpt/FileAPI/support/Blob.js
+++ b/test/fixtures/wpt/FileAPI/support/Blob.js
@@ -1,6 +1,6 @@
'use strict'
-function test_blob(fn, expectations) {
+self.test_blob = (fn, expectations) => {
var expected = expectations.expected,
type = expectations.type,
desc = expectations.desc;
@@ -24,7 +24,7 @@ function test_blob(fn, expectations) {
});
}
-function test_blob_binary(fn, expectations) {
+self.test_blob_binary = (fn, expectations) => {
var expected = expectations.expected,
type = expectations.type,
desc = expectations.desc;
diff --git a/test/fixtures/wpt/FileAPI/support/empty-document.html b/test/fixtures/wpt/FileAPI/support/empty-document.html
new file mode 100644
index 000000000000000..b9cd130a07f77ee
--- /dev/null
+++ b/test/fixtures/wpt/FileAPI/support/empty-document.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/test/fixtures/wpt/FileAPI/support/send-file-formdata-helper.js b/test/fixtures/wpt/FileAPI/support/send-file-formdata-helper.js
index 53572ef36c8d1b1..53c8cca7e09b8e8 100644
--- a/test/fixtures/wpt/FileAPI/support/send-file-formdata-helper.js
+++ b/test/fixtures/wpt/FileAPI/support/send-file-formdata-helper.js
@@ -70,19 +70,21 @@ const formDataPostFileUploadTest = ({
}`,
);
- const asName = fileBaseName.replace(/[\r\n"]/g, encodeURIComponent);
+ const asValue = fileBaseName.replace(/\r\n?|\n/g, "\r\n");
+ const asName = asValue.replace(/[\r\n"]/g, encodeURIComponent);
+ const asFilename = fileBaseName.replace(/[\r\n"]/g, encodeURIComponent);
const expectedText = [
boundary,
'Content-Disposition: form-data; name="filename"',
"",
- fileBaseName,
+ asValue,
boundary,
`Content-Disposition: form-data; name="${asName}"`,
"",
"filename",
boundary,
`Content-Disposition: form-data; name="file"; ` +
- `filename="${asName}"`,
+ `filename="${asFilename}"`,
"Content-Type: text/plain",
"",
kTestChars,
diff --git a/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html b/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html
index 21b8c5bb1986d59..ce9d680709e0581 100644
--- a/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html
+++ b/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html
@@ -2,6 +2,7 @@
+
\ No newline at end of file
+
diff --git a/test/fixtures/wpt/FileAPI/url/url-format.any.js b/test/fixtures/wpt/FileAPI/url/url-format.any.js
index 33732fa61fc3ddd..69c51113e6b99b0 100644
--- a/test/fixtures/wpt/FileAPI/url/url-format.any.js
+++ b/test/fixtures/wpt/FileAPI/url/url-format.any.js
@@ -2,10 +2,16 @@
const blob = new Blob(['test']);
const file = new File(['test'], 'name');
-test(() => {
+test(t => {
const url_count = 5000;
let list = [];
+ t.add_cleanup(() => {
+ for (let url of list) {
+ URL.revokeObjectURL(url);
+ }
+ });
+
for (let i = 0; i < url_count; ++i)
list.push(URL.createObjectURL(blob));
diff --git a/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js b/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js
index 9bd8d383df4e1e1..54e6a3da5afe9e9 100644
--- a/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js
+++ b/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js
@@ -1,4 +1,5 @@
// META: script=resources/fetch-tests.js
+// META: script=/common/gc.js
function fetch_should_succeed(test, request) {
return fetch(request).then(response => response.text());
@@ -37,6 +38,24 @@ promise_test(t => {
});
}, 'Revoke blob URL after creating Request, will fetch');
+promise_test(async t => {
+ const blob_contents = 'test blob contents';
+ const blob = new Blob([blob_contents]);
+ const url = URL.createObjectURL(blob);
+ let request = new Request(url);
+
+ // Revoke the object URL. Request should take a reference to the blob as
+ // soon as it receives it in open(), so the request succeeds even though we
+ // revoke the URL before calling fetch().
+ URL.revokeObjectURL(url);
+
+ request = request.clone();
+ await garbageCollect();
+
+ const text = await fetch_should_succeed(t, request);
+ assert_equals(text, blob_contents);
+}, 'Revoke blob URL after creating Request, then clone Request, will fetch');
+
promise_test(function(t) {
const blob_contents = 'test blob contents';
const blob = new Blob([blob_contents]);
diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md
index 98b9369b88c322d..5f70de24b97b646 100644
--- a/test/fixtures/wpt/README.md
+++ b/test/fixtures/wpt/README.md
@@ -10,14 +10,13 @@ See [test/wpt](../../wpt/README.md) for information on how these tests are run.
Last update:
-- common: https://github.com/web-platform-tests/wpt/tree/03c5072aff/common
+- common: https://github.com/web-platform-tests/wpt/tree/dbd648158d/common
- console: https://github.com/web-platform-tests/wpt/tree/767ae35464/console
- dom/abort: https://github.com/web-platform-tests/wpt/tree/8fadb38120/dom/abort
- dom/events: https://github.com/web-platform-tests/wpt/tree/ab8999891c/dom/events
- encoding: https://github.com/web-platform-tests/wpt/tree/0c1b9d1622/encoding
- fetch/data-urls/resources: https://github.com/web-platform-tests/wpt/tree/7c79d998ff/fetch/data-urls/resources
-- FileAPI: https://github.com/web-platform-tests/wpt/tree/3b279420d4/FileAPI
-- FileAPI/file: https://github.com/web-platform-tests/wpt/tree/c01f637cca/FileAPI/file
+- FileAPI: https://github.com/web-platform-tests/wpt/tree/1e432c4550/FileAPI
- hr-time: https://github.com/web-platform-tests/wpt/tree/34cafd797e/hr-time
- html/webappapis/atob: https://github.com/web-platform-tests/wpt/tree/f267e1dca6/html/webappapis/atob
- html/webappapis/microtask-queuing: https://github.com/web-platform-tests/wpt/tree/2c5c3c4c27/html/webappapis/microtask-queuing
@@ -26,8 +25,8 @@ Last update:
- interfaces: https://github.com/web-platform-tests/wpt/tree/df731dab88/interfaces
- performance-timeline: https://github.com/web-platform-tests/wpt/tree/17ebc3aea0/performance-timeline
- resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing
-- resources: https://github.com/web-platform-tests/wpt/tree/fbf1e7d247/resources
-- streams: https://github.com/web-platform-tests/wpt/tree/9e5ef42bd3/streams
+- resources: https://github.com/web-platform-tests/wpt/tree/46e1750aa9/resources
+- streams: https://github.com/web-platform-tests/wpt/tree/819f38a9cf/streams
- url: https://github.com/web-platform-tests/wpt/tree/1eaeb0e178/url
- user-timing: https://github.com/web-platform-tests/wpt/tree/df24fb604e/user-timing
- wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/d8dbe6990b/wasm/jsapi
diff --git a/test/fixtures/wpt/common/custom-cors-response.js b/test/fixtures/wpt/common/custom-cors-response.js
new file mode 100644
index 000000000000000..be9c7ce3bdc3c9e
--- /dev/null
+++ b/test/fixtures/wpt/common/custom-cors-response.js
@@ -0,0 +1,32 @@
+const custom_cors_response = (payload, base_url) => {
+ base_url = base_url || new URL(location.href);
+
+ // Clone the given `payload` so that, as we modify it, we won't be mutating
+ // the caller's value in unexpected ways.
+ payload = Object.assign({}, payload);
+ payload.headers = payload.headers || {};
+ // Note that, in order to have out-of-the-box support for tests that don't
+ // call `setup({'allow_uncaught_exception': true})` we return a no-op JS
+ // payload. This approach will avoid hitting syntax errors if the resource is
+ // interpreted as script. Without this workaround, the SyntaxError would be
+ // caught by the test harness and trigger a test failure.
+ payload.content = payload.content || '/* custom-cors-response.js content */';
+ payload.status_code = payload.status_code || 200;
+
+ // Assume that we'll be doing a CORS-enabled fetch so we'll need to set ACAO.
+ const acao = "Access-Control-Allow-Origin";
+ if (!(acao in payload.headers)) {
+ payload.headers[acao] = '*';
+ }
+
+ if (!("Content-Type" in payload.headers)) {
+ payload.headers["Content-Type"] = "text/javascript";
+ }
+
+ let ret = new URL("/common/CustomCorsResponse.py", base_url);
+ for (const key in payload) {
+ ret.searchParams.append(key, JSON.stringify(payload[key]));
+ }
+
+ return ret;
+};
diff --git a/test/fixtures/wpt/common/dispatcher/README.md b/test/fixtures/wpt/common/dispatcher/README.md
new file mode 100644
index 000000000000000..cfaafb6e5d64968
--- /dev/null
+++ b/test/fixtures/wpt/common/dispatcher/README.md
@@ -0,0 +1,228 @@
+# `RemoteContext`: API for script execution in another context
+
+`RemoteContext` in `/common/dispatcher/dispatcher.js` provides an interface to
+execute JavaScript in another global object (page or worker, the "executor"),
+based on:
+
+- [WPT RFC 88: context IDs from uuid searchParams in URL](https://github.com/web-platform-tests/rfcs/pull/88),
+- [WPT RFC 89: execute_script](https://github.com/web-platform-tests/rfcs/pull/89) and
+- [WPT RFC 91: RemoteContext](https://github.com/web-platform-tests/rfcs/pull/91).
+
+Tests can send arbitrary javascript to executors to evaluate in its global
+object, like:
+
+```
+// injector.html
+const argOnLocalContext = ...;
+
+async function execute() {
+ window.open('executor.html?uuid=' + uuid);
+ const ctx = new RemoteContext(uuid);
+ await ctx.execute_script(
+ (arg) => functionOnRemoteContext(arg),
+ [argOnLocalContext]);
+};
+```
+
+and on executor:
+
+```
+// executor.html
+function functionOnRemoteContext(arg) { ... }
+
+const uuid = new URLSearchParams(window.location.search).get('uuid');
+const executor = new Executor(uuid);
+```
+
+For concrete examples, see
+[events.html](../../html/browsers/browsing-the-web/back-forward-cache/events.html)
+and
+[executor.html](../../html/browsers/browsing-the-web/back-forward-cache/resources/executor.html)
+in back-forward cache tests.
+
+Note that `executor*` files under `/common/dispatcher/` are NOT for
+`RemoteContext.execute_script()`. Use `remote-executor.html` instead.
+
+This is universal and avoids introducing many specific `XXX-helper.html`
+resources.
+Moreover, tests are easier to read, because the whole logic of the test can be
+defined in a single file.
+
+## `new RemoteContext(uuid)`
+
+- `uuid` is a UUID string that identifies the remote context and should match
+ with the `uuid` parameter of the URL of the remote context.
+- Callers should create the remote context outside this constructor (e.g.
+ `window.open('executor.html?uuid=' + uuid)`).
+
+## `RemoteContext.execute_script(fn, args)`
+
+- `fn` is a JavaScript function to execute on the remote context, which is
+ converted to a string using `toString()` and sent to the remote context.
+- `args` is null or an array of arguments to pass to the function on the
+ remote context. Arguments are passed as JSON.
+- If the return value of `fn` when executed in the remote context is a promise,
+ the promise returned by `execute_script` resolves to the resolved value of
+ that promise. Otherwise the `execute_script` promise resolves to the return
+ value of `fn`.
+
+Note that `fn` is evaluated on the remote context (`executor.html` in the
+example above), while `args` are evaluated on the caller context
+(`injector.html`) and then passed to the remote context.
+
+## Return value of injected functions and `execute_script()`
+
+If the return value of the injected function when executed in the remote
+context is a promise, the promise returned by `execute_script` resolves to the
+resolved value of that promise. Otherwise the `execute_script` promise resolves
+to the return value of the function.
+
+When the return value of an injected script is a Promise, it should be resolved
+before any navigation starts on the remote context. For example, it shouldn't
+be resolved after navigating out and navigating back to the page again.
+It's fine to create a Promise to be resolved after navigations, if it's not the
+return value of the injected function.
+
+## Calling timing of `execute_script()`
+
+When `RemoteContext.execute_script()` is called when the remote context is not
+active (for example before it is created, before navigation to the page, or
+during the page is in back-forward cache), the injected script is evaluated
+after the remote context becomes active.
+
+Multiple calls to `RemoteContext.execute_script()` will result in multiple scripts
+being executed in remote context and ordering will be maintained.
+
+## Errors from `execute_script()`
+
+Errors from `execute_script()` will result in promise rejections, so it is
+important to await the result. This can be `await ctx.execute_script(...)` for
+every call but if there are multiple scripts to executed, it may be preferable
+to wait on them in parallel to avoid incurring full round-trip time for each,
+e.g.
+
+```js
+await Promise.all(
+ ctx1.execute_script(...),
+ ctx1.execute_script(...),
+ ctx2.execute_script(...),
+ ctx2.execute_script(...),
+ ...
+)
+```
+
+## Evaluation timing of injected functions
+
+The script injected by `RemoteContext.execute_script()` can be evaluated any
+time during the remote context is active.
+For example, even before DOMContentLoaded events or even during navigation.
+It's the responsibility of test-specific code/helpers to ensure evaluation
+timing constraints (which can be also test-specific), if any needed.
+
+### Ensuring evaluation timing around page load
+
+For example, to ensure that injected functions (`mainFunction` below) are
+evaluated after the first `pageshow` event, we can use pure JavaScript code
+like below:
+
+```
+// executor.html
+window.pageShowPromise = new Promise(resolve =>
+ window.addEventListener('pageshow', resolve, {once: true}));
+
+
+// injector.html
+const waitForPageShow = async () => {
+ while (!window.pageShowPromise) {
+ await new Promise(resolve => setTimeout(resolve, 100));
+ }
+ await window.pageShowPromise;
+};
+
+await ctx.execute(waitForPageShow);
+await ctx.execute(mainFunction);
+```
+
+### Ensuring evaluation timing around navigation out/unloading
+
+It can be important to ensure there are no injected functions nor code behind
+`RemoteContext` (such as Fetch APIs accessing server-side stash) running after
+navigation is initiated, for example in the case of back-forward cache testing.
+
+To ensure this,
+
+- Do not call the next `RemoteContext.execute()` for the remote context after
+ triggering the navigation, until we are sure that the remote context is not
+ active (e.g. after we confirm that the new page is loaded).
+- Call `Executor.suspend(callback)` synchronously within the injected script.
+ This suspends executor-related code, and calls `callback` when it is ready
+ to start navigation.
+
+The code on the injector side would be like:
+
+```
+// injector.html
+await ctx.execute_script(() => {
+ executor.suspend(() => {
+ location.href = 'new-url.html';
+ });
+});
+```
+
+## Future Work: Possible integration with `test_driver`
+
+Currently `RemoteContext` is implemented by JavaScript and WPT-server-side
+stash, and not integrated with `test_driver` nor `testharness`.
+There is a proposal of `test_driver`-integrated version (see the RFCs listed
+above).
+
+The API semantics and guidelines in this document are designed to be applicable
+to both the current stash-based `RemoteContext` and `test_driver`-based
+version, and thus the tests using `RemoteContext` will be migrated with minimum
+modifications (mostly in `/common/dispatcher/dispatcher.js` and executors), for
+example in a
+[draft CL](https://chromium-review.googlesource.com/c/chromium/src/+/3082215/).
+
+
+# `send()`/`receive()` Message passing APIs
+
+`dispatcher.js` (and its server-side backend `dispatcher.py`) provides a
+universal queue-based message passing API.
+Each queue is identified by a UUID, and accessed via the following APIs:
+
+- `send(uuid, message)` pushes a string `message` to the queue `uuid`.
+- `receive(uuid)` pops the first item from the queue `uuid`.
+- `showRequestHeaders(origin, uuid)` and
+ `cacheableShowRequestHeaders(origin, uuid)` return URLs, that push request
+ headers to the queue `uuid` upon fetching.
+
+It works cross-origin, and even access different browser context groups.
+
+Messages are queued, this means one doesn't need to wait for the receiver to
+listen, before sending the first message
+(but still need to wait for the resolution of the promise returned by `send()`
+to ensure the order between `send()`s).
+
+## Executors
+
+Similar to `RemoteContext.execute_script()`, `send()`/`receive()` can be used
+for sending arbitrary javascript to be evaluated in another page or worker.
+
+- `executor.html` (as a Document),
+- `executor-worker.js` (as a Web Worker), and
+- `executor-service-worker.js` (as a Service Worker)
+
+are examples of executors.
+Note that these executors are NOT compatible with
+`RemoteContext.execute_script()`.
+
+## Future Work
+
+`send()`, `receive()` and the executors below are kept for COEP/COOP tests.
+
+For remote script execution, new tests should use
+`RemoteContext.execute_script()` instead.
+
+For message passing,
+[WPT RFC 90](https://github.com/web-platform-tests/rfcs/pull/90) is still under
+discussion.
diff --git a/test/fixtures/wpt/common/dispatcher/dispatcher.js b/test/fixtures/wpt/common/dispatcher/dispatcher.js
new file mode 100644
index 000000000000000..a0f9f43e622483c
--- /dev/null
+++ b/test/fixtures/wpt/common/dispatcher/dispatcher.js
@@ -0,0 +1,256 @@
+// Define a universal message passing API. It works cross-origin and across
+// browsing context groups.
+const dispatcher_path = "/common/dispatcher/dispatcher.py";
+const dispatcher_url = new URL(dispatcher_path, location.href).href;
+
+// Return a promise, limiting the number of concurrent accesses to a shared
+// resources to |max_concurrent_access|.
+const concurrencyLimiter = (max_concurrency) => {
+ let pending = 0;
+ let waiting = [];
+ return async (task) => {
+ pending++;
+ if (pending > max_concurrency)
+ await new Promise(resolve => waiting.push(resolve));
+ let result = await task();
+ pending--;
+ waiting.shift()?.();
+ return result;
+ };
+}
+
+// Wait for a random amount of time in the range [10ms,100ms].
+const randomDelay = () => {
+ return new Promise(resolve => setTimeout(resolve, 10 + 90*Math.random()));
+}
+
+// Sending too many requests in parallel causes congestion. Limiting it improves
+// throughput.
+//
+// Note: The following table has been determined on the test:
+// ../cache-storage.tentative.https.html
+// using Chrome with a 64 core CPU / 64GB ram, in release mode:
+// ┌───────────┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬────┐
+// │concurrency│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 10│ 15│ 20│ 30│ 50│ 100│
+// ├───────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼────┤
+// │time (s) │ 54│ 38│ 31│ 29│ 26│ 24│ 22│ 22│ 22│ 22│ 34│ 36 │
+// └───────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴────┘
+const limiter = concurrencyLimiter(6);
+
+// While requests to different remote contexts can go in parallel, we need to
+// ensure that requests to each remote context are done in order. This maps a
+// uuid to a queue of requests to send. A queue is processed until it is empty
+// and then is deleted from the map.
+const sendQueues = new Map();
+
+// Sends a single item (with rate-limiting) and calls the associated resolver
+// when it is successfully sent.
+const sendItem = async function (uuid, resolver, message) {
+ await limiter(async () => {
+ // Requests might be dropped. Retry until getting a confirmation it has been
+ // processed.
+ while(1) {
+ try {
+ let response = await fetch(dispatcher_url + `?uuid=${uuid}`, {
+ method: 'POST',
+ body: message
+ })
+ if (await response.text() == "done") {
+ resolver();
+ return;
+ }
+ } catch (fetch_error) {}
+ await randomDelay();
+ };
+ });
+}
+
+// While the queue is non-empty, send the next item. This is async and new items
+// may be added to the queue while others are being sent.
+const processQueue = async function (uuid, queue) {
+ while (queue.length) {
+ const [resolver, message] = queue.shift();
+ await sendItem(uuid, resolver, message);
+ }
+ // The queue is empty, delete it.
+ sendQueues.delete(uuid);
+}
+
+const send = async function (uuid, message) {
+ const itemSentPromise = new Promise((resolve) => {
+ const item = [resolve, message];
+ if (sendQueues.has(uuid)) {
+ // There is already a queue for `uuid`, just add to it and it will be processed.
+ sendQueues.get(uuid).push(item);
+ } else {
+ // There is no queue for `uuid`, create it and start processing.
+ const queue = [item];
+ sendQueues.set(uuid, queue);
+ processQueue(uuid, queue);
+ }
+ });
+ // Wait until the item has been successfully sent.
+ await itemSentPromise;
+}
+
+const receive = async function (uuid) {
+ while(1) {
+ let data = "not ready";
+ try {
+ data = await limiter(async () => {
+ let response = await fetch(dispatcher_url + `?uuid=${uuid}`);
+ return await response.text();
+ });
+ } catch (fetch_error) {}
+
+ if (data == "not ready") {
+ await randomDelay();
+ continue;
+ }
+
+ return data;
+ }
+}
+
+// Returns an URL. When called, the server sends toward the `uuid` queue the
+// request headers. Useful for determining if something was requested with
+// Cookies.
+const showRequestHeaders = function(origin, uuid) {
+ return origin + dispatcher_path + `?uuid=${uuid}&show-headers`;
+}
+
+// Same as above, except for the response is cacheable.
+const cacheableShowRequestHeaders = function(origin, uuid) {
+ return origin + dispatcher_path + `?uuid=${uuid}&cacheable&show-headers`;
+}
+
+// This script requires
+// - `/common/utils.js` for `token()`.
+
+// Returns the URL of a document that can be used as a `RemoteContext`.
+//
+// `uuid` should be a UUID uniquely identifying the given remote context.
+// `options` has the following shape:
+//
+// {
+// host: (optional) Sets the returned URL's `host` property. Useful for
+// cross-origin executors.
+// protocol: (optional) Sets the returned URL's `protocol` property.
+// }
+function remoteExecutorUrl(uuid, options) {
+ const url = new URL("/common/dispatcher/remote-executor.html", location);
+ url.searchParams.set("uuid", uuid);
+
+ if (options?.host) {
+ url.host = options.host;
+ }
+
+ if (options?.protocol) {
+ url.protocol = options.protocol;
+ }
+
+ return url;
+}
+
+// Represents a remote executor. For more detailed explanation see `README.md`.
+class RemoteContext {
+ // `uuid` is a UUID string that identifies the remote context and should
+ // match with the `uuid` parameter of the URL of the remote context.
+ constructor(uuid) {
+ this.context_id = uuid;
+ }
+
+ // Evaluates the script `expr` on the executor.
+ // - If `expr` is evaluated to a Promise that is resolved with a value:
+ // `execute_script()` returns a Promise resolved with the value.
+ // - If `expr` is evaluated to a non-Promise value:
+ // `execute_script()` returns a Promise resolved with the value.
+ // - If `expr` throws an error or is evaluated to a Promise that is rejected:
+ // `execute_script()` returns a rejected Promise with the error's
+ // `message`.
+ // Note that currently the type of error (e.g. DOMException) is not
+ // preserved, except for `TypeError`.
+ // The values should be able to be serialized by JSON.stringify().
+ async execute_script(fn, args) {
+ const receiver = token();
+ await this.send({receiver: receiver, fn: fn.toString(), args: args});
+ const response = JSON.parse(await receive(receiver));
+ if (response.status === 'success') {
+ return response.value;
+ }
+
+ // exception
+ if (response.name === 'TypeError') {
+ throw new TypeError(response.value);
+ }
+ throw new Error(response.value);
+ }
+
+ async send(msg) {
+ return await send(this.context_id, JSON.stringify(msg));
+ }
+};
+
+class Executor {
+ constructor(uuid) {
+ this.uuid = uuid;
+
+ // If `suspend_callback` is not `null`, the executor should be suspended
+ // when there are no ongoing tasks.
+ this.suspend_callback = null;
+
+ this.execute();
+ }
+
+ // Wait until there are no ongoing tasks nor fetch requests for polling
+ // tasks, and then suspend the executor and call `callback()`.
+ // Navigation from the executor page should be triggered inside `callback()`,
+ // to avoid conflict with in-flight fetch requests.
+ suspend(callback) {
+ this.suspend_callback = callback;
+ }
+
+ resume() {
+ }
+
+ async execute() {
+ while(true) {
+ if (this.suspend_callback !== null) {
+ this.suspend_callback();
+ this.suspend_callback = null;
+ // Wait for `resume()` to be called.
+ await new Promise(resolve => this.resume = resolve);
+
+ // Workaround for https://crbug.com/1244230.
+ // Without this workaround, the executor is resumed and the fetch
+ // request to poll the next task is initiated synchronously from
+ // pageshow event after the page restored from BFCache, and the fetch
+ // request promise is never resolved (and thus the test results in
+ // timeout) due to https://crbug.com/1244230. The root cause is not yet
+ // known, but setTimeout() with 0ms causes the resume triggered on
+ // another task and seems to resolve the issue.
+ await new Promise(resolve => setTimeout(resolve, 0));
+
+ continue;
+ }
+
+ const task = JSON.parse(await receive(this.uuid));
+
+ let response;
+ try {
+ const value = await eval(task.fn).apply(null, task.args);
+ response = JSON.stringify({
+ status: 'success',
+ value: value
+ });
+ } catch(e) {
+ response = JSON.stringify({
+ status: 'exception',
+ name: e.name,
+ value: e.message
+ });
+ }
+ await send(task.receiver, response);
+ }
+ }
+}
diff --git a/test/fixtures/wpt/common/dispatcher/executor-service-worker.js b/test/fixtures/wpt/common/dispatcher/executor-service-worker.js
new file mode 100644
index 000000000000000..0b47d66b65f066f
--- /dev/null
+++ b/test/fixtures/wpt/common/dispatcher/executor-service-worker.js
@@ -0,0 +1,24 @@
+importScripts('./dispatcher.js');
+
+const params = new URLSearchParams(location.search);
+const uuid = params.get('uuid');
+
+// The fetch handler must be registered before parsing the main script response.
+// So do it here, for future use.
+fetchHandler = () => {}
+addEventListener('fetch', e => {
+ fetchHandler(e);
+});
+
+// Force ServiceWorker to immediately activate itself.
+addEventListener('install', event => {
+ skipWaiting();
+});
+
+let executeOrders = async function() {
+ while(true) {
+ let task = await receive(uuid);
+ eval(`(async () => {${task}})()`);
+ }
+};
+executeOrders();
diff --git a/test/fixtures/wpt/common/dispatcher/executor-worker.js b/test/fixtures/wpt/common/dispatcher/executor-worker.js
new file mode 100644
index 000000000000000..ea065a6bf119555
--- /dev/null
+++ b/test/fixtures/wpt/common/dispatcher/executor-worker.js
@@ -0,0 +1,12 @@
+importScripts('./dispatcher.js');
+
+const params = new URLSearchParams(location.search);
+const uuid = params.get('uuid');
+
+let executeOrders = async function() {
+ while(true) {
+ let task = await receive(uuid);
+ eval(`(async () => {${task}})()`);
+ }
+};
+executeOrders();
diff --git a/test/fixtures/wpt/common/dispatcher/executor.html b/test/fixtures/wpt/common/dispatcher/executor.html
new file mode 100644
index 000000000000000..5fe6a95efaf97d8
--- /dev/null
+++ b/test/fixtures/wpt/common/dispatcher/executor.html
@@ -0,0 +1,15 @@
+
+
diff --git a/test/fixtures/wpt/common/dispatcher/remote-executor.html b/test/fixtures/wpt/common/dispatcher/remote-executor.html
new file mode 100644
index 000000000000000..8b0030390d0d196
--- /dev/null
+++ b/test/fixtures/wpt/common/dispatcher/remote-executor.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/test/fixtures/wpt/common/gc.js b/test/fixtures/wpt/common/gc.js
new file mode 100644
index 000000000000000..ac43a4cfaf7735a
--- /dev/null
+++ b/test/fixtures/wpt/common/gc.js
@@ -0,0 +1,52 @@
+/**
+ * Does a best-effort attempt at invoking garbage collection. Attempts to use
+ * the standardized `TestUtils.gc()` function, but falls back to other
+ * environment-specific nonstandard functions, with a final result of just
+ * creating a lot of garbage (in which case you will get a console warning).
+ *
+ * This should generally only be used to attempt to trigger bugs and crashes
+ * inside tests, i.e. cases where if garbage collection happened, then this
+ * should not trigger some misbehavior. You cannot rely on garbage collection
+ * successfully trigger, or that any particular unreachable object will be
+ * collected.
+ *
+ * @returns {Promise} A promise you should await to ensure garbage
+ * collection has had a chance to complete.
+ */
+self.garbageCollect = async () => {
+ // https://testutils.spec.whatwg.org/#the-testutils-namespace
+ if (self.TestUtils?.gc) {
+ return TestUtils.gc();
+ }
+
+ // Use --expose_gc for V8 (and Node.js)
+ // to pass this flag at chrome launch use: --js-flags="--expose-gc"
+ // Exposed in SpiderMonkey shell as well
+ if (self.gc) {
+ return self.gc();
+ }
+
+ // Present in some WebKit development environments
+ if (self.GCController) {
+ return GCController.collect();
+ }
+
+ console.warn(
+ 'Tests are running without the ability to do manual garbage collection. ' +
+ 'They will still work, but coverage will be suboptimal.');
+
+ for (var i = 0; i < 1000; i++) {
+ gcRec(10);
+ }
+
+ function gcRec(n) {
+ if (n < 1) {
+ return {};
+ }
+
+ let temp = { i: "ab" + i + i / 100000 };
+ temp += "foo";
+
+ gcRec(n - 1);
+ }
+};
diff --git a/test/fixtures/wpt/common/media.js b/test/fixtures/wpt/common/media.js
index e9b1e6b0fbe6ae9..f2dc86126604954 100644
--- a/test/fixtures/wpt/common/media.js
+++ b/test/fixtures/wpt/common/media.js
@@ -47,9 +47,9 @@ function getMediaContentType(url) {
var extension = new URL(url, location).pathname.split(".").pop();
var map = {
"mp4": "video/mp4",
- "ogv": "video/ogg",
+ "ogv": "application/ogg",
"mp3": "audio/mp3",
- "oga": "audio/ogg",
+ "oga": "application/ogg",
};
return map[extension];
}
diff --git a/test/fixtures/wpt/common/object-association.js b/test/fixtures/wpt/common/object-association.js
index 458aae67db0cef0..669c17c07b1ae5b 100644
--- a/test/fixtures/wpt/common/object-association.js
+++ b/test/fixtures/wpt/common/object-association.js
@@ -1,58 +1,64 @@
"use strict";
-// For now this only has per-Window tests, but we could expand it to also test per-Document
+// This is for testing whether an object (e.g., a global property) is associated with Window, or
+// with Document. Recall that Window and Document are 1:1 except when doing a same-origin navigation
+// away from the initial about:blank. In that case the Window object gets reused for the new
+// Document.
+//
+// So:
+// - If something is per-Window, then it should maintain its identity across an about:blank
+// navigation.
+// - If something is per-Document, then it should be recreated across an about:blank navigation.
-/**
- * Run tests for window[propertyName] after discarding the browsing context, navigating, etc.
- * @param {string} propertyName
- */
window.testIsPerWindow = propertyName => {
- test(t => {
+ runTests(propertyName, assert_equals, "must not");
+};
+
+window.testIsPerDocument = propertyName => {
+ runTests(propertyName, assert_not_equals, "must");
+};
+
+function runTests(propertyName, equalityOrInequalityAsserter, mustOrMustNotReplace) {
+ async_test(t => {
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const frame = iframe.contentWindow;
const before = frame[propertyName];
- assert_true(before !== undefined && before !== null, `window.${propertyName} must be implemented`);
+ assert_implements(before, `window.${propertyName} must be implemented`);
- iframe.remove();
+ iframe.onload = t.step_func_done(() => {
+ const after = frame[propertyName];
+ equalityOrInequalityAsserter(after, before);
+ });
- const after = frame[propertyName];
- assert_equals(after, before, `window.${propertyName} should not change after iframe.remove()`);
- }, `Discarding the browsing context must not change window.${propertyName}`);
+ iframe.src = "/common/blank.html";
+ }, `Navigating from the initial about:blank ${mustOrMustNotReplace} replace window.${propertyName}`);
- async_test(t => {
+ // Per spec, discarding a browsing context should not change any of the global objects.
+ test(() => {
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);
const frame = iframe.contentWindow;
const before = frame[propertyName];
- assert_true(before !== undefined && before !== null, `window.${propertyName} must be implemented`);
-
- // Note: cannot use step_func_done for this because it might be called twice, per the below comment.
- iframe.onload = t.step_func(() => {
- if (frame.location.href === "about:blank") {
- // Browsers are not reliable on whether about:blank fires the load event; see
- // https://github.com/whatwg/html/issues/490
- return;
- }
+ assert_implements(before, `window.${propertyName} must be implemented`);
- const after = frame[propertyName];
- assert_equals(after, before);
- t.done();
- });
+ iframe.remove();
- iframe.src = "/common/blank.html";
- }, `Navigating from the initial about:blank must not replace window.${propertyName}`);
+ const after = frame[propertyName];
+ assert_equals(after, before, `window.${propertyName} should not change after iframe.remove()`);
+ }, `Discarding the browsing context must not change window.${propertyName}`);
- // Per spec, document.open() should not change any of the Window state.
+ // Per spec, document.open() should not change any of the global objects. In historical versions
+ // of the spec, it did, so we test here.
async_test(t => {
const iframe = document.createElement("iframe");
iframe.onload = t.step_func_done(() => {
const frame = iframe.contentWindow;
const before = frame[propertyName];
- assert_true(before !== undefined && before !== null, `window.${propertyName} must be implemented`);
+ assert_implements(before, `window.${propertyName} must be implemented`);
frame.document.open();
@@ -64,5 +70,5 @@ window.testIsPerWindow = propertyName => {
iframe.src = "/common/blank.html";
document.body.appendChild(iframe);
- }, `document.open() must replace window.${propertyName}`);
-};
+ }, `document.open() must not replace window.${propertyName}`);
+}
diff --git a/test/fixtures/wpt/common/proxy-all.sub.pac b/test/fixtures/wpt/common/proxy-all.sub.pac
new file mode 100644
index 000000000000000..de601e5d7020e01
--- /dev/null
+++ b/test/fixtures/wpt/common/proxy-all.sub.pac
@@ -0,0 +1,3 @@
+function FindProxyForURL(url, host) {
+ return "PROXY {{host}}:{{ports[http][0]}}"
+}
diff --git a/test/fixtures/wpt/common/reftest-wait.js b/test/fixtures/wpt/common/reftest-wait.js
index 0a30a197f07f4df..64fe9bfd7f54ae9 100644
--- a/test/fixtures/wpt/common/reftest-wait.js
+++ b/test/fixtures/wpt/common/reftest-wait.js
@@ -18,3 +18,22 @@ function takeScreenshotDelayed(timeout) {
takeScreenshot();
}, timeout);
}
+
+/**
+ * Ensure that a precondition is met before waiting for a screenshot.
+ * @param {bool} condition - Fail the test if this evaluates to false
+ * @param {string} msg - Error message to write to the screenshot
+ */
+function failIfNot(condition, msg) {
+ const fail = () => {
+ (document.body || document.documentElement).textContent = `Precondition Failed: ${msg}`;
+ takeScreenshot();
+ };
+ if (!condition) {
+ if (document.readyState == "interactive") {
+ fail();
+ } else {
+ document.addEventListener("DOMContentLoaded", fail, false);
+ }
+ }
+}
diff --git a/test/fixtures/wpt/common/sab.js b/test/fixtures/wpt/common/sab.js
index 47d12970d393c12..a3ea610e165d0d5 100644
--- a/test/fixtures/wpt/common/sab.js
+++ b/test/fixtures/wpt/common/sab.js
@@ -6,14 +6,14 @@ const createBuffer = (() => {
} catch(e) {
sabConstructor = null;
}
- return (type, length) => {
+ return (type, length, opts) => {
if (type === "ArrayBuffer") {
- return new ArrayBuffer(length);
+ return new ArrayBuffer(length, opts);
} else if (type === "SharedArrayBuffer") {
if (sabConstructor && sabConstructor.name !== "SharedArrayBuffer") {
throw new Error("WebAssembly.Memory does not support shared:true");
}
- return new sabConstructor(length);
+ return new sabConstructor(length, opts);
} else {
throw new Error("type has to be ArrayBuffer or SharedArrayBuffer");
}
diff --git a/test/fixtures/wpt/common/security-features/resources/common.sub.js b/test/fixtures/wpt/common/security-features/resources/common.sub.js
index 402ce9bbacf3473..96ca280597bf295 100644
--- a/test/fixtures/wpt/common/security-features/resources/common.sub.js
+++ b/test/fixtures/wpt/common/security-features/resources/common.sub.js
@@ -485,9 +485,13 @@ function dedicatedWorkerUrlThatFetches(url) {
.catch((e) => postMessage(e.message));`;
}
-function workerUrlThatImports(url) {
+function workerUrlThatImports(url, additionalAttributes) {
+ let csp = "";
+ if (additionalAttributes && additionalAttributes.contentSecurityPolicy) {
+ csp=`&contentSecurityPolicy=${additionalAttributes.contentSecurityPolicy}`;
+ }
return `/common/security-features/subresource/static-import.py` +
- `?import_url=${encodeURIComponent(url)}`;
+ `?import_url=${encodeURIComponent(url)}${csp}`;
}
function workerDataUrlThatImports(url) {
@@ -630,6 +634,24 @@ function requestViaScript(url, additionalAttributes) {
.then(event => wrapResult(event.data));
}
+/**
+ * Creates a new script element that performs a dynamic import to `url`, and
+ * appends the script element to {@code document.body}.
+ * @param {string} url The src URL.
+ * @return {Promise} The promise for success/error events.
+ */
+function requestViaDynamicImport(url, additionalAttributes) {
+ const scriptUrl = `data:text/javascript,import("${url}");`;
+ const script = createElement(
+ "script",
+ Object.assign({"src": scriptUrl}, additionalAttributes),
+ document.body,
+ false);
+
+ return bindEvents2(window, "message", script, "error", window, "error")
+ .then(event => wrapResult(event.data));
+}
+
/**
* Creates a new form element, sets attributes, appends it to
* {@code document.body} and submits the form.
@@ -866,6 +888,10 @@ const subresourceMap = {
path: "/common/security-features/subresource/script.py",
invoker: requestViaScript,
},
+ "script-tag-dynamic-import": {
+ path: "/common/security-features/subresource/script.py",
+ invoker: requestViaDynamicImport,
+ },
"video-tag": {
path: "/common/security-features/subresource/video.py",
invoker: requestViaVideo,
@@ -885,8 +911,8 @@ const subresourceMap = {
},
"worker-import": {
path: "/common/security-features/subresource/worker.py",
- invoker: url =>
- requestViaDedicatedWorker(workerUrlThatImports(url), {type: "module"}),
+ invoker: (url, additionalAttributes) =>
+ requestViaDedicatedWorker(workerUrlThatImports(url, additionalAttributes), {type: "module"}),
},
"worker-import-data": {
path: "/common/security-features/subresource/worker.py",
@@ -903,8 +929,8 @@ const subresourceMap = {
},
"sharedworker-import": {
path: "/common/security-features/subresource/shared-worker.py",
- invoker: url =>
- requestViaSharedWorker(workerUrlThatImports(url), {type: "module"}),
+ invoker: (url, additionalAttributes) =>
+ requestViaSharedWorker(workerUrlThatImports(url, additionalAttributes), {type: "module"}),
},
"sharedworker-import-data": {
path: "/common/security-features/subresource/shared-worker.py",
@@ -1087,6 +1113,10 @@ function invokeRequest(subresource, sourceContextList) {
additionalAttributes[policyDelivery.key] = policyDelivery.value;
} else if (policyDelivery.deliveryType === "rel-noref") {
additionalAttributes["rel"] = "noreferrer";
+ } else if (policyDelivery.deliveryType === "http-rp") {
+ additionalAttributes[policyDelivery.key] = policyDelivery.value;
+ } else if (policyDelivery.deliveryType === "meta") {
+ additionalAttributes[policyDelivery.key] = policyDelivery.value;
}
}
diff --git a/test/fixtures/wpt/common/security-features/tools/spec.src.json b/test/fixtures/wpt/common/security-features/tools/spec.src.json
index 0a46a1cd6d31821..4a84493f475b010 100644
--- a/test/fixtures/wpt/common/security-features/tools/spec.src.json
+++ b/test/fixtures/wpt/common/security-features/tools/spec.src.json
@@ -106,6 +106,7 @@
"object-tag",
"picture-tag",
"script-tag",
+ "script-tag-dynamic-import",
"sharedworker-classic",
"sharedworker-import",
"sharedworker-import-data",
@@ -327,26 +328,6 @@
],
"subresourcePolicyDeliveries": []
},
- "worker-classic-inherit": {
- // This is applicable to upgrade-insecure-requests and mixed-content tests.
- // Use "worker-classic" for referrer-policy.
- "description": "dedicated workers should inherit its parent's policy.",
- "sourceContextList": [
- {
- "sourceContextType": "top",
- "policyDeliveries": [
- "policy"
- ]
- },
- {
- "sourceContextType": "worker-classic",
- "policyDeliveries": [
- "anotherPolicy"
- ]
- }
- ],
- "subresourcePolicyDeliveries": []
- },
"worker-classic-data": {
"description": "data: dedicated workers should inherit its parent's policy.",
"sourceContextList": [
@@ -365,7 +346,6 @@
},
"worker-module": {
// This is applicable to referrer-policy tests.
- // Use "worker-module-inherit" for CSP (mixed-content, etc.).
"description": "dedicated workers shouldn't inherit its parent's policy.",
"sourceContextList": [
{
@@ -383,26 +363,6 @@
],
"subresourcePolicyDeliveries": []
},
- "worker-module-inherit": {
- // This is applicable to upgrade-insecure-requests and mixed-content tests.
- // Use "worker-module" for referrer-policy.
- "description": "dedicated workers should inherit its parent's policy.",
- "sourceContextList": [
- {
- "sourceContextType": "top",
- "policyDeliveries": [
- "policy"
- ]
- },
- {
- "sourceContextType": "worker-module",
- "policyDeliveries": [
- "anotherPolicy"
- ]
- }
- ],
- "subresourcePolicyDeliveries": []
- },
"worker-module-data": {
"description": "data: dedicated workers should inherit its parent's policy.",
"sourceContextList": [
@@ -505,10 +465,8 @@
"iframe",
"iframe-blank-inherit",
"worker-classic",
- "worker-classic-inherit",
"worker-classic-data",
"worker-module",
- "worker-module-inherit",
"worker-module-data",
"sharedworker-classic",
"sharedworker-classic-data",
@@ -550,6 +508,7 @@
"object-tag",
"picture-tag",
"script-tag",
+ "script-tag-dynamic-import",
"sharedworker-classic",
"sharedworker-import",
"sharedworker-import-data",
diff --git a/test/fixtures/wpt/common/stringifiers.js b/test/fixtures/wpt/common/stringifiers.js
index 18de6c8c5b32c5e..8dadac1d4929d91 100644
--- a/test/fixtures/wpt/common/stringifiers.js
+++ b/test/fixtures/wpt/common/stringifiers.js
@@ -1,5 +1,5 @@
/**
- * Runs tests for .
+ * Runs tests for .
* @param {Object} aObject - object to test
* @param {string} aAttribute - IDL attribute name that is annotated with `stringifier`
* @param {boolean} aIsUnforgeable - whether the IDL attribute is `[LegacyUnforgeable]`
diff --git a/test/fixtures/wpt/resources/check-layout-th.js b/test/fixtures/wpt/resources/check-layout-th.js
index 9cd8abc938d9fb1..f14ca3246b8ea21 100644
--- a/test/fixtures/wpt/resources/check-layout-th.js
+++ b/test/fixtures/wpt/resources/check-layout-th.js
@@ -26,7 +26,11 @@ function assert_tolerance(actual, expected, message)
}
function checkDataKeys(node) {
+ // The purpose of this list of data-* attributes is simply to ensure typos
+ // in tests are caught. It is therefore "ok" to add to this list for
+ // specific tests.
var validData = new Set([
+ "data-anchor-polyfill",
"data-expected-width",
"data-expected-height",
"data-offset-x",
diff --git a/test/fixtures/wpt/resources/declarative-shadow-dom-polyfill.js b/test/fixtures/wpt/resources/declarative-shadow-dom-polyfill.js
new file mode 100644
index 000000000000000..8ab08b0294c297f
--- /dev/null
+++ b/test/fixtures/wpt/resources/declarative-shadow-dom-polyfill.js
@@ -0,0 +1,24 @@
+/*
+ * Polyfill for attaching shadow trees for declarative Shadow DOM for
+ * implementations that do not support declarative Shadow DOM.
+ *
+ * Note: this polyfill will feature-detect the native feature, and do nothing
+ * if supported.
+ *
+ * See: https://github.com/whatwg/html/pull/5465
+ *
+ * root: The root of the subtree in which to upgrade shadow roots
+ *
+ */
+
+function polyfill_declarative_shadow_dom(root) {
+ if (HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode'))
+ return;
+ root.querySelectorAll("template[shadowrootmode]").forEach(template => {
+ const mode = template.getAttribute("shadowrootmode");
+ const shadowRoot = template.parentNode.attachShadow({ mode });
+ shadowRoot.appendChild(template.content);
+ template.remove();
+ polyfill_declarative_shadow_dom(shadowRoot);
+ });
+}
diff --git a/test/fixtures/wpt/resources/idlharness-shadowrealm.js b/test/fixtures/wpt/resources/idlharness-shadowrealm.js
index 05c4a1affc8699b..9484ca6f512ad04 100644
--- a/test/fixtures/wpt/resources/idlharness-shadowrealm.js
+++ b/test/fixtures/wpt/resources/idlharness-shadowrealm.js
@@ -21,11 +21,6 @@ function fetch_text(url) {
* dependency (i.e. have already been seen).
*/
function idl_test_shadowrealm(srcs, deps) {
- const script_urls = [
- "/resources/testharness.js",
- "/resources/WebIDLParser.js",
- "/resources/idlharness.js",
- ];
promise_setup(async t => {
const realm = new ShadowRealm();
// https://github.com/web-platform-tests/wpt/issues/31996
@@ -38,44 +33,29 @@ function idl_test_shadowrealm(srcs, deps) {
isShadowRealm: function() { return true; },
}; undefined;
`);
-
- const ss = await Promise.all(script_urls.map(url => fetch_text(url)));
- for (const s of ss) {
- realm.evaluate(s);
- }
const specs = await Promise.all(srcs.concat(deps).map(spec => {
return fetch_text("/interfaces/" + spec + ".idl");
}));
const idls = JSON.stringify(specs);
-
- const results = JSON.parse(await new Promise(
- realm.evaluate(`(resolve,reject) => {
- const idls = ${idls};
- add_completion_callback(function (tests, harness_status, asserts_run) {
- resolve(JSON.stringify(tests));
- });
-
- // Without the wrapping test, testharness.js will think it's done after it has run
- // the first idlharness test.
- test(() => {
- const idl_array = new IdlArray();
- for (let i = 0; i < ${srcs.length}; i++) {
- idl_array.add_idls(idls[i]);
- }
- for (let i = ${srcs.length}; i < ${srcs.length + deps.length}; i++) {
- idl_array.add_dependency_idls(idls[i]);
- }
- idl_array.test();
- }, "setup");
- }`)
- ));
-
- // We ran the tests in the ShadowRealm and gathered the results. Now treat them as if
- // we'd run them directly here, so we can see them.
- for (const {name, status, message} of results) {
- // TODO: make this an API in testharness.js - needs RFC?
- promise_test(t => {t.set_status(status, message); t.phase = t.phases.HAS_RESULT; t.done()}, name);
- }
- }, "outer setup");
+ await new Promise(
+ realm.evaluate(`(resolve,reject) => {
+ (async () => {
+ await import("/resources/testharness.js");
+ await import("/resources/WebIDLParser.js");
+ await import("/resources/idlharness.js");
+ const idls = ${idls};
+ const idl_array = new IdlArray();
+ for (let i = 0; i < ${srcs.length}; i++) {
+ idl_array.add_idls(idls[i]);
+ }
+ for (let i = ${srcs.length}; i < ${srcs.length + deps.length}; i++) {
+ idl_array.add_dependency_idls(idls[i]);
+ }
+ idl_array.test();
+ })().then(resolve, (e) => reject(e.toString()));
+ }`)
+ );
+ await fetch_tests_from_shadow_realm(realm);
+ });
}
// vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker:
diff --git a/test/fixtures/wpt/resources/idlharness.js b/test/fixtures/wpt/resources/idlharness.js
index d2fb0366c8022ae..46aa11e5ca123c2 100644
--- a/test/fixtures/wpt/resources/idlharness.js
+++ b/test/fixtures/wpt/resources/idlharness.js
@@ -2433,44 +2433,6 @@ IdlInterface.prototype.test_to_json_operation = function(desc, memberHolderObjec
}
};
-IdlInterface.prototype.test_member_iterable = function(member)
-{
- subsetTestByKey(this.name, test, function()
- {
- var isPairIterator = member.idlType.length === 2;
- var proto = this.get_interface_object().prototype;
- var iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
-
- assert_true(iteratorDesc.writable, "@@iterator property should be writable");
- assert_true(iteratorDesc.configurable, "@@iterator property should be configurable");
- assert_false(iteratorDesc.enumerable, "@@iterator property should not be enumerable");
- assert_equals(typeof iteratorDesc.value, "function", "@@iterator property should be a function");
- assert_equals(iteratorDesc.value.length, 0, "@@iterator function object length should be 0");
- assert_equals(iteratorDesc.value.name, isPairIterator ? "entries" : "values", "@@iterator function object should have the right name");
-
- if (isPairIterator) {
- assert_equals(proto["entries"], proto[Symbol.iterator], "entries method should be the same as @@iterator method");
- [
- ["entries", 0],
- ["keys", 0],
- ["values", 0],
- ["forEach", 1]
- ].forEach(([property, length]) => {
- var desc = Object.getOwnPropertyDescriptor(proto, property);
- assert_equals(typeof desc.value, "function", property + " property should be a function");
- assert_equals(desc.value.length, length, property + " function object should have the right length");
- assert_equals(desc.value.name, property, property + " function object should have the right name");
- });
- } else {
- assert_equals(proto[Symbol.iterator], Array.prototype[Symbol.iterator], "@@iterator method should be the same as Array prototype's");
- ["entries", "keys", "values", "forEach", Symbol.iterator].forEach(property => {
- var propertyName = property === Symbol.iterator ? "@@iterator" : property;
- assert_equals(proto[property], Array.prototype[property], propertyName + " method should be the same as Array prototype's");
- });
- }
- }.bind(this), this.name + " interface: iterable<" + member.idlType.map(function(t) { return t.idlType; }).join(", ") + ">");
-};
-
IdlInterface.prototype.test_member_maplike = function(member) {
subsetTestByKey(this.name, test, () => {
const proto = this.get_interface_object().prototype;
@@ -2487,14 +2449,14 @@ IdlInterface.prototype.test_member_maplike = function(member) {
methods.push(
["set", 2],
["delete", 1],
- ["clear", 1]
+ ["clear", 0]
);
}
for (const [name, length] of methods) {
const desc = Object.getOwnPropertyDescriptor(proto, name);
assert_equals(typeof desc.value, "function", `${name} should be a function`);
- assert_equals(desc.enumerable, false, `${name} enumerable`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
assert_equals(desc.configurable, true, `${name} configurable`);
assert_equals(desc.writable, true, `${name} writable`);
assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
@@ -2510,10 +2472,10 @@ IdlInterface.prototype.test_member_maplike = function(member) {
const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size");
assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`);
assert_equals(sizeDesc.set, undefined, `size should not have a setter`);
- assert_equals(sizeDesc.enumerable, false, `size enumerable`);
+ assert_equals(sizeDesc.enumerable, true, `size enumerable`);
assert_equals(sizeDesc.configurable, true, `size configurable`);
- assert_equals(sizeDesc.get.length, 0, `size getter length should have the right length`);
- assert_equals(sizeDesc.get.name, "get size", `size getter have the right name`);
+ assert_equals(sizeDesc.get.length, 0, `size getter length`);
+ assert_equals(sizeDesc.get.name, "get size", `size getter name`);
}, `${this.name} interface: maplike<${member.idlType.map(t => t.idlType).join(", ")}>`);
};
@@ -2532,14 +2494,14 @@ IdlInterface.prototype.test_member_setlike = function(member) {
methods.push(
["add", 1],
["delete", 1],
- ["clear", 1]
+ ["clear", 0]
);
}
for (const [name, length] of methods) {
const desc = Object.getOwnPropertyDescriptor(proto, name);
assert_equals(typeof desc.value, "function", `${name} should be a function`);
- assert_equals(desc.enumerable, false, `${name} enumerable`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
assert_equals(desc.configurable, true, `${name} configurable`);
assert_equals(desc.writable, true, `${name} writable`);
assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
@@ -2555,42 +2517,92 @@ IdlInterface.prototype.test_member_setlike = function(member) {
const sizeDesc = Object.getOwnPropertyDescriptor(proto, "size");
assert_equals(typeof sizeDesc.get, "function", `size getter should be a function`);
assert_equals(sizeDesc.set, undefined, `size should not have a setter`);
- assert_equals(sizeDesc.enumerable, false, `size enumerable`);
+ assert_equals(sizeDesc.enumerable, true, `size enumerable`);
assert_equals(sizeDesc.configurable, true, `size configurable`);
- assert_equals(sizeDesc.get.length, 0, `size getter length should have the right length`);
- assert_equals(sizeDesc.get.name, "size", `size getter have the right name`);
+ assert_equals(sizeDesc.get.length, 0, `size getter length`);
+ assert_equals(sizeDesc.get.name, "get size", `size getter name`);
}, `${this.name} interface: setlike<${member.idlType.map(t => t.idlType).join(", ")}>`);
};
-IdlInterface.prototype.test_member_async_iterable = function(member)
-{
- subsetTestByKey(this.name, test, function()
- {
- var isPairIterator = member.idlType.length === 2;
- var proto = this.get_interface_object().prototype;
- var iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.asyncIterator);
+IdlInterface.prototype.test_member_iterable = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const isPairIterator = member.idlType.length === 2;
+ const proto = this.get_interface_object().prototype;
- assert_true(iteratorDesc.writable, "@@asyncIterator property should be writable");
- assert_true(iteratorDesc.configurable, "@@asyncIterator property should be configurable");
- assert_false(iteratorDesc.enumerable, "@@asyncIterator property should not be enumerable");
- assert_equals(typeof iteratorDesc.value, "function", "@@asyncIterator property should be a function");
- assert_equals(iteratorDesc.value.length, 0, "@@asyncIterator function object length should be 0");
- assert_equals(iteratorDesc.value.name, isPairIterator ? "entries" : "values", "@@asyncIterator function object should have the right name");
+ const methods = [
+ ["entries", 0],
+ ["keys", 0],
+ ["values", 0],
+ ["forEach", 1]
+ ];
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+
+ if (!isPairIterator) {
+ assert_equals(desc.value, Array.prototype[name], `${name} equality with Array.prototype version`);
+ }
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.iterator);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
if (isPairIterator) {
- assert_equals(proto["entries"], proto[Symbol.asyncIterator], "entries method should be the same as @@asyncIterator method");
- ["entries", "keys", "values"].forEach(property => {
- var desc = Object.getOwnPropertyDescriptor(proto, property);
- assert_equals(typeof desc.value, "function", property + " property should be a function");
- assert_equals(desc.value.length, 0, property + " function object length should be 0");
- assert_equals(desc.value.name, property, property + " function object should have the right name");
- });
+ assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`);
} else {
- assert_equals(proto["values"], proto[Symbol.asyncIterator], "values method should be the same as @@asyncIterator method");
- assert_false("entries" in proto, "should not have an entries method");
- assert_false("keys" in proto, "should not have a keys method");
+ assert_equals(iteratorDesc.value, Array.prototype[Symbol.iterator], `@@iterator equality with Array.prototype version`);
}
- }.bind(this), this.name + " interface: async iterable<" + member.idlType.map(function(t) { return t.idlType; }).join(", ") + ">");
+ }, `${this.name} interface: iterable<${member.idlType.map(t => t.idlType).join(", ")}>`);
+};
+
+IdlInterface.prototype.test_member_async_iterable = function(member) {
+ subsetTestByKey(this.name, test, () => {
+ const isPairIterator = member.idlType.length === 2;
+ const proto = this.get_interface_object().prototype;
+
+ // Note that although the spec allows arguments, which will be passed to the @@asyncIterator
+ // method (which is either values or entries), those arguments must always be optional. So
+ // length of 0 is still correct for values and entries.
+ const methods = [
+ ["values", 0],
+ ];
+
+ if (isPairIterator) {
+ methods.push(
+ ["entries", 0],
+ ["keys", 0]
+ );
+ }
+
+ for (const [name, length] of methods) {
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
+ assert_equals(typeof desc.value, "function", `${name} should be a function`);
+ assert_equals(desc.enumerable, true, `${name} enumerable`);
+ assert_equals(desc.configurable, true, `${name} configurable`);
+ assert_equals(desc.writable, true, `${name} writable`);
+ assert_equals(desc.value.length, length, `${name} function object length should be ${length}`);
+ assert_equals(desc.value.name, name, `${name} function object should have the right name`);
+ }
+
+ const iteratorDesc = Object.getOwnPropertyDescriptor(proto, Symbol.asyncIterator);
+ assert_equals(iteratorDesc.enumerable, false, `@@iterator enumerable`);
+ assert_equals(iteratorDesc.configurable, true, `@@iterator configurable`);
+ assert_equals(iteratorDesc.writable, true, `@@iterator writable`);
+
+ if (isPairIterator) {
+ assert_equals(iteratorDesc.value, proto.entries, `@@iterator equality with entries`);
+ } else {
+ assert_equals(iteratorDesc.value, proto.values, `@@iterator equality with values`);
+ }
+ }, `${this.name} interface: async iterable<${member.idlType.map(t => t.idlType).join(", ")}>`);
};
IdlInterface.prototype.test_member_stringifier = function(member)
@@ -2656,6 +2668,7 @@ IdlInterface.prototype.test_member_stringifier = function(member)
IdlInterface.prototype.test_members = function()
{
+ var unexposed_members = new Set();
for (var i = 0; i < this.members.length; i++)
{
var member = this.members[i];
@@ -2664,15 +2677,18 @@ IdlInterface.prototype.test_members = function()
}
if (!exposed_in(exposure_set(member, this.exposureSet))) {
- subsetTestByKey(this.name, test, function() {
- // It's not exposed, so we shouldn't find it anywhere.
- assert_false(member.name in this.get_interface_object(),
- "The interface object must not have a property " +
- format_value(member.name));
- assert_false(member.name in this.get_interface_object().prototype,
- "The prototype object must not have a property " +
- format_value(member.name));
- }.bind(this), this.name + " interface: member " + member.name);
+ if (!unexposed_members.has(member.name)) {
+ unexposed_members.add(member.name);
+ subsetTestByKey(this.name, test, function() {
+ // It's not exposed, so we shouldn't find it anywhere.
+ assert_false(member.name in this.get_interface_object(),
+ "The interface object must not have a property " +
+ format_value(member.name));
+ assert_false(member.name in this.get_interface_object().prototype,
+ "The prototype object must not have a property " +
+ format_value(member.name));
+ }.bind(this), this.name + " interface: member " + member.name);
+ }
continue;
}
@@ -2751,21 +2767,26 @@ IdlInterface.prototype.test_object = function(desc)
expected_typeof = "object";
}
- this.test_primary_interface_of(desc, obj, exception, expected_typeof);
+ if (this.is_callback()) {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_equals(typeof obj, expected_typeof, "wrong typeof object");
+ } else {
+ this.test_primary_interface_of(desc, obj, exception, expected_typeof);
- var current_interface = this;
- while (current_interface)
- {
- if (!(current_interface.name in this.array.members))
+ var current_interface = this;
+ while (current_interface)
{
- throw new IdlHarnessError("Interface " + current_interface.name + " not found (inherited by " + this.name + ")");
- }
- if (current_interface.prevent_multiple_testing && current_interface.already_tested)
- {
- return;
+ if (!(current_interface.name in this.array.members))
+ {
+ throw new IdlHarnessError("Interface " + current_interface.name + " not found (inherited by " + this.name + ")");
+ }
+ if (current_interface.prevent_multiple_testing && current_interface.already_tested)
+ {
+ return;
+ }
+ current_interface.test_interface_of(desc, obj, exception, expected_typeof);
+ current_interface = this.array.members[current_interface.base];
}
- current_interface.test_interface_of(desc, obj, exception, expected_typeof);
- current_interface = this.array.members[current_interface.base];
}
};
@@ -2838,17 +2859,23 @@ IdlInterface.prototype.test_interface_of = function(desc, obj, exception, expect
return;
}
+ var unexposed_properties = new Set();
for (var i = 0; i < this.members.length; i++)
{
var member = this.members[i];
if (member.untested) {
continue;
}
- if (!exposed_in(exposure_set(member, this.exposureSet))) {
- subsetTestByKey(this.name, test, function() {
- assert_equals(exception, null, "Unexpected exception when evaluating object");
- assert_false(member.name in obj);
- }.bind(this), this.name + " interface: " + desc + ' must not have property "' + member.name + '"');
+ if (!exposed_in(exposure_set(member, this.exposureSet)))
+ {
+ if (!unexposed_properties.has(member.name))
+ {
+ unexposed_properties.add(member.name);
+ subsetTestByKey(this.name, test, function() {
+ assert_equals(exception, null, "Unexpected exception when evaluating object");
+ assert_false(member.name in obj);
+ }.bind(this), this.name + " interface: " + desc + ' must not have property "' + member.name + '"');
+ }
continue;
}
if (member.type == "attribute" && member.isUnforgeable)
diff --git a/test/fixtures/wpt/resources/testdriver.js b/test/fixtures/wpt/resources/testdriver.js
index 0737e64a50b313a..76ae2834fdfb0a1 100644
--- a/test/fixtures/wpt/resources/testdriver.js
+++ b/test/fixtures/wpt/resources/testdriver.js
@@ -78,8 +78,8 @@
* Trigger user interaction in order to grant additional privileges to
* a provided function.
*
- * See `triggered by user activation
- * `_.
+ * See `Tracking user activation
+ * `_.
*
* @example
* var mediaElement = document.createElement('video');
@@ -184,6 +184,42 @@
return window.test_driver_internal.delete_all_cookies(context);
},
+ /**
+ * Get details for all cookies in the current context.
+ * See https://w3c.github.io/webdriver/#get-all-cookies
+ *
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Returns an array of cookies objects as defined in the spec:
+ * https://w3c.github.io/webdriver/#cookies
+ */
+ get_all_cookies: function(context=null) {
+ return window.test_driver_internal.get_all_cookies(context);
+ },
+
+ /**
+ * Get details for a cookie in the current context by name if it exists.
+ * See https://w3c.github.io/webdriver/#get-named-cookie
+ *
+ * @param {String} name - The name of the cookie to get.
+ * @param {WindowProxy} context - Browsing context in which
+ * to run the call, or null for the current
+ * browsing context.
+ *
+ * @returns {Promise} Returns the matching cookie as defined in the spec:
+ * https://w3c.github.io/webdriver/#cookies
+ * Rejected if no such cookie exists.
+ */
+ get_named_cookie: async function(name, context=null) {
+ let cookie = await window.test_driver_internal.get_named_cookie(name, context);
+ if (!cookie) {
+ throw new Error("no such cookie");
+ }
+ return cookie;
+ },
+
/**
* Send keys to an element.
*
@@ -362,24 +398,22 @@
*
* @example
* await test_driver.set_permission({ name: "background-fetch" }, "denied");
- * await test_driver.set_permission({ name: "push", userVisibleOnly: true }, "granted", true);
+ * await test_driver.set_permission({ name: "push", userVisibleOnly: true }, "granted");
*
- * @param {Object} descriptor - a `PermissionDescriptor
- * `_
- * object
+ * @param {PermissionDescriptor} descriptor - a `PermissionDescriptor
+ * `_
+ * dictionary.
* @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.
* @returns {Promise} fulfilled after the permission is set, or rejected if setting the
* permission fails
*/
- set_permission: function(descriptor, state, one_realm=false, context=null) {
+ set_permission: function(descriptor, state, context=null) {
let permission_params = {
descriptor,
state,
- oneRealm: one_realm,
};
return window.test_driver_internal.set_permission(permission_params, context);
},
@@ -637,6 +671,14 @@
return Promise.reject(new Error("unimplemented"));
},
+ get_all_cookies: function(context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
+ get_named_cookie: function(name, context=null) {
+ return Promise.reject(new Error("unimplemented"));
+ },
+
send_keys: function(element, keys) {
if (this.in_automation) {
return Promise.reject(new Error('Not implemented'));
diff --git a/test/fixtures/wpt/resources/testharness.js b/test/fixtures/wpt/resources/testharness.js
index 9ec328222097e07..a99b0a0086efb0e 100644
--- a/test/fixtures/wpt/resources/testharness.js
+++ b/test/fixtures/wpt/resources/testharness.js
@@ -494,7 +494,7 @@
ShellTestEnvironment.prototype.next_default_test_name = function() {
var suffix = this.name_counter > 0 ? " " + this.name_counter : "";
this.name_counter++;
- return "Untitled" + suffix;
+ return get_title() + suffix;
};
ShellTestEnvironment.prototype.on_new_harness_properties = function() {};
@@ -2246,7 +2246,8 @@
ReadOnlyError: 0,
VersionError: 0,
OperationError: 0,
- NotAllowedError: 0
+ NotAllowedError: 0,
+ OptOutError: 0
};
var code_name_map = {};
@@ -2477,6 +2478,10 @@
this._user_defined_cleanup_count = 0;
this._done_callbacks = [];
+ if (typeof AbortController === "function") {
+ this._abortController = new AbortController();
+ }
+
// Tests declared following harness completion are likely an indication
// of a programming error, but they cannot be reported
// deterministically.
@@ -2953,6 +2958,10 @@
this.phase = this.phases.CLEANING;
+ if (this._abortController) {
+ this._abortController.abort("Test cleanup");
+ }
+
forEach(this.cleanup_callbacks,
function(cleanup_callback) {
var result;
@@ -3046,6 +3055,16 @@
test._done_callbacks.length = 0;
}
+ /**
+ * Gives an AbortSignal that will be aborted when the test finishes.
+ */
+ Test.prototype.get_signal = function() {
+ if (!this._abortController) {
+ throw new Error("AbortController is not supported in this browser");
+ }
+ return this._abortController.signal;
+ }
+
/**
* A RemoteTest object mirrors a Test object on a remote worker. The
* associated RemoteWorker updates the RemoteTest object in response to
@@ -3822,7 +3841,9 @@
return;
}
- this.pending_remotes.push(this.create_remote_window(remote));
+ var remoteContext = this.create_remote_window(remote);
+ this.pending_remotes.push(remoteContext);
+ return remoteContext.done;
};
/**
@@ -3837,7 +3858,7 @@
* @param {Window} window - The window to fetch tests from.
*/
function fetch_tests_from_window(window) {
- tests.fetch_tests_from_window(window);
+ return tests.fetch_tests_from_window(window);
}
expose(fetch_tests_from_window, 'fetch_tests_from_window');
@@ -3871,7 +3892,7 @@
*/
function begin_shadow_realm_tests(postMessage) {
if (!(test_environment instanceof ShadowRealmTestEnvironment)) {
- throw new Error("beign_shadow_realm_tests called in non-Shadow Realm environment");
+ throw new Error("begin_shadow_realm_tests called in non-Shadow Realm environment");
}
test_environment.begin(function (msg) {
diff --git a/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js b/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js
index f7e2d06ae5cdf36..e578176777adafe 100644
--- a/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js
+++ b/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js
@@ -1,5 +1,6 @@
// META: global=window,worker
// META: script=../resources/test-utils.js
+// META: script=/common/gc.js
'use strict';
promise_test(async () => {
diff --git a/test/fixtures/wpt/streams/resources/test-utils.js b/test/fixtures/wpt/streams/resources/test-utils.js
index fb34e270ff37189..5ff8fc8cec939a4 100644
--- a/test/fixtures/wpt/streams/resources/test-utils.js
+++ b/test/fixtures/wpt/streams/resources/test-utils.js
@@ -47,26 +47,6 @@ self.constructorThrowsForAll = (constructor, firstArgs) => {
'constructor should throw a TypeError'));
};
-self.garbageCollect = async () => {
- if (self.TestUtils?.gc) {
- // https://testutils.spec.whatwg.org/#the-testutils-namespace
- await TestUtils.gc();
- } else if (self.gc) {
- // Use --expose_gc for V8 (and Node.js)
- // to pass this flag at chrome launch use: --js-flags="--expose-gc"
- // Exposed in SpiderMonkey shell as well
- self.gc();
- } else if (self.GCController) {
- // Present in some WebKit development environments
- GCController.collect();
- } else {
- /* eslint-disable no-console */
- console.warn('Tests are running without the ability to do manual garbage collection. They will still work, but ' +
- 'coverage will be suboptimal.');
- /* eslint-enable no-console */
- }
-};
-
self.delay = ms => new Promise(resolve => step_timeout(resolve, ms));
// For tests which verify that the implementation doesn't do something it shouldn't, it's better not to use a
diff --git a/test/fixtures/wpt/streams/transferable/gc-crash.html b/test/fixtures/wpt/streams/transferable/gc-crash.html
new file mode 100644
index 000000000000000..0d331e6be08059d
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/gc-crash.html
@@ -0,0 +1,17 @@
+
+
+
+
diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json
index f3a49312328e6df..f36f5ad20cd611c 100644
--- a/test/fixtures/wpt/versions.json
+++ b/test/fixtures/wpt/versions.json
@@ -1,6 +1,6 @@
{
"common": {
- "commit": "03c5072affa496c5fd2a506e5c40d23e36b5e3aa",
+ "commit": "dbd648158d337580885e70a54f929daf215211a0",
"path": "common"
},
"console": {
@@ -24,13 +24,9 @@
"path": "fetch/data-urls/resources"
},
"FileAPI": {
- "commit": "3b279420d40afea32506e823f9ac005448f4f3d8",
+ "commit": "1e432c4550a1595888d7c9eb26d90895e9e7e022",
"path": "FileAPI"
},
- "FileAPI/file": {
- "commit": "c01f637cca43f0e08ce8e4269121dcd89ccbdd82",
- "path": "FileAPI/file"
- },
"hr-time": {
"commit": "34cafd797e58dad280d20040eee012d49ccfa91f",
"path": "hr-time"
@@ -64,11 +60,11 @@
"path": "resource-timing"
},
"resources": {
- "commit": "fbf1e7d24776b6da144dbca45c22d39dc0512d2d",
+ "commit": "46e1750aa994319806887d7896d43f119ba05c7d",
"path": "resources"
},
"streams": {
- "commit": "9e5ef42bd34b5b19b76d0d4cb19012e52c222664",
+ "commit": "819f38a9cf2e527afa28f1fb31ebc8258717afba",
"path": "streams"
},
"url": {
diff --git a/test/wpt/status/FileAPI/blob.json b/test/wpt/status/FileAPI/blob.json
index 902ac232dd48723..017d931d7abdc4f 100644
--- a/test/wpt/status/FileAPI/blob.json
+++ b/test/wpt/status/FileAPI/blob.json
@@ -17,6 +17,7 @@
"ArrayBuffer elements of the blobParts array should be supported.",
"Passing typed arrays as elements of the blobParts array should work.",
"Passing a Float64Array as element of the blobParts array should work.",
+ "Passing BigInt typed arrays as elements of the blobParts array should work.",
"Array with two blobs",
"Array with two buffers",
"Array with two bufferviews",
@@ -43,5 +44,12 @@
},
"Blob-slice.any.js": {
"skip": "Depends on File API"
+ },
+ "Blob-stream.any.js": {
+ "fail": {
+ "expected": [
+ "Reading Blob.stream() with BYOB reader"
+ ]
+ }
}
}
diff --git a/test/wpt/status/streams.json b/test/wpt/status/streams.json
index 9899c581d9f96e0..187b17587417852 100644
--- a/test/wpt/status/streams.json
+++ b/test/wpt/status/streams.json
@@ -1,4 +1,11 @@
{
+ "idlharness.any.js": {
+ "fail": {
+ "expected": [
+ "ReadableStream interface: async iterable"
+ ]
+ }
+ },
"queuing-strategies-size-function-per-global.window.js": {
"skip": "Browser-specific test"
},
diff --git a/test/wpt/test-url.js b/test/wpt/test-url.js
index f8ba3c3839d6423..fc84c22a275c380 100644
--- a/test/wpt/test-url.js
+++ b/test/wpt/test-url.js
@@ -13,7 +13,4 @@ runner.setScriptModifier((obj) => {
}
});
runner.pretendGlobalThisAs('Window');
-runner.setInitScript(`
- globalThis.location ||= {};
-`);
runner.runJsTests();