From c65027f249ab6da0fdfc7ad5d616bb706cbcccab Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 2 Mar 2023 18:04:40 +0100 Subject: [PATCH] test: update WPT fixtures resources, common, streams, FileAPI --- test/common/wpt.js | 26 +- test/common/wpt/worker.js | 17 +- .../Blob-methods-from-detached-frame.html | 59 ++++ .../cross-partition.tentative.https.html | 276 ++++++++++++++++++ .../wpt/FileAPI/blob/Blob-constructor.any.js | 11 +- .../wpt/FileAPI/blob/Blob-in-worker.worker.js | 15 +- .../FileAPI/blob/Blob-stream-byob-crash.html | 11 + .../blob/Blob-stream-sync-xhr-crash.html | 13 + .../wpt/FileAPI/blob/Blob-stream.any.js | 27 +- test/fixtures/wpt/FileAPI/fileReader.any.js | 59 ++++ test/fixtures/wpt/FileAPI/idlharness.any.js | 19 ++ test/fixtures/wpt/FileAPI/idlharness.html | 7 +- .../fixtures/wpt/FileAPI/idlharness.worker.js | 3 - .../Determining-Encoding.any.js | 81 +++++ ...FileReader-event-handler-attributes.any.js | 17 ++ .../FileReader-multiple-reads.any.js | 81 +++++ .../filereader_abort.any.js | 38 +++ .../filereader_error.any.js | 19 ++ .../filereader_readAsArrayBuffer.any.js | 23 ++ .../filereader_readAsBinaryString.any.js | 23 ++ .../filereader_readAsDataURL.any.js | 42 +++ .../filereader_readAsText.any.js | 36 +++ .../filereader_readystate.any.js | 19 ++ .../filereader_result.any.js | 82 ++++++ test/fixtures/wpt/FileAPI/support/Blob.js | 4 +- .../wpt/FileAPI/support/empty-document.html | 3 + .../support/send-file-formdata-helper.js | 8 +- .../FileAPI/url/cross-global-revoke.sub.html | 5 +- .../wpt/FileAPI/url/url-format.any.js | 8 +- .../wpt/FileAPI/url/url-with-fetch.any.js | 19 ++ test/fixtures/wpt/README.md | 9 +- .../wpt/common/custom-cors-response.js | 32 ++ test/fixtures/wpt/common/dispatcher/README.md | 228 +++++++++++++++ .../wpt/common/dispatcher/dispatcher.js | 256 ++++++++++++++++ .../dispatcher/executor-service-worker.js | 24 ++ .../wpt/common/dispatcher/executor-worker.js | 12 + .../wpt/common/dispatcher/executor.html | 15 + .../common/dispatcher/remote-executor.html | 12 + test/fixtures/wpt/common/gc.js | 52 ++++ test/fixtures/wpt/common/media.js | 4 +- .../fixtures/wpt/common/object-association.js | 68 +++-- test/fixtures/wpt/common/proxy-all.sub.pac | 3 + test/fixtures/wpt/common/reftest-wait.js | 19 ++ test/fixtures/wpt/common/sab.js | 6 +- .../security-features/resources/common.sub.js | 42 ++- .../security-features/tools/spec.src.json | 45 +-- test/fixtures/wpt/common/stringifiers.js | 2 +- .../fixtures/wpt/resources/check-layout-th.js | 4 + .../declarative-shadow-dom-polyfill.js | 24 ++ .../wpt/resources/idlharness-shadowrealm.js | 60 ++-- test/fixtures/wpt/resources/idlharness.js | 223 +++++++------- test/fixtures/wpt/resources/testdriver.js | 60 +++- test/fixtures/wpt/resources/testharness.js | 33 ++- .../garbage-collection.any.js | 1 + .../wpt/streams/resources/test-utils.js | 20 -- .../wpt/streams/transferable/gc-crash.html | 17 ++ test/fixtures/wpt/versions.json | 12 +- test/wpt/status/FileAPI/blob.json | 8 + test/wpt/status/streams.json | 7 + test/wpt/status/url.json | 2 + test/wpt/test-url.js | 3 - 61 files changed, 2024 insertions(+), 330 deletions(-) create mode 100644 test/fixtures/wpt/FileAPI/Blob-methods-from-detached-frame.html create mode 100644 test/fixtures/wpt/FileAPI/BlobURL/cross-partition.tentative.https.html create mode 100644 test/fixtures/wpt/FileAPI/blob/Blob-stream-byob-crash.html create mode 100644 test/fixtures/wpt/FileAPI/blob/Blob-stream-sync-xhr-crash.html create mode 100644 test/fixtures/wpt/FileAPI/fileReader.any.js create mode 100644 test/fixtures/wpt/FileAPI/idlharness.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.any.js create mode 100644 test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.any.js create mode 100644 test/fixtures/wpt/FileAPI/support/empty-document.html create mode 100644 test/fixtures/wpt/common/custom-cors-response.js create mode 100644 test/fixtures/wpt/common/dispatcher/README.md create mode 100644 test/fixtures/wpt/common/dispatcher/dispatcher.js create mode 100644 test/fixtures/wpt/common/dispatcher/executor-service-worker.js create mode 100644 test/fixtures/wpt/common/dispatcher/executor-worker.js create mode 100644 test/fixtures/wpt/common/dispatcher/executor.html create mode 100644 test/fixtures/wpt/common/dispatcher/remote-executor.html create mode 100644 test/fixtures/wpt/common/gc.js create mode 100644 test/fixtures/wpt/common/proxy-all.sub.pac create mode 100644 test/fixtures/wpt/resources/declarative-shadow-dom-polyfill.js create mode 100644 test/fixtures/wpt/streams/transferable/gc-crash.html diff --git a/test/common/wpt.js b/test/common/wpt.js index 9558b9a4e8d917..57c409f5cc8a0f 100644 --- a/test/common/wpt.js +++ b/test/common/wpt.js @@ -453,14 +453,13 @@ class WPTRunner { this.scriptsModifier = modifier; } - fullInitScript(hasSubsetScript, locationSearchString) { + fullInitScript(url, metaTitle) { let { initScript } = this; - if (hasSubsetScript || locationSearchString) { - initScript = `${initScript}\n\n//===\nglobalThis.location ||= {};`; - } - if (locationSearchString) { - initScript = `${initScript}\n\n//===\nglobalThis.location.search = "${locationSearchString}";`; + initScript = `${initScript}\n\n//===\nglobalThis.location = new URL("${url.href}");`; + + if (metaTitle) { + initScript = `${initScript}\n\n//===\nglobalThis.META_TITLE = "${metaTitle}";`; } if (this.globalThisInitScripts.length === null) { @@ -553,13 +552,13 @@ class WPTRunner { const relativePath = spec.getRelativePath(); 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) { for (const script of meta.script) { - 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), @@ -592,12 +591,13 @@ class WPTRunner { testRelativePath: relativePath, wptRunner: __filename, wptPath: this.path, - initScript: this.fullInitScript(hasSubsetScript, variant), + initScript: this.fullInitScript(new URL(`/${relativePath.replace(/\.js$/, '.html')}${variant}`, 'http://wpt'), meta.title), harness: { code: fs.readFileSync(harnessPath, 'utf8'), filename: harnessPath, }, scriptsToRun, + needsGc, }, }); this.workers.set(testFileName, worker); @@ -749,11 +749,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 34368ab5c5beff..14d3d887aad5eb 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 00000000000000..37efd5ed2016b7 --- /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 00000000000000..c75ce07d054eb7 --- /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 6c34d7e34b93f9..d16f760caeeb2d 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 a67060e7b85eff..a0ca84551dd7cc 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 00000000000000..5992ed1396ca90 --- /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 00000000000000..fe54fb615b1579 --- /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 792b6639c35a26..87710a171a9752 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 00000000000000..2876dcb4ce314e --- /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 00000000000000..1744242b9f3ff1 --- /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 5e0a43f80df3f8..45e8684f0027b2 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 786b7e4199fb45..002aaed40a562e 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 00000000000000..5b69f7ed9821ac --- /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 00000000000000..fc71c6434812e2 --- /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 00000000000000..4b19c69b425188 --- /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 00000000000000..c778ae55bb573b --- /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 00000000000000..9845962090132e --- /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 00000000000000..d06e3170782b7c --- /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 00000000000000..e69ff15e75b590 --- /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 00000000000000..d6812121295bee --- /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 00000000000000..4d0fa113931d34 --- /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 00000000000000..3cb36ab999653b --- /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 00000000000000..28c068bb349c3d --- /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 04069acd3ccbe7..2c249746858918 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 00000000000000..b9cd130a07f77e --- /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 53572ef36c8d1b..53c8cca7e09b8e 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 21b8c5bb1986d5..ce9d680709e058 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 33732fa61fc3dd..69c51113e6b99b 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 9bd8d383df4e1e..54e6a3da5afe9e 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 98b9369b88c322..d32168e9558099 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/919874f84f/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 00000000000000..be9c7ce3bdc3c9 --- /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 00000000000000..cfaafb6e5d6496 --- /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 00000000000000..a0f9f43e622483 --- /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 00000000000000..0b47d66b65f066 --- /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 00000000000000..ea065a6bf11955 --- /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 00000000000000..5fe6a95efaf97d --- /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 00000000000000..8b0030390d0d19 --- /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 00000000000000..ac43a4cfaf7735 --- /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 e9b1e6b0fbe6ae..f2dc8612660495 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 458aae67db0cef..669c17c07b1ae5 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 00000000000000..de601e5d7020e0 --- /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 0a30a197f07f4d..64fe9bfd7f54ae 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 47d12970d393c1..a3ea610e165d0d 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 402ce9bbacf347..96ca280597bf29 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 0a46a1cd6d3182..4a84493f475b01 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 18de6c8c5b32c5..8dadac1d4929d9 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 9cd8abc938d9fb..f14ca3246b8ea2 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 00000000000000..8ab08b0294c297 --- /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 05c4a1affc8699..9484ca6f512ad0 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 d2fb0366c8022a..46aa11e5ca123c 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 0737e64a50b313..76ae2834fdfb0a 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 9ec328222097e0..112790bb1eeb87 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) { @@ -4733,7 +4754,7 @@ if ('META_TITLE' in global_scope && META_TITLE) { return META_TITLE; } - if ('location' in global_scope) { + if ('location' in global_scope && 'pathname' in location) { return location.pathname.substring(location.pathname.lastIndexOf('/') + 1, location.pathname.indexOf('.')); } return "Untitled"; 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 f7e2d06ae5cdf3..e578176777adaf 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 fb34e270ff3718..5ff8fc8cec939a 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 00000000000000..0d331e6be08059 --- /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 f3a49312328e6d..e5ddc22472e141 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": "919874f84ff3703365063e749161a34af38c3d2a", "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 902ac232dd4872..017d931d7abdc4 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 9899c581d9f96e..187b1758741785 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/status/url.json b/test/wpt/status/url.json index a0957dccb53c73..c333559537f6a9 100644 --- a/test/wpt/status/url.json +++ b/test/wpt/status/url.json @@ -9,7 +9,9 @@ "historical.any.js": { "requires": ["small-icu"], "fail": { + "note": "We are faking location with a URL object for the sake of the testharness and it has searchParams.", "expected": [ + "searchParams on location object", "URL: no structured serialize/deserialize support", "URLSearchParams: no structured serialize/deserialize support" ] diff --git a/test/wpt/test-url.js b/test/wpt/test-url.js index f8ba3c3839d642..fc84c22a275c38 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();