diff --git a/test/common/wpt.js b/test/common/wpt.js index 8a4482de7142af..ad31ecbd68c62b 100644 --- a/test/common/wpt.js +++ b/test/common/wpt.js @@ -251,6 +251,9 @@ class StatusRuleSet { // A specification of WPT test class WPTTestSpec { + + #content; + /** * @param {string} mod name of the WPT module, e.g. * 'html/webappapis/microtask-queuing' @@ -298,7 +301,8 @@ class WPTTestSpec { } getContent() { - return fs.readFileSync(this.getAbsolutePath(), 'utf8'); + this.#content ??= fs.readFileSync(this.getAbsolutePath(), 'utf8'); + return this.#content; } } @@ -355,7 +359,7 @@ class StatusLoader { } /** - * Grep for all .*.js file recursively in a directory. + * Grep for all .*.js file recursively in a directory, omitting helpers. * @param {string} dir */ grep(dir) { @@ -374,6 +378,7 @@ class StatusLoader { result.push(filepath); } } + result = result.filter((filepath) => !filepath.endsWith('.helper.js')); return result; } @@ -484,9 +489,7 @@ class WPTRunner { pretendGlobalThisAs(name) { switch (name) { case 'Window': { - this.globalThisInitScripts.push( - `global.Window = Object.getPrototypeOf(globalThis).constructor; - self.GLOBAL.isWorker = () => false;`); + this.globalThisInitScripts.push('globalThis.Window = Object.getPrototypeOf(globalThis).constructor;'); this.loadLazyGlobals(); break; } @@ -526,39 +529,10 @@ class WPTRunner { this.globalThisInitScripts.push(script); } - brandCheckGlobalScopeAttribute(name) { - // TODO(legendecas): idlharness GlobalScope attribute receiver validation. - const script = ` - const desc = Object.getOwnPropertyDescriptor(globalThis, '${name}'); - function getter() { - // Mimic GlobalScope instance brand check. - if (this !== globalThis) { - throw new TypeError('Illegal invocation'); - } - return desc.get(); - } - Object.defineProperty(getter, 'name', { value: 'get ${name}' }); - - function setter(value) { - // Mimic GlobalScope instance brand check. - if (this !== globalThis) { - throw new TypeError('Illegal invocation'); - } - desc.set(value); - } - Object.defineProperty(setter, 'name', { value: 'set ${name}' }); - - Object.defineProperty(globalThis, '${name}', { - get: getter, - set: setter, - }); - `; - this.globalThisInitScripts.push(script); - } - // TODO(joyeecheung): work with the upstream to port more tests in .html // to .js. async runJsTests() { + this.pretendGlobalThisAs('Window'); let queue = []; // If the tests are run as `node test/wpt/test-something.js subset.any.js`, @@ -914,6 +888,17 @@ class WPTRunner { continue; } + if (filename.includes('tentative')) { + this.skip(filename, [ 'test makes assertions not yet required by any specification' ]); + continue; + } + + const isServiceWorker = spec.getContent().includes('importScripts('); + if (isServiceWorker) { + this.skip(filename, [ 'importScripts is not defined' ]); + continue; + } + queue.push(spec); } return queue; diff --git a/test/fixtures/wpt/FileAPI/BlobURL/test2-manual.html b/test/fixtures/wpt/FileAPI/BlobURL/test2-manual.html deleted file mode 100644 index 07fb27ef8af10b..00000000000000 --- a/test/fixtures/wpt/FileAPI/BlobURL/test2-manual.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - Blob and File reference URL Test(2) - - - - - - -
-
-
- -
-

Test steps:

-
    -
  1. Download the file.
  2. -
  3. Select the file in the file inputbox.
  4. -
  5. Delete the file.
  6. -
  7. Click the 'start' button.
  8. -
-
- -
- - - - diff --git a/test/fixtures/wpt/FileAPI/FileReader/progress_event_bubbles_cancelable.html b/test/fixtures/wpt/FileAPI/FileReader/progress_event_bubbles_cancelable.html deleted file mode 100644 index 6a03243f934081..00000000000000 --- a/test/fixtures/wpt/FileAPI/FileReader/progress_event_bubbles_cancelable.html +++ /dev/null @@ -1,33 +0,0 @@ - - -File API Test: Progress Event - bubbles, cancelable - - - - -
- - diff --git a/test/fixtures/wpt/FileAPI/FileReader/support/file_test1.txt b/test/fixtures/wpt/FileAPI/FileReader/support/file_test1.txt deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/test/fixtures/wpt/FileAPI/FileReader/test_errors-manual.html b/test/fixtures/wpt/FileAPI/FileReader/test_errors-manual.html deleted file mode 100644 index b8c3f84d2bf23a..00000000000000 --- a/test/fixtures/wpt/FileAPI/FileReader/test_errors-manual.html +++ /dev/null @@ -1,72 +0,0 @@ - - - - - FileReader Errors Test - - - - - - -
-
-
- -
-

Test steps:

-
    -
  1. Download the file.
  2. -
  3. Select the file in the file inputbox.
  4. -
  5. Delete the file.
  6. -
  7. Click the 'start' button.
  8. -
-
- -
- - - - diff --git a/test/fixtures/wpt/FileAPI/FileReader/test_notreadableerrors-manual.html b/test/fixtures/wpt/FileAPI/FileReader/test_notreadableerrors-manual.html deleted file mode 100644 index 46d73598a0f91a..00000000000000 --- a/test/fixtures/wpt/FileAPI/FileReader/test_notreadableerrors-manual.html +++ /dev/null @@ -1,42 +0,0 @@ - - -FileReader NotReadableError Test - - - - -
-
-
- -
-

Test steps:

-
    -
  1. Download the file.
  2. -
  3. Select the file in the file inputbox.
  4. -
  5. Delete the file's readable permission.
  6. -
  7. Click the 'start' button.
  8. -
-
- - - diff --git a/test/fixtures/wpt/FileAPI/FileReader/test_securityerrors-manual.html b/test/fixtures/wpt/FileAPI/FileReader/test_securityerrors-manual.html deleted file mode 100644 index add93ed69d139a..00000000000000 --- a/test/fixtures/wpt/FileAPI/FileReader/test_securityerrors-manual.html +++ /dev/null @@ -1,40 +0,0 @@ - - -FileReader SecurityError Test - - - - -
-
-
- -
-

Test steps:

-
    -
  1. Select a system sensitive file (e.g. files in /usr/bin, password files, - and other native operating system executables) in the file inputbox.
  2. -
  3. Click the 'start' button.
  4. -
-
- - diff --git a/test/fixtures/wpt/FileAPI/FileReader/workers.html b/test/fixtures/wpt/FileAPI/FileReader/workers.html deleted file mode 100644 index 8e114eeaf86ff5..00000000000000 --- a/test/fixtures/wpt/FileAPI/FileReader/workers.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - diff --git a/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js b/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js deleted file mode 100644 index 3d7a0222f31266..00000000000000 --- a/test/fixtures/wpt/FileAPI/FileReaderSync.worker.js +++ /dev/null @@ -1,56 +0,0 @@ -importScripts("/resources/testharness.js"); - -var blob, empty_blob, readerSync; -setup(() => { - readerSync = new FileReaderSync(); - blob = new Blob(["test"]); - empty_blob = new Blob(); -}); - -test(() => { - assert_true(readerSync instanceof FileReaderSync); -}, "Interface"); - -test(() => { - var text = readerSync.readAsText(blob); - assert_equals(text, "test"); -}, "readAsText"); - -test(() => { - var text = readerSync.readAsText(empty_blob); - assert_equals(text, ""); -}, "readAsText with empty blob"); - -test(() => { - var data = readerSync.readAsDataURL(blob); - assert_equals(data.indexOf("data:"), 0); -}, "readAsDataURL"); - -test(() => { - var data = readerSync.readAsDataURL(empty_blob); - assert_equals(data.indexOf("data:"), 0); -}, "readAsDataURL with empty blob"); - -test(() => { - var data = readerSync.readAsBinaryString(blob); - assert_equals(data, "test"); -}, "readAsBinaryString"); - -test(() => { - var data = readerSync.readAsBinaryString(empty_blob); - assert_equals(data, ""); -}, "readAsBinaryString with empty blob"); - -test(() => { - var data = readerSync.readAsArrayBuffer(blob); - assert_true(data instanceof ArrayBuffer); - assert_equals(data.byteLength, "test".length); -}, "readAsArrayBuffer"); - -test(() => { - var data = readerSync.readAsArrayBuffer(empty_blob); - assert_true(data instanceof ArrayBuffer); - assert_equals(data.byteLength, 0); -}, "readAsArrayBuffer with empty blob"); - -done(); diff --git a/test/fixtures/wpt/FileAPI/META.yml b/test/fixtures/wpt/FileAPI/META.yml deleted file mode 100644 index 506a59fec1eb33..00000000000000 --- a/test/fixtures/wpt/FileAPI/META.yml +++ /dev/null @@ -1,6 +0,0 @@ -spec: https://w3c.github.io/FileAPI/ -suggested_reviewers: - - inexorabletash - - zqzhang - - jdm - - mkruisselbrink 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-constructor.html b/test/fixtures/wpt/FileAPI/blob/Blob-constructor.html deleted file mode 100644 index 62a649aed66418..00000000000000 --- a/test/fixtures/wpt/FileAPI/blob/Blob-constructor.html +++ /dev/null @@ -1,501 +0,0 @@ - - -Blob constructor - - - - - - - -
- 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-slice-overflow.html b/test/fixtures/wpt/FileAPI/blob/Blob-slice-overflow.html deleted file mode 100644 index 74cd83a34f7116..00000000000000 --- a/test/fixtures/wpt/FileAPI/blob/Blob-slice-overflow.html +++ /dev/null @@ -1,42 +0,0 @@ - - -Blob slice overflow - - - - -
- - diff --git a/test/fixtures/wpt/FileAPI/blob/Blob-slice.html b/test/fixtures/wpt/FileAPI/blob/Blob-slice.html deleted file mode 100644 index 03fe6ca5343bd1..00000000000000 --- a/test/fixtures/wpt/FileAPI/blob/Blob-slice.html +++ /dev/null @@ -1,238 +0,0 @@ - - -Blob slice - - - - - -
- 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.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/file/File-constructor.html b/test/fixtures/wpt/FileAPI/file/File-constructor.html deleted file mode 100644 index 3477e4ada16e92..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/File-constructor.html +++ /dev/null @@ -1,159 +0,0 @@ - - -File constructor - - - -
- diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-controls.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-controls.tentative.html deleted file mode 100644 index d11f4a860931b4..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/send-file-form-controls.tentative.html +++ /dev/null @@ -1,117 +0,0 @@ - - -Upload files named using controls (tentative) - - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-iso-2022-jp.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-iso-2022-jp.tentative.html deleted file mode 100644 index 659af3bde85852..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/send-file-form-iso-2022-jp.tentative.html +++ /dev/null @@ -1,72 +0,0 @@ - - - -Upload files in ISO-2022-JP form (tentative) - - - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-punctuation.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-punctuation.tentative.html deleted file mode 100644 index 5c2d6d0bf1fe01..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/send-file-form-punctuation.tentative.html +++ /dev/null @@ -1,230 +0,0 @@ - - -Upload files named using punctuation (tentative) - - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-windows-1252.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-windows-1252.tentative.html deleted file mode 100644 index a2c37186b3e023..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/send-file-form-windows-1252.tentative.html +++ /dev/null @@ -1,69 +0,0 @@ - - -Upload files in Windows-1252 form (tentative) - - - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/file/send-file-form-x-user-defined.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-form-x-user-defined.tentative.html deleted file mode 100644 index 503b08a51706f7..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/send-file-form-x-user-defined.tentative.html +++ /dev/null @@ -1,70 +0,0 @@ - - -Upload files in x-user-defined form (tentative) - - - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.tentative.html deleted file mode 100644 index 4259741b63ef31..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/send-file-formdata-controls.tentative.html +++ /dev/null @@ -1,93 +0,0 @@ - - -FormData: Upload files named using controls (tentative) - - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.tentative.html b/test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.tentative.html deleted file mode 100644 index d8e84e9d978094..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/send-file-formdata-punctuation.tentative.html +++ /dev/null @@ -1,168 +0,0 @@ - - -FormData: Upload files named using punctuation (tentative) - - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.html b/test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.html deleted file mode 100644 index 7a7f6cefe776b9..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/send-file-formdata-utf-8.html +++ /dev/null @@ -1,53 +0,0 @@ - - -FormData: Upload files in UTF-8 fetch() - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/file/send-file-formdata.html b/test/fixtures/wpt/FileAPI/file/send-file-formdata.html deleted file mode 100644 index 77e048e54741c0..00000000000000 --- a/test/fixtures/wpt/FileAPI/file/send-file-formdata.html +++ /dev/null @@ -1,28 +0,0 @@ - - -FormData: Upload ASCII-named file in UTF-8 form - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/fileReader.html b/test/fixtures/wpt/FileAPI/fileReader.html deleted file mode 100644 index b767e22d4a66eb..00000000000000 --- a/test/fixtures/wpt/FileAPI/fileReader.html +++ /dev/null @@ -1,67 +0,0 @@ - - - - FileReader States - - - - - - -
- - - diff --git a/test/fixtures/wpt/FileAPI/filelist-section/filelist.html b/test/fixtures/wpt/FileAPI/filelist-section/filelist.html deleted file mode 100644 index b97dcde19f647c..00000000000000 --- a/test/fixtures/wpt/FileAPI/filelist-section/filelist.html +++ /dev/null @@ -1,57 +0,0 @@ - - - - - FileAPI Test: filelist - - - - - - - - - -
- -
-
- - - - - diff --git a/test/fixtures/wpt/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html b/test/fixtures/wpt/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html deleted file mode 100644 index 2efaa059fa4897..00000000000000 --- a/test/fixtures/wpt/FileAPI/filelist-section/filelist_multiple_selected_files-manual.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - FileAPI Test: filelist_multiple_selected_files - - - - - - - - - -
- -
-
-

Test steps:

-
    -
  1. Download upload.txt, upload.zip to local.
  2. -
  3. Select the local two files (upload.txt, upload.zip) to run the test.
  4. -
-
- -
- - - - diff --git a/test/fixtures/wpt/FileAPI/filelist-section/filelist_selected_file-manual.html b/test/fixtures/wpt/FileAPI/filelist-section/filelist_selected_file-manual.html deleted file mode 100644 index 966aadda615589..00000000000000 --- a/test/fixtures/wpt/FileAPI/filelist-section/filelist_selected_file-manual.html +++ /dev/null @@ -1,64 +0,0 @@ - - - - - FileAPI Test: filelist_selected_file - - - - - - - - - -
- -
-
-

Test steps:

-
    -
  1. Download upload.txt to local.
  2. -
  3. Select the local upload.txt file to run the test.
  4. -
-
- -
- - - - diff --git a/test/fixtures/wpt/FileAPI/filelist-section/support/upload.txt b/test/fixtures/wpt/FileAPI/filelist-section/support/upload.txt deleted file mode 100644 index f45965b711f127..00000000000000 --- a/test/fixtures/wpt/FileAPI/filelist-section/support/upload.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, this is test file for file upload. diff --git a/test/fixtures/wpt/FileAPI/filelist-section/support/upload.zip b/test/fixtures/wpt/FileAPI/filelist-section/support/upload.zip deleted file mode 100644 index a933d6a949428c..00000000000000 Binary files a/test/fixtures/wpt/FileAPI/filelist-section/support/upload.zip and /dev/null differ diff --git a/test/fixtures/wpt/FileAPI/historical.https.html b/test/fixtures/wpt/FileAPI/historical.https.html deleted file mode 100644 index 4f841f17639459..00000000000000 --- a/test/fixtures/wpt/FileAPI/historical.https.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - Historical features - - - - - -
- - - diff --git a/test/fixtures/wpt/FileAPI/idlharness-manual.html b/test/fixtures/wpt/FileAPI/idlharness-manual.html deleted file mode 100644 index c1d8b0c7149d75..00000000000000 --- a/test/fixtures/wpt/FileAPI/idlharness-manual.html +++ /dev/null @@ -1,45 +0,0 @@ - - - - - File API manual IDL tests - - - - - - - - -

File API manual IDL tests

- -

Either download upload.txt and select it below or select an - arbitrary local file.

- -
- -
- -
- - - - diff --git a/test/fixtures/wpt/FileAPI/idlharness.html b/test/fixtures/wpt/FileAPI/idlharness.html deleted file mode 100644 index 5e0a43f80df3f8..00000000000000 --- a/test/fixtures/wpt/FileAPI/idlharness.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - - File API automated IDL tests - - - - - - - - -

File API automated IDL tests

- -
- -
- -
- - - - - diff --git a/test/fixtures/wpt/FileAPI/idlharness.worker.js b/test/fixtures/wpt/FileAPI/idlharness.worker.js deleted file mode 100644 index 786b7e4199fb45..00000000000000 --- a/test/fixtures/wpt/FileAPI/idlharness.worker.js +++ /dev/null @@ -1,20 +0,0 @@ -importScripts("/resources/testharness.js"); -importScripts("/resources/WebIDLParser.js", "/resources/idlharness.js"); - -'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()'], - FileReaderSync: ['new FileReaderSync()'] - }); - } -); -done(); diff --git a/test/fixtures/wpt/FileAPI/progress-manual.html b/test/fixtures/wpt/FileAPI/progress-manual.html deleted file mode 100644 index b2e03b3eb27387..00000000000000 --- a/test/fixtures/wpt/FileAPI/progress-manual.html +++ /dev/null @@ -1,49 +0,0 @@ - - -Process Events for FileReader - - - - -Please choose one file through this input below.
- -
- diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.html b/test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.html deleted file mode 100644 index d65ae9db18a1ff..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/Determining-Encoding.html +++ /dev/null @@ -1,91 +0,0 @@ - - -FileAPI Test: Blob Determining Encoding - - - - - -
- diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.html b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.html deleted file mode 100644 index 86657b5711aff1..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-event-handler-attributes.html +++ /dev/null @@ -1,23 +0,0 @@ - - -FileReader event handler attributes - - -
- diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.html b/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.html deleted file mode 100644 index e7279fe4bd445e..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/FileReader-multiple-reads.html +++ /dev/null @@ -1,89 +0,0 @@ -๏ปฟ -FileReader: starting new reads while one is in progress - - - - -
- diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.html deleted file mode 100644 index 940a775d35bf42..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_abort.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - FileAPI Test: filereader_abort - - - - - - -
- - - - diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.html deleted file mode 100644 index cf4524825b80ca..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_error.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - FileAPI Test: filereader_error - - - - - - - -
- - - - diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_events.any.js b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_events.any.js deleted file mode 100644 index ac692907d119f7..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_events.any.js +++ /dev/null @@ -1,19 +0,0 @@ -promise_test(async t => { - var reader = new FileReader(); - var eventWatcher = new EventWatcher(t, reader, ['loadstart', 'progress', 'abort', 'error', 'load', 'loadend']); - reader.readAsText(new Blob([])); - await eventWatcher.wait_for('loadstart'); - // No progress event for an empty blob, as no data is loaded. - await eventWatcher.wait_for('load'); - await eventWatcher.wait_for('loadend'); -}, 'events are dispatched in the correct order for an empty blob'); - -promise_test(async t => { - var reader = new FileReader(); - var eventWatcher = new EventWatcher(t, reader, ['loadstart', 'progress', 'abort', 'error', 'load', 'loadend']); - reader.readAsText(new Blob(['a'])); - await eventWatcher.wait_for('loadstart'); - await eventWatcher.wait_for('progress'); - await eventWatcher.wait_for('load'); - await eventWatcher.wait_for('loadend'); -}, 'events are dispatched in the correct order for a non-empty blob'); diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file-manual.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file-manual.html deleted file mode 100644 index 702ca9afd7b067..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file-manual.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - FileAPI Test: filereader_file - - - - - - - -
-

Test step:

-
    -
  1. Download blue-100x100.png to local.
  2. -
  3. Select the local file (blue-100x100.png) to run the test.
  4. -
-
- -
- -
- -
- - - diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file_img-manual.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file_img-manual.html deleted file mode 100644 index fca42c7fceba48..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_file_img-manual.html +++ /dev/null @@ -1,47 +0,0 @@ - - - - - FileAPI Test: filereader_file_img - - - - - - - -
-

Test step:

-
    -
  1. Download blue-100x100.png to local.
  2. -
  3. Select the local file (blue-100x100.png) to run the test.
  4. -
-
- -
- -
- -
- - - diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.html deleted file mode 100644 index 31001a51a0727f..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsArrayBuffer.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - FileAPI Test: filereader_readAsArrayBuffer - - - - - - -
- - - - diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.html deleted file mode 100644 index b550e4d0a96dc7..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsBinaryString.html +++ /dev/null @@ -1,32 +0,0 @@ - - -FileAPI Test: filereader_readAsBinaryString - - - - - diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.html deleted file mode 100644 index 5bc39499a229d1..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsDataURL.html +++ /dev/null @@ -1,51 +0,0 @@ - - -FileAPI Test: FileReader.readAsDataURL - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.html deleted file mode 100644 index 7d639d0111473b..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readAsText.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - FileAPI Test: filereader_readAsText - - - - - - -
- - - - diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.html deleted file mode 100644 index 1586b8995059f7..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_readystate.html +++ /dev/null @@ -1,34 +0,0 @@ - - - - - FileAPI Test: filereader_readystate - - - - - - -
- - - - diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.html b/test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.html deleted file mode 100644 index b80322ed424f83..00000000000000 --- a/test/fixtures/wpt/FileAPI/reading-data-section/filereader_result.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - FileAPI Test: filereader_result - - - - - - -
- - - - diff --git a/test/fixtures/wpt/FileAPI/reading-data-section/support/blue-100x100.png b/test/fixtures/wpt/FileAPI/reading-data-section/support/blue-100x100.png deleted file mode 100644 index 5748719ff22a34..00000000000000 Binary files a/test/fixtures/wpt/FileAPI/reading-data-section/support/blue-100x100.png and /dev/null differ 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/unicode.html b/test/fixtures/wpt/FileAPI/unicode.html deleted file mode 100644 index ce3e3579d7c2c7..00000000000000 --- a/test/fixtures/wpt/FileAPI/unicode.html +++ /dev/null @@ -1,46 +0,0 @@ - - -Blob/Unicode interaction: normalization and encoding - - - diff --git a/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html b/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html deleted file mode 100644 index 21b8c5bb1986d5..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/cross-global-revoke.sub.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/multi-global-origin-serialization.sub.html b/test/fixtures/wpt/FileAPI/url/multi-global-origin-serialization.sub.html deleted file mode 100644 index 0052b26fa62130..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/multi-global-origin-serialization.sub.html +++ /dev/null @@ -1,26 +0,0 @@ - - -Blob URL serialization (specifically the origin) in multi-global situations - - - - - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/url/resources/create-helper.html b/test/fixtures/wpt/FileAPI/url/resources/create-helper.html deleted file mode 100644 index fa6cf4e671e835..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/resources/create-helper.html +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/resources/create-helper.js b/test/fixtures/wpt/FileAPI/url/resources/create-helper.js deleted file mode 100644 index e6344f700ced60..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/resources/create-helper.js +++ /dev/null @@ -1,4 +0,0 @@ -self.addEventListener('message', e => { - let url = URL.createObjectURL(e.data.blob); - self.postMessage({url: url}); -}); diff --git a/test/fixtures/wpt/FileAPI/url/resources/fetch-tests.js b/test/fixtures/wpt/FileAPI/url/resources/fetch-tests.js deleted file mode 100644 index a81ea1e7b1de35..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/resources/fetch-tests.js +++ /dev/null @@ -1,71 +0,0 @@ -// This method generates a number of tests verifying fetching of blob URLs, -// allowing the same tests to be used both with fetch() and XMLHttpRequest. -// -// |fetch_method| is only used in test names, and should describe the -// (javascript) method being used by the other two arguments (i.e. 'fetch' or 'XHR'). -// -// |fetch_should_succeed| is a callback that is called with the Test and a URL. -// Fetching the URL is expected to succeed. The callback should return a promise -// resolved with whatever contents were fetched. -// -// |fetch_should_fail| similarly is a callback that is called with the Test, a URL -// to fetch, and optionally a method to use to do the fetch. If no method is -// specified the callback should use the 'GET' method. Fetching of these URLs is -// expected to fail, and the callback should return a promise that resolves iff -// fetching did indeed fail. -function fetch_tests(fetch_method, fetch_should_succeed, fetch_should_fail) { - const blob_contents = 'test blob contents'; - const blob = new Blob([blob_contents]); - - promise_test(t => { - const url = URL.createObjectURL(blob); - - return fetch_should_succeed(t, url).then(text => { - assert_equals(text, blob_contents); - }); - }, 'Blob URLs can be used in ' + fetch_method); - - promise_test(t => { - const url = URL.createObjectURL(blob); - - return fetch_should_succeed(t, url + '#fragment').then(text => { - assert_equals(text, blob_contents); - }); - }, fetch_method + ' with a fragment should succeed'); - - promise_test(t => { - const url = URL.createObjectURL(blob); - URL.revokeObjectURL(url); - - return fetch_should_fail(t, url); - }, fetch_method + ' of a revoked URL should fail'); - - promise_test(t => { - const url = URL.createObjectURL(blob); - URL.revokeObjectURL(url + '#fragment'); - - return fetch_should_succeed(t, url).then(text => { - assert_equals(text, blob_contents); - }); - }, 'Only exact matches should revoke URLs, using ' + fetch_method); - - promise_test(t => { - const url = URL.createObjectURL(blob); - - return fetch_should_fail(t, url + '?querystring'); - }, 'Appending a query string should cause ' + fetch_method + ' to fail'); - - promise_test(t => { - const url = URL.createObjectURL(blob); - - return fetch_should_fail(t, url + '/path'); - }, 'Appending a path should cause ' + fetch_method + ' to fail'); - - for (const method of ['HEAD', 'POST', 'DELETE', 'OPTIONS', 'PUT', 'CUSTOM']) { - const url = URL.createObjectURL(blob); - - promise_test(t => { - return fetch_should_fail(t, url, method); - }, fetch_method + ' with method "' + method + '" should fail'); - } -} \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.html b/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.html deleted file mode 100644 index adf5a014a668d6..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.html +++ /dev/null @@ -1,7 +0,0 @@ - - \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.js b/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.js deleted file mode 100644 index c3e05b64b1a6c8..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/resources/revoke-helper.js +++ /dev/null @@ -1,9 +0,0 @@ -self.addEventListener('message', e => { - URL.revokeObjectURL(e.data.url); - // Registering a new object URL will make absolutely sure that the revocation - // has propagated. Without this at least in chrome it is possible for the - // below postMessage to arrive at its destination before the revocation has - // been fully processed. - URL.createObjectURL(new Blob([])); - self.postMessage('revoked'); -}); diff --git a/test/fixtures/wpt/FileAPI/url/sandboxed-iframe.html b/test/fixtures/wpt/FileAPI/url/sandboxed-iframe.html deleted file mode 100644 index a52939a3eb297c..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/sandboxed-iframe.html +++ /dev/null @@ -1,32 +0,0 @@ - - -FileAPI Test: Verify behavior of Blob URL in unique origins - - - - - - - diff --git a/test/fixtures/wpt/FileAPI/url/unicode-origin.sub.html b/test/fixtures/wpt/FileAPI/url/unicode-origin.sub.html deleted file mode 100644 index 2c4921c0344998..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/unicode-origin.sub.html +++ /dev/null @@ -1,23 +0,0 @@ - - -FileAPI Test: Verify origin of Blob URL - - - - diff --git a/test/fixtures/wpt/FileAPI/url/url-charset.window.js b/test/fixtures/wpt/FileAPI/url/url-charset.window.js deleted file mode 100644 index 777709b64a50e5..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url-charset.window.js +++ /dev/null @@ -1,34 +0,0 @@ -async_test(t => { - // This could be detected as ISO-2022-JP, in which case there would be no - // bbb` - ], - {type: 'text/html;charset=utf-8'}); - const url = URL.createObjectURL(blob); - const win = window.open(url); - t.add_cleanup(() => { - win.close(); - }); - - win.onload = t.step_func_done(() => { - assert_equals(win.document.charset, 'UTF-8'); - }); -}, 'Blob charset should override any auto-detected charset.'); - -async_test(t => { - const blob = new Blob( - [`\n`], - {type: 'text/html;charset=utf-8'}); - const url = URL.createObjectURL(blob); - const win = window.open(url); - t.add_cleanup(() => { - win.close(); - }); - - win.onload = t.step_func_done(() => { - assert_equals(win.document.charset, 'UTF-8'); - }); -}, 'Blob charset should override .'); diff --git a/test/fixtures/wpt/FileAPI/url/url-format.any.js b/test/fixtures/wpt/FileAPI/url/url-format.any.js deleted file mode 100644 index 33732fa61fc3dd..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url-format.any.js +++ /dev/null @@ -1,64 +0,0 @@ -// META: timeout=long -const blob = new Blob(['test']); -const file = new File(['test'], 'name'); - -test(() => { - const url_count = 5000; - let list = []; - - for (let i = 0; i < url_count; ++i) - list.push(URL.createObjectURL(blob)); - - list.sort(); - - for (let i = 1; i < list.length; ++i) - assert_not_equals(list[i], list[i-1], 'generated Blob URLs should be unique'); -}, 'Generated Blob URLs are unique'); - -test(() => { - const url = URL.createObjectURL(blob); - assert_equals(typeof url, 'string'); - assert_true(url.startsWith('blob:')); -}, 'Blob URL starts with "blob:"'); - -test(() => { - const url = URL.createObjectURL(file); - assert_equals(typeof url, 'string'); - assert_true(url.startsWith('blob:')); -}, 'Blob URL starts with "blob:" for Files'); - -test(() => { - const url = URL.createObjectURL(blob); - assert_equals(new URL(url).origin, location.origin); - if (location.origin !== 'null') { - assert_true(url.includes(location.origin)); - assert_true(url.startsWith('blob:' + location.protocol)); - } -}, 'Origin of Blob URL matches our origin'); - -test(() => { - const url = URL.createObjectURL(blob); - const url_record = new URL(url); - assert_equals(url_record.protocol, 'blob:'); - assert_equals(url_record.origin, location.origin); - assert_equals(url_record.host, '', 'host should be an empty string'); - assert_equals(url_record.port, '', 'port should be an empty string'); - const uuid_path_re = /\/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - assert_true(uuid_path_re.test(url_record.pathname), 'Path must end with a valid UUID'); - if (location.origin !== 'null') { - const nested_url = new URL(url_record.pathname); - assert_equals(nested_url.origin, location.origin); - assert_equals(nested_url.pathname.search(uuid_path_re), 0, 'Path must be a valid UUID'); - assert_true(url.includes(location.origin)); - assert_true(url.startsWith('blob:' + location.protocol)); - } -}, 'Blob URL parses correctly'); - -test(() => { - const url = URL.createObjectURL(file); - assert_equals(new URL(url).origin, location.origin); - if (location.origin !== 'null') { - assert_true(url.includes(location.origin)); - assert_true(url.startsWith('blob:' + location.protocol)); - } -}, 'Origin of Blob URL matches our origin for Files'); diff --git a/test/fixtures/wpt/FileAPI/url/url-in-tags-revoke.window.js b/test/fixtures/wpt/FileAPI/url/url-in-tags-revoke.window.js deleted file mode 100644 index 1cdad79f7e34e0..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url-in-tags-revoke.window.js +++ /dev/null @@ -1,115 +0,0 @@ -// META: timeout=long -async_test(t => { - const run_result = 'test_frame_OK'; - const blob_contents = '\n\n' + - ''; - const blob = new Blob([blob_contents], {type: 'text/html'}); - const url = URL.createObjectURL(blob); - - const frame = document.createElement('iframe'); - frame.setAttribute('src', url); - frame.setAttribute('style', 'display:none;'); - document.body.appendChild(frame); - URL.revokeObjectURL(url); - - frame.onload = t.step_func_done(() => { - assert_equals(frame.contentWindow.test_result, run_result); - }); -}, 'Fetching a blob URL immediately before revoking it works in an iframe.'); - -async_test(t => { - const run_result = 'test_frame_OK'; - const blob_contents = '\n\n' + - ''; - const blob = new Blob([blob_contents], {type: 'text/html'}); - const url = URL.createObjectURL(blob); - - const frame = document.createElement('iframe'); - frame.setAttribute('src', '/common/blank.html'); - frame.setAttribute('style', 'display:none;'); - document.body.appendChild(frame); - - frame.onload = t.step_func(() => { - frame.contentWindow.location = url; - URL.revokeObjectURL(url); - frame.onload = t.step_func_done(() => { - assert_equals(frame.contentWindow.test_result, run_result); - }); - }); -}, 'Fetching a blob URL immediately before revoking it works in an iframe navigation.'); - -async_test(t => { - const run_result = 'test_frame_OK'; - const blob_contents = '\n\n' + - ''; - const blob = new Blob([blob_contents], {type: 'text/html'}); - const url = URL.createObjectURL(blob); - const win = window.open(url); - URL.revokeObjectURL(url); - add_completion_callback(() => { win.close(); }); - - win.onload = t.step_func_done(() => { - assert_equals(win.test_result, run_result); - }); -}, 'Opening a blob URL in a new window immediately before revoking it works.'); - -function receive_message_on_channel(t, channel_name) { - const channel = new BroadcastChannel(channel_name); - return new Promise(resolve => { - channel.addEventListener('message', t.step_func(e => { - resolve(e.data); - })); - }); -} - -function window_contents_for_channel(channel_name) { - return '\n' + - ''; -} - -async_test(t => { - const channel_name = 'noopener-window-test'; - const blob = new Blob([window_contents_for_channel(channel_name)], {type: 'text/html'}); - receive_message_on_channel(t, channel_name).then(t.step_func_done(t => { - assert_equals(t, 'foobar'); - })); - const url = URL.createObjectURL(blob); - const win = window.open(); - win.opener = null; - win.location = url; - URL.revokeObjectURL(url); -}, 'Opening a blob URL in a noopener about:blank window immediately before revoking it works.'); - -async_test(t => { - const run_result = 'test_script_OK'; - const blob_contents = 'window.script_test_result = "' + run_result + '";'; - const blob = new Blob([blob_contents]); - const url = URL.createObjectURL(blob); - - const e = document.createElement('script'); - e.setAttribute('src', url); - e.onload = t.step_func_done(() => { - assert_equals(window.script_test_result, run_result); - }); - - document.body.appendChild(e); - URL.revokeObjectURL(url); -}, 'Fetching a blob URL immediately before revoking it works in '; - const blob = new Blob([blob_contents], {type: 'text/html'}); - const url = URL.createObjectURL(blob); - - const frame = document.createElement('iframe'); - frame.setAttribute('src', url); - frame.setAttribute('style', 'display:none;'); - document.body.appendChild(frame); - - frame.onload = t.step_func_done(() => { - assert_equals(frame.contentWindow.test_result, run_result); - }); -}, 'Blob URLs can be used in iframes, and are treated same origin'); - -async_test(t => { - const blob_contents = '\n\n' + - '\n' + - '\n' + - '
\n' + - '
'; - const blob = new Blob([blob_contents], {type: 'text/html'}); - const url = URL.createObjectURL(blob); - - const frame = document.createElement('iframe'); - frame.setAttribute('src', url + '#block2'); - document.body.appendChild(frame); - frame.contentWindow.onscroll = t.step_func_done(() => { - assert_equals(frame.contentWindow.scrollY, 5000); - }); -}, 'Blob URL fragment is implemented.'); diff --git a/test/fixtures/wpt/FileAPI/url/url-lifetime.html b/test/fixtures/wpt/FileAPI/url/url-lifetime.html deleted file mode 100644 index ad5d667193a3d0..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url-lifetime.html +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/url/url-reload.window.js b/test/fixtures/wpt/FileAPI/url/url-reload.window.js deleted file mode 100644 index d333b3a74aa82c..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url-reload.window.js +++ /dev/null @@ -1,36 +0,0 @@ -function blob_url_reload_test(t, revoke_before_reload) { - const run_result = 'test_frame_OK'; - const blob_contents = '\n\n' + - ''; - const blob = new Blob([blob_contents], {type: 'text/html'}); - const url = URL.createObjectURL(blob); - - const frame = document.createElement('iframe'); - frame.setAttribute('src', url); - frame.setAttribute('style', 'display:none;'); - document.body.appendChild(frame); - - frame.onload = t.step_func(() => { - if (revoke_before_reload) - URL.revokeObjectURL(url); - assert_equals(frame.contentWindow.test_result, run_result); - frame.contentWindow.test_result = null; - frame.onload = t.step_func_done(() => { - assert_equals(frame.contentWindow.test_result, run_result); - }); - // Slight delay before reloading to ensure revoke actually has had a chance - // to be processed. - t.step_timeout(() => { - frame.contentWindow.location.reload(); - }, 250); - }); -} - -async_test(t => { - blob_url_reload_test(t, false); -}, 'Reloading a blob URL succeeds.'); - - -async_test(t => { - blob_url_reload_test(t, true); -}, 'Reloading a blob URL succeeds even if the URL was revoked.'); diff --git a/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js b/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js deleted file mode 100644 index 9bd8d383df4e1e..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url-with-fetch.any.js +++ /dev/null @@ -1,53 +0,0 @@ -// META: script=resources/fetch-tests.js - -function fetch_should_succeed(test, request) { - return fetch(request).then(response => response.text()); -} - -function fetch_should_fail(test, url, method = 'GET') { - return promise_rejects_js(test, TypeError, fetch(url, {method: method})); -} - -fetch_tests('fetch', fetch_should_succeed, fetch_should_fail); - -promise_test(t => { - const blob_contents = 'test blob contents'; - const blob_type = 'image/png'; - const blob = new Blob([blob_contents], {type: blob_type}); - const url = URL.createObjectURL(blob); - - return fetch(url).then(response => { - assert_equals(response.headers.get('Content-Type'), blob_type); - }); -}, 'fetch should return Content-Type from Blob'); - -promise_test(t => { - const blob_contents = 'test blob contents'; - const blob = new Blob([blob_contents]); - const url = URL.createObjectURL(blob); - const 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); - - return fetch_should_succeed(t, request).then(text => { - assert_equals(text, blob_contents); - }); -}, 'Revoke blob URL after creating Request, will fetch'); - -promise_test(function(t) { - const blob_contents = 'test blob contents'; - const blob = new Blob([blob_contents]); - const url = URL.createObjectURL(blob); - - const result = fetch_should_succeed(t, url).then(text => { - assert_equals(text, blob_contents); - }); - - // Revoke the object URL. fetch should have already resolved the blob URL. - URL.revokeObjectURL(url); - - return result; -}, 'Revoke blob URL after calling fetch, fetch should succeed'); diff --git a/test/fixtures/wpt/FileAPI/url/url-with-xhr.any.js b/test/fixtures/wpt/FileAPI/url/url-with-xhr.any.js deleted file mode 100644 index 29d83080ab5845..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url-with-xhr.any.js +++ /dev/null @@ -1,68 +0,0 @@ -// META: script=resources/fetch-tests.js - -function xhr_should_succeed(test, url) { - return new Promise((resolve, reject) => { - const xhr = new XMLHttpRequest(); - xhr.open('GET', url); - xhr.onload = test.step_func(() => { - assert_equals(xhr.status, 200); - assert_equals(xhr.statusText, 'OK'); - resolve(xhr.response); - }); - xhr.onerror = () => reject('Got unexpected error event'); - xhr.send(); - }); -} - -function xhr_should_fail(test, url, method = 'GET') { - const xhr = new XMLHttpRequest(); - xhr.open(method, url); - const result1 = new Promise((resolve, reject) => { - xhr.onload = () => reject('Got unexpected load event'); - xhr.onerror = resolve; - }); - const result2 = new Promise(resolve => { - xhr.onreadystatechange = test.step_func(() => { - if (xhr.readyState !== xhr.DONE) return; - assert_equals(xhr.status, 0); - resolve(); - }); - }); - xhr.send(); - return Promise.all([result1, result2]); -} - -fetch_tests('XHR', xhr_should_succeed, xhr_should_fail); - -async_test(t => { - const blob_contents = 'test blob contents'; - const blob_type = 'image/png'; - const blob = new Blob([blob_contents], {type: blob_type}); - const url = URL.createObjectURL(blob); - const xhr = new XMLHttpRequest(); - xhr.open('GET', url); - xhr.onloadend = t.step_func_done(() => { - assert_equals(xhr.getResponseHeader('Content-Type'), blob_type); - }); - xhr.send(); -}, 'XHR should return Content-Type from Blob'); - -async_test(t => { - const blob_contents = 'test blob contents'; - const blob = new Blob([blob_contents]); - const url = URL.createObjectURL(blob); - const xhr = new XMLHttpRequest(); - xhr.open('GET', url); - - // Revoke the object URL. XHR 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 send(). - URL.revokeObjectURL(url); - - xhr.onload = t.step_func_done(() => { - assert_equals(xhr.response, blob_contents); - }); - xhr.onerror = t.unreached_func('Got unexpected error event'); - - xhr.send(); -}, 'Revoke blob URL after open(), will fetch'); diff --git a/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file-manual.html b/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file-manual.html deleted file mode 100644 index 7ae32512e07c76..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file-manual.html +++ /dev/null @@ -1,45 +0,0 @@ - - -FileAPI Test: Creating Blob URL with File - - - - - - -
-

Test steps:

-
    -
  1. Download blue96x96.png to local.
  2. -
  3. Select the local file (blue96x96.png) to run the test.
  4. -
-
- -
- -
- -
- - - diff --git a/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file_img-manual.html b/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file_img-manual.html deleted file mode 100644 index 534c1de9968da8..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url_createobjecturl_file_img-manual.html +++ /dev/null @@ -1,28 +0,0 @@ - - -FileAPI Test: Creating Blob URL with File as image source - - - -
-

Test steps:

-
    -
  1. Download blue96x96.png to local.
  2. -
  3. Select the local file (blue96x96.png) to run the test.
  4. -
-

Pass/fail criteria:

-

Test passes if there is a filled blue square.

- -

-

-
- - - diff --git a/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img-ref.html b/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img-ref.html deleted file mode 100644 index 7d7390442d3631..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img-ref.html +++ /dev/null @@ -1,12 +0,0 @@ - - -FileAPI Reference File - - - -

Test passes if there is a filled blue square.

- -

- -

- diff --git a/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img.html b/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img.html deleted file mode 100644 index 468dcb086d770a..00000000000000 --- a/test/fixtures/wpt/FileAPI/url/url_xmlhttprequest_img.html +++ /dev/null @@ -1,27 +0,0 @@ - - - -FileAPI Test: Creating Blob URL via XMLHttpRequest as image source - - - - -

Test passes if there is a filled blue square.

- -

- -

- - - - diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 958f67474d931e..f9a4fc00e317f3 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -10,27 +10,28 @@ 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/779d175c40/encoding +- dom/abort: https://github.com/web-platform-tests/wpt/tree/e8cba6918a/dom/abort +- dom/events: https://github.com/web-platform-tests/wpt/tree/c821e5c4e8/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/blob: https://github.com/web-platform-tests/wpt/tree/c9a3667cce/FileAPI/blob - FileAPI/file: https://github.com/web-platform-tests/wpt/tree/c01f637cca/FileAPI/file +- FileAPI/support: https://github.com/web-platform-tests/wpt/tree/049c26522d/FileAPI/support - 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 -- html/webappapis/structured-clone: https://github.com/web-platform-tests/wpt/tree/47d3fb280c/html/webappapis/structured-clone -- html/webappapis/timers: https://github.com/web-platform-tests/wpt/tree/5873f2d8f1/html/webappapis/timers -- 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 -- url: https://github.com/web-platform-tests/wpt/tree/f1ade799d0/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 +- html/webappapis/microtask-queuing: https://github.com/web-platform-tests/wpt/tree/22ecfc9bac/html/webappapis/microtask-queuing +- html/webappapis/structured-clone: https://github.com/web-platform-tests/wpt/tree/5656a2f465/html/webappapis/structured-clone +- html/webappapis/timers: https://github.com/web-platform-tests/wpt/tree/22ecfc9bac/html/webappapis/timers +- interfaces: https://github.com/web-platform-tests/wpt/tree/4304540ff0/interfaces +- performance-timeline: https://github.com/web-platform-tests/wpt/tree/b6f6bf16fe/performance-timeline +- resource-timing: https://github.com/web-platform-tests/wpt/tree/a8d60f5514/resource-timing +- resources: https://github.com/web-platform-tests/wpt/tree/bbea8dcb1c/resources +- streams: https://github.com/web-platform-tests/wpt/tree/e97fac4791/streams +- url: https://github.com/web-platform-tests/wpt/tree/e257f628d3/url +- user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing +- wasm/jsapi: https://github.com/web-platform-tests/wpt/tree/cde25e7e3c/wasm/jsapi - wasm/webapi: https://github.com/web-platform-tests/wpt/tree/fd1b23eeaa/wasm/webapi - WebCryptoAPI: https://github.com/web-platform-tests/wpt/tree/238d9d9bac/WebCryptoAPI - webidl/ecmascript-binding/es-exceptions: https://github.com/web-platform-tests/wpt/tree/a370aad338/webidl/ecmascript-binding/es-exceptions 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/dom/abort/abort-signal-any.tentative.any.js b/test/fixtures/wpt/dom/abort/abort-signal-any.tentative.any.js new file mode 100644 index 00000000000000..b4abb14c1a3eff --- /dev/null +++ b/test/fixtures/wpt/dom/abort/abort-signal-any.tentative.any.js @@ -0,0 +1,4 @@ +// META: script=./resources/abort-signal-any-tests.js + +abortSignalAnySignalOnlyTests(AbortSignal); +abortSignalAnyTests(AbortSignal, AbortController); diff --git a/test/fixtures/wpt/dom/abort/resources/abort-signal-any-tests.js b/test/fixtures/wpt/dom/abort/resources/abort-signal-any-tests.js new file mode 100644 index 00000000000000..66e4141eaccb08 --- /dev/null +++ b/test/fixtures/wpt/dom/abort/resources/abort-signal-any-tests.js @@ -0,0 +1,185 @@ +// Tests for AbortSignal.any() and subclasses that don't use a controller. +function abortSignalAnySignalOnlyTests(signalInterface) { + const desc = `${signalInterface.name}.any()` + + test(t => { + const signal = signalInterface.any([]); + assert_false(signal.aborted); + }, `${desc} works with an empty array of signals`); +} + +// Tests for AbortSignal.any() and subclasses that use a controller. +function abortSignalAnyTests(signalInterface, controllerInterface) { + const suffix = `(using ${controllerInterface.name})`; + const desc = `${signalInterface.name}.any()`; + + test(t => { + const controller = new controllerInterface(); + const signal = controller.signal; + const cloneSignal = signalInterface.any([signal]); + assert_false(cloneSignal.aborted); + assert_true("reason" in cloneSignal, "cloneSignal has reason property"); + assert_equals(cloneSignal.reason, undefined, + "cloneSignal.reason is initially undefined"); + assert_not_equals(signal, cloneSignal, + `${desc} returns a new signal.`); + + let eventFired = false; + cloneSignal.onabort = t.step_func((e) => { + assert_equals(e.target, cloneSignal, + `The event target is the signal returned by ${desc}`); + eventFired = true; + }); + + controller.abort("reason string"); + assert_true(signal.aborted); + assert_true(cloneSignal.aborted); + assert_true(eventFired); + assert_equals(cloneSignal.reason, "reason string", + `${desc} propagates the abort reason`); + }, `${desc} follows a single signal ${suffix}`); + + test(t => { + for (let i = 0; i < 3; ++i) { + const controllers = []; + for (let j = 0; j < 3; ++j) { + controllers.push(new controllerInterface()); + } + const combinedSignal = signalInterface.any(controllers.map(c => c.signal)); + + let eventFired = false; + combinedSignal.onabort = t.step_func((e) => { + assert_equals(e.target, combinedSignal, + `The event target is the signal returned by ${desc}`); + eventFired = true; + }); + + controllers[i].abort(); + assert_true(eventFired); + assert_true(combinedSignal.aborted); + assert_true(combinedSignal.reason instanceof DOMException, + "signal.reason is a DOMException"); + assert_equals(combinedSignal.reason.name, "AbortError", + "signal.reason is a AbortError"); + } + }, `${desc} follows multiple signals ${suffix}`); + + test(t => { + const controllers = []; + for (let i = 0; i < 3; ++i) { + controllers.push(new controllerInterface()); + } + controllers[1].abort("reason 1"); + controllers[2].abort("reason 2"); + + const signal = signalInterface.any(controllers.map(c => c.signal)); + assert_true(signal.aborted); + assert_equals(signal.reason, "reason 1", + "The signal should be aborted with the first reason"); + }, `${desc} returns an aborted signal if passed an aborted signal ${suffix}`); + + test(t => { + const controller = new controllerInterface(); + const signal = signalInterface.any([controller.signal, controller.signal]); + assert_false(signal.aborted); + controller.abort("reason"); + assert_true(signal.aborted); + assert_equals(signal.reason, "reason"); + }, `${desc} can be passed the same signal more than once ${suffix}`); + + test(t => { + const controller1 = new controllerInterface(); + controller1.abort("reason 1"); + const controller2 = new controllerInterface(); + controller2.abort("reason 2"); + + const signal = signalInterface.any([controller1.signal, controller2.signal, controller1.signal]); + assert_true(signal.aborted); + assert_equals(signal.reason, "reason 1"); + }, `${desc} uses the first instance of a duplicate signal ${suffix}`); + + test(t => { + for (let i = 0; i < 3; ++i) { + const controllers = []; + for (let j = 0; j < 3; ++j) { + controllers.push(new controllerInterface()); + } + const combinedSignal1 = + signalInterface.any([controllers[0].signal, controllers[1].signal]); + const combinedSignal2 = + signalInterface.any([combinedSignal1, controllers[2].signal]); + + let eventFired = false; + combinedSignal2.onabort = t.step_func((e) => { + eventFired = true; + }); + + controllers[i].abort(); + assert_true(eventFired); + assert_true(combinedSignal2.aborted); + assert_true(combinedSignal2.reason instanceof DOMException, + "signal.reason is a DOMException"); + assert_equals(combinedSignal2.reason.name, "AbortError", + "signal.reason is a AbortError"); + } + }, `${desc} signals are composable ${suffix}`); + + async_test(t => { + const controller = new controllerInterface(); + const timeoutSignal = AbortSignal.timeout(5); + + const combinedSignal = signalInterface.any([controller.signal, timeoutSignal]); + + combinedSignal.onabort = t.step_func_done(() => { + assert_true(combinedSignal.aborted); + assert_true(combinedSignal.reason instanceof DOMException, + "combinedSignal.reason is a DOMException"); + assert_equals(combinedSignal.reason.name, "TimeoutError", + "combinedSignal.reason is a TimeoutError"); + }); + }, `${desc} works with signals returned by AbortSignal.timeout() ${suffix}`); + + test(t => { + const controller = new controllerInterface(); + let combined = signalInterface.any([controller.signal]); + combined = signalInterface.any([combined]); + combined = signalInterface.any([combined]); + combined = signalInterface.any([combined]); + + let eventFired = false; + combined.onabort = () => { + eventFired = true; + } + + assert_false(eventFired); + assert_false(combined.aborted); + + controller.abort("the reason"); + + assert_true(eventFired); + assert_true(combined.aborted); + assert_equals(combined.reason, "the reason"); + }, `${desc} works with intermediate signals ${suffix}`); + + test(t => { + const controller = new controllerInterface(); + const signals = []; + // The first event should be dispatched on the originating signal. + signals.push(controller.signal); + // All dependents are linked to `controller.signal` (never to another + // composite signal), so this is the order events should fire. + signals.push(signalInterface.any([controller.signal])); + signals.push(signalInterface.any([controller.signal])); + signals.push(signalInterface.any([signals[0]])); + signals.push(signalInterface.any([signals[1]])); + + let result = ""; + for (let i = 0; i < signals.length; i++) { + signals[i].addEventListener('abort', () => { + result += i; + }); + } + controller.abort(); + assert_equals(result, "01234"); + }, `Abort events for ${desc} signals fire in the right order ${suffix}`); +} diff --git a/test/fixtures/wpt/dom/events/Event-dispatch-click.html b/test/fixtures/wpt/dom/events/Event-dispatch-click.html index 010305775df7e9..ab4a24a5ad5096 100644 --- a/test/fixtures/wpt/dom/events/Event-dispatch-click.html +++ b/test/fixtures/wpt/dom/events/Event-dispatch-click.html @@ -87,6 +87,22 @@ child.dispatchEvent(new MouseEvent("click", {bubbles:true})) }, "pick the first with activation behavior ") +async_test(function(t) { + var input = document.createElement("input") + input.type = "radio" + dump.appendChild(input) + input.onclick = t.step_func(function() { + assert_false(input.checked, "input pre-click must not be triggered") + }) + var child = input.appendChild(document.createElement("input")) + child.type = "radio" + child.onclick = t.step_func(function() { + assert_true(child.checked, "child pre-click must be triggered") + }) + child.dispatchEvent(new MouseEvent("click", {bubbles:true})) + t.done() +}, "pick the first with activation behavior ") + async_test(function(t) { var input = document.createElement("input") input.type = "checkbox" @@ -173,6 +189,46 @@ t.done() }, "disabled checkbox still has activation behavior, part 2") +async_test(function(t) { + var state = "start" + + var form = document.createElement("form") + form.onsubmit = t.step_func(() => { + if(state == "start" || state == "radio") { + state = "failure" + } else if(state == "form") { + state = "done" + } + return false + }) + dump.appendChild(form) + var button = form.appendChild(document.createElement("button")) + button.type = "submit" + var radio = button.appendChild(document.createElement("input")) + radio.type = "radio" + radio.onclick = t.step_func(() => { + if(state == "start") { + assert_unreached() + } else if(state == "radio") { + assert_true(radio.checked) + } + }) + radio.disabled = true + radio.click() + assert_equals(state, "start") + + state = "radio" + radio.disabled = false + radio.click() + assert_equals(state, "radio") + + state = "form" + button.click() + assert_equals(state, "done") + + t.done() +}, "disabled radio still has activation behavior") + async_test(function(t) { var input = document.createElement("input") input.type = "checkbox" diff --git a/test/fixtures/wpt/dom/events/scrolling/scroll_support.js b/test/fixtures/wpt/dom/events/scrolling/scroll_support.js index 169393e4c3e419..74b531cd3dfedf 100644 --- a/test/fixtures/wpt/dom/events/scrolling/scroll_support.js +++ b/test/fixtures/wpt/dom/events/scrolling/scroll_support.js @@ -1,15 +1,62 @@ -async function waitForScrollendEvent(test, target, timeoutMs = 500) { +async function waitForEvent(eventName, test, target, timeoutMs = 500) { return new Promise((resolve, reject) => { const timeoutCallback = test.step_timeout(() => { - reject(`No Scrollend event received for target ${target}`); + reject(`No ${eventName} event received for target ${target}`); }, timeoutMs); - target.addEventListener('scrollend', (evt) => { + target.addEventListener(eventName, (evt) => { clearTimeout(timeoutCallback); resolve(evt); }, { once: true }); }); } +async function waitForScrollendEvent(test, target, timeoutMs = 500) { + return waitForEvent("scrollend", test, target, timeoutMs); +} + +async function waitForPointercancelEvent(test, target, timeoutMs = 500) { + return waitForEvent("pointercancel", test, target, timeoutMs); +} + +async function createScrollendPromiseForTarget(test, + target_div, + timeoutMs = 500) { + return waitForScrollendEvent(test, target_div, timeoutMs).then(evt => { + assert_false(evt.cancelable, 'Event is not cancelable'); + assert_false(evt.bubbles, 'Event targeting element does not bubble'); + }); +} + +function verifyNoScrollendOnDocument(test) { + const callback = + test.unreached_func("window got unexpected scrollend event."); + window.addEventListener('scrollend', callback); + test.add_cleanup(() => { + window.removeEventListener('scrollend', callback); + }); +} + +async function verifyScrollStopped(test, target_div) { + const unscaled_pause_time_in_ms = 100; + const x = target_div.scrollLeft; + const y = target_div.scrollTop; + return new Promise(resolve => { + test.step_timeout(() => { + assert_equals(x, target_div.scrollLeft); + assert_equals(y, target_div.scrollTop); + resolve(); + }, unscaled_pause_time_in_ms); + }); +} + +async function resetTargetScrollState(test, target_div) { + if (target_div.scrollTop != 0 || target_div.scrollLeft != 0) { + target_div.scrollTop = 0; + target_div.scrollLeft = 0; + return waitForScrollendEvent(test, target_div); + } +} + const MAX_FRAME = 700; const MAX_UNCHANGED_FRAMES = 20; @@ -45,6 +92,29 @@ function waitForCompositorCommit() { }); } +// Please don't remove this. This is necessary for chromium-based browsers. +// This shouldn't be necessary if the test harness deferred running the tests +// until after paint holding. This can be a no-op on user-agents that do not +// have a separate compositor thread. +async function waitForCompositorReady() { + const animation = + document.body.animate({ opacity: [ 1, 1 ] }, {duration: 1 }); + return animation.finished; +} + +function waitForNextFrame() { + const startTime = performance.now(); + return new Promise(resolve => { + window.requestAnimationFrame((frameTime) => { + if (frameTime < startTime) { + window.requestAnimationFrame(resolve); + } else { + resolve(); + } + }); + }); +} + // TODO(crbug.com/1400399): Deprecate as frame rates may vary greatly in // different test environments. function waitForAnimationEnd(getValue) { @@ -70,6 +140,10 @@ function waitForAnimationEnd(getValue) { } // Scrolls in target according to move_path with pauses in between +// The move_path should contains coordinates that are within target boundaries. +// Keep in mind that 0,0 is the center of the target element and is also +// the pointerDown position. +// pointerUp() is fired after sequence of moves. function touchScrollInTargetSequentiallyWithPause(target, move_path, pause_time_in_ms = 100) { const test_driver_actions = new test_driver.Actions() .addPointer("pointer1", "touch") @@ -88,7 +162,7 @@ function touchScrollInTargetSequentiallyWithPause(target, move_path, pause_time_ y += step_y; test_driver_actions.pointerMove(x, y, {origin: target}); } - test_driver_actions.pause(pause_time_in_ms); + test_driver_actions.pause(pause_time_in_ms); // To prevent inertial scroll } return test_driver_actions.pointerUp().send(); diff --git a/test/fixtures/wpt/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html b/test/fixtures/wpt/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html index 77bf029ced58c5..f6fe46fa803910 100644 --- a/test/fixtures/wpt/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html +++ b/test/fixtures/wpt/dom/events/scrolling/scrollend-event-fired-after-sequence-of-scrolls.tentative.html @@ -14,7 +14,7 @@ } #innerDiv { - width: 500px; + width: 4000px; height: 4000px; } @@ -28,36 +28,64 @@ diff --git a/test/fixtures/wpt/dom/events/scrolling/scrollend-event-fired-for-mandatory-snap-point-after-load.html b/test/fixtures/wpt/dom/events/scrolling/scrollend-event-fired-for-mandatory-snap-point-after-load.html new file mode 100644 index 00000000000000..f3791134204497 --- /dev/null +++ b/test/fixtures/wpt/dom/events/scrolling/scrollend-event-fired-for-mandatory-snap-point-after-load.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + scrollend + mandatory scroll snap test + + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/dom/events/scrolling/scrollend-event-for-user-scroll.html b/test/fixtures/wpt/dom/events/scrolling/scrollend-event-for-user-scroll.html index 5146c5f719a1e4..fae969faabc060 100644 --- a/test/fixtures/wpt/dom/events/scrolling/scrollend-event-for-user-scroll.html +++ b/test/fixtures/wpt/dom/events/scrolling/scrollend-event-for-user-scroll.html @@ -31,40 +31,6 @@ + + + + + diff --git a/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js index 660477dca4c621..00a86fa74b9a96 100644 --- a/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js +++ b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js @@ -9,6 +9,7 @@ * - `postTest()`: An optional, async function run after a test is done * - `structuredClone(obj, transferList)`: Required function that somehow * structurally clones an object. + * Must return a promise. * - `hasDocument`: When true, disables tests that require a document. True by default. */ @@ -30,11 +31,11 @@ function runStructuredCloneBatteryOfTests(runner) { } return new Promise(resolve => { - promise_test(async _ => { + promise_test(async t => { test = await test; await setupPromise; await runner.preTest(test); - await test.f(runner) + await test.f(runner, t) await runner.postTest(test); resolve(); }, test.description); diff --git a/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js index 744f1168196001..23cf4f651ac70a 100644 --- a/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js +++ b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js @@ -20,3 +20,150 @@ structuredCloneBatteryOfTests.push({ }); // TODO: ImageBitmap + +structuredCloneBatteryOfTests.push({ + description: 'A detached ArrayBuffer cannot be transferred', + async f(runner, t) { + const buffer = new ArrayBuffer(); + await runner.structuredClone(buffer, [buffer]); + await promise_rejects_dom( + t, + "DataCloneError", + runner.structuredClone(buffer, [buffer]) + ); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'A detached platform object cannot be transferred', + async f(runner, t) { + const {port1} = new MessageChannel(); + await runner.structuredClone(port1, [port1]); + await promise_rejects_dom( + t, + "DataCloneError", + runner.structuredClone(port1, [port1]) + ); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'Transferring a non-transferable platform object fails', + async f(runner, t) { + const blob = new Blob(); + await promise_rejects_dom( + t, + "DataCloneError", + runner.structuredClone(blob, [blob]) + ); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'An object whose interface is deleted from the global object must still be received', + async f(runner) { + const {port1} = new MessageChannel(); + const messagePortInterface = globalThis.MessagePort; + delete globalThis.MessagePort; + try { + const transfer = await runner.structuredClone(port1, [port1]); + assert_true(transfer instanceof messagePortInterface); + } finally { + globalThis.MessagePort = messagePortInterface; + } + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'A subclass instance will be received as its closest transferable superclass', + async f(runner) { + // MessagePort doesn't have a constructor, so we must use something else. + + // Make sure that ReadableStream is transferable before we test its subclasses. + try { + const stream = new ReadableStream(); + await runner.structuredClone(stream, [stream]); + } catch(err) { + if (err instanceof DOMException && err.code === DOMException.DATA_CLONE_ERR) { + throw new OptionalFeatureUnsupportedError("ReadableStream isn't transferable"); + } else { + throw err; + } + } + + class ReadableStreamSubclass extends ReadableStream {} + const original = new ReadableStreamSubclass(); + const transfer = await runner.structuredClone(original, [original]); + assert_equals(Object.getPrototypeOf(transfer), ReadableStream.prototype); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'Resizable ArrayBuffer is transferable', + async f(runner) { + const buffer = new ArrayBuffer(16, { maxByteLength: 1024 }); + const copy = await runner.structuredClone(buffer, [buffer]); + assert_equals(buffer.byteLength, 0); + assert_equals(copy.byteLength, 16); + assert_equals(copy.maxByteLength, 1024); + assert_true(copy.resizable); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'Length-tracking TypedArray is transferable', + async f(runner) { + const ab = new ArrayBuffer(16, { maxByteLength: 1024 }); + const ta = new Uint8Array(ab); + const copy = await runner.structuredClone(ta, [ab]); + assert_equals(ab.byteLength, 0); + assert_equals(copy.buffer.byteLength, 16); + assert_equals(copy.buffer.maxByteLength, 1024); + assert_true(copy.buffer.resizable); + copy.buffer.resize(32); + assert_equals(copy.byteLength, 32); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'Length-tracking DataView is transferable', + async f(runner) { + const ab = new ArrayBuffer(16, { maxByteLength: 1024 }); + const dv = new DataView(ab); + const copy = await runner.structuredClone(dv, [ab]); + assert_equals(ab.byteLength, 0); + assert_equals(copy.buffer.byteLength, 16); + assert_equals(copy.buffer.maxByteLength, 1024); + assert_true(copy.buffer.resizable); + copy.buffer.resize(32); + assert_equals(copy.byteLength, 32); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'Transferring OOB TypedArray throws', + async f(runner, t) { + const ab = new ArrayBuffer(16, { maxByteLength: 1024 }); + const ta = new Uint8Array(ab, 8); + ab.resize(0); + await promise_rejects_dom( + t, + "DataCloneError", + runner.structuredClone(ta, [ab]) + ); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'Transferring OOB DataView throws', + async f(runner, t) { + const ab = new ArrayBuffer(16, { maxByteLength: 1024 }); + const dv = new DataView(ab, 8); + ab.resize(0); + await promise_rejects_dom( + t, + "DataCloneError", + runner.structuredClone(dv, [ab]) + ); + } +}); diff --git a/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests.js b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests.js index 0c96ded2088357..923ac9dc164918 100644 --- a/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests.js +++ b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-battery-of-tests.js @@ -3,11 +3,6 @@ structuredCloneBatteryOfTests = []; function check(description, input, callback, requiresDocument = false) { - testObjMock = { - done() {}, - step_func(f) {return _ => f()}, - }; - structuredCloneBatteryOfTests.push({ description, async f(runner) { @@ -16,47 +11,41 @@ function check(description, input, callback, requiresDocument = false) { newInput = input(); } const copy = await runner.structuredClone(newInput); - await callback(copy, newInput, testObjMock); + await callback(copy, newInput); }, requiresDocument }); } -function compare_primitive(actual, input, test_obj) { +function compare_primitive(actual, input) { assert_equals(actual, input); - if (test_obj) - test_obj.done(); } -function compare_Array(callback, callback_is_async) { - return function(actual, input, test_obj) { +function compare_Array(callback) { + return async function(actual, input) { if (typeof actual === 'string') assert_unreached(actual); assert_true(actual instanceof Array, 'instanceof Array'); assert_not_equals(actual, input); assert_equals(actual.length, input.length, 'length'); - callback(actual, input); - if (test_obj && !callback_is_async) - test_obj.done(); + await callback(actual, input); } } -function compare_Object(callback, callback_is_async) { - return function(actual, input, test_obj) { +function compare_Object(callback) { + return async function(actual, input) { if (typeof actual === 'string') assert_unreached(actual); assert_true(actual instanceof Object, 'instanceof Object'); assert_false(actual instanceof Array, 'instanceof Array'); assert_not_equals(actual, input); - callback(actual, input); - if (test_obj && !callback_is_async) - test_obj.done(); + await callback(actual, input); } } -function enumerate_props(compare_func, test_obj) { - return function(actual, input) { +function enumerate_props(compare_func) { + return async function(actual, input) { for (const x in input) { - compare_func(actual[x], input[x], test_obj); + await compare_func(actual[x], input[x]); } }; } @@ -127,14 +116,12 @@ check('Object primitives', {'undefined':undefined, '9007199254740994':9007199254740994, '-9007199254740994':-9007199254740994}, compare_Object(enumerate_props(compare_primitive))); -function compare_Boolean(actual, input, test_obj) { +function compare_Boolean(actual, input) { if (typeof actual === 'string') assert_unreached(actual); assert_true(actual instanceof Boolean, 'instanceof Boolean'); assert_equals(String(actual), String(input), 'converted to primitive'); assert_not_equals(actual, input); - if (test_obj) - test_obj.done(); } check('Boolean true', new Boolean(true), compare_Boolean); check('Boolean false', new Boolean(false), compare_Boolean); @@ -143,14 +130,12 @@ check('Object Boolean objects', {'true':new Boolean(true), 'false':new Boolean(f function compare_obj(what) { const Type = self[what]; - return function(actual, input, test_obj) { + return function(actual, input) { if (typeof actual === 'string') assert_unreached(actual); assert_true(actual instanceof Type, 'instanceof '+what); assert_equals(Type(actual), Type(input), 'converted to primitive'); assert_not_equals(actual, input); - if (test_obj) - test_obj.done(); }; } check('String empty string', new String(''), compare_obj('String')); @@ -203,14 +188,12 @@ check('Object Number objects', {'0.2':new Number(0.2), '9007199254740994':new Number(9007199254740994), '-9007199254740994':new Number(-9007199254740994)}, compare_Object(enumerate_props(compare_obj('Number')))); -function compare_Date(actual, input, test_obj) { +function compare_Date(actual, input) { if (typeof actual === 'string') assert_unreached(actual); assert_true(actual instanceof Date, 'instanceof Date'); assert_equals(Number(actual), Number(input), 'converted to primitive'); assert_not_equals(actual, input); - if (test_obj) - test_obj.done(); } check('Date 0', new Date(0), compare_Date); check('Date -0', new Date(-0), compare_Date); @@ -227,7 +210,7 @@ check('Object Date objects', {'0':new Date(0), function compare_RegExp(expected_source) { // XXX ES6 spec doesn't define exact serialization for `source` (it allows several ways to escape) - return function(actual, input, test_obj) { + return function(actual, input) { if (typeof actual === 'string') assert_unreached(actual); assert_true(actual instanceof RegExp, 'instanceof RegExp'); @@ -239,8 +222,6 @@ function compare_RegExp(expected_source) { assert_equals(actual.unicode, input.unicode, 'unicode'); assert_equals(actual.lastIndex, 0, 'lastIndex'); assert_not_equals(actual, input); - if (test_obj) - test_obj.done(); } } function func_RegExp_flags_lastIndex() { @@ -273,7 +254,28 @@ check('Object RegExp object, RegExp empty', {'x':new RegExp('')}, compare_Object check('Object RegExp object, RegExp slash', {'x':new RegExp('/')}, compare_Object(enumerate_props(compare_RegExp('\\/')))); check('Object RegExp object, RegExp new line', {'x':new RegExp('\n')}, compare_Object(enumerate_props(compare_RegExp('\\n')))); -async function compare_Blob(actual, input, test_obj, expect_File) { +function compare_Error(actual, input) { + assert_true(actual instanceof Error, "Checking instanceof"); + assert_equals(actual.constructor, input.constructor, "Checking constructor"); + assert_equals(actual.name, input.name, "Checking name"); + assert_equals(actual.hasOwnProperty("message"), input.hasOwnProperty("message"), "Checking message existence"); + assert_equals(actual.message, input.message, "Checking message"); + assert_equals(actual.foo, undefined, "Checking for absence of custom property"); +} + +check('Empty Error object', new Error, compare_Error); + +const errorConstructors = [Error, EvalError, RangeError, ReferenceError, + SyntaxError, TypeError, URIError]; +for (const constructor of errorConstructors) { + check(`${constructor.name} object`, () => { + let error = new constructor("Error message here"); + error.foo = "testing"; + return error; + }, compare_Error); +} + +async function compare_Blob(actual, input, expect_File) { if (typeof actual === 'string') assert_unreached(actual); assert_true(actual instanceof Blob, 'instanceof Blob'); @@ -333,33 +335,33 @@ function func_Blob_NUL() { } check('Blob NUL', func_Blob_NUL, compare_Blob); -check('Array Blob object, Blob basic', [func_Blob_basic()], compare_Array(enumerate_props(compare_Blob), true)); -check('Array Blob object, Blob unpaired high surrogate (invalid utf-8)', [func_Blob_bytes([0xD800])()], compare_Array(enumerate_props(compare_Blob), true)); -check('Array Blob object, Blob unpaired low surrogate (invalid utf-8)', [func_Blob_bytes([0xDC00])()], compare_Array(enumerate_props(compare_Blob), true)); -check('Array Blob object, Blob paired surrogates (invalid utf-8)', [func_Blob_bytes([0xD800, 0xDC00])()], compare_Array(enumerate_props(compare_Blob), true)); -check('Array Blob object, Blob empty', [func_Blob_empty()], compare_Array(enumerate_props(compare_Blob), true)); -check('Array Blob object, Blob NUL', [func_Blob_NUL()], compare_Array(enumerate_props(compare_Blob), true)); -check('Array Blob object, two Blobs', [func_Blob_basic(), func_Blob_empty()], compare_Array(enumerate_props(compare_Blob), true)); - -check('Object Blob object, Blob basic', {'x':func_Blob_basic()}, compare_Object(enumerate_props(compare_Blob), true)); -check('Object Blob object, Blob unpaired high surrogate (invalid utf-8)', {'x':func_Blob_bytes([0xD800])()}, compare_Object(enumerate_props(compare_Blob), true)); -check('Object Blob object, Blob unpaired low surrogate (invalid utf-8)', {'x':func_Blob_bytes([0xDC00])()}, compare_Object(enumerate_props(compare_Blob), true)); -check('Object Blob object, Blob paired surrogates (invalid utf-8)', {'x':func_Blob_bytes([0xD800, 0xDC00])() }, compare_Object(enumerate_props(compare_Blob), true)); -check('Object Blob object, Blob empty', {'x':func_Blob_empty()}, compare_Object(enumerate_props(compare_Blob), true)); -check('Object Blob object, Blob NUL', {'x':func_Blob_NUL()}, compare_Object(enumerate_props(compare_Blob), true)); - -function compare_File(actual, input, test_obj) { +check('Array Blob object, Blob basic', [func_Blob_basic()], compare_Array(enumerate_props(compare_Blob))); +check('Array Blob object, Blob unpaired high surrogate (invalid utf-8)', [func_Blob_bytes([0xD800])()], compare_Array(enumerate_props(compare_Blob))); +check('Array Blob object, Blob unpaired low surrogate (invalid utf-8)', [func_Blob_bytes([0xDC00])()], compare_Array(enumerate_props(compare_Blob))); +check('Array Blob object, Blob paired surrogates (invalid utf-8)', [func_Blob_bytes([0xD800, 0xDC00])()], compare_Array(enumerate_props(compare_Blob))); +check('Array Blob object, Blob empty', [func_Blob_empty()], compare_Array(enumerate_props(compare_Blob))); +check('Array Blob object, Blob NUL', [func_Blob_NUL()], compare_Array(enumerate_props(compare_Blob))); +check('Array Blob object, two Blobs', [func_Blob_basic(), func_Blob_empty()], compare_Array(enumerate_props(compare_Blob))); + +check('Object Blob object, Blob basic', {'x':func_Blob_basic()}, compare_Object(enumerate_props(compare_Blob))); +check('Object Blob object, Blob unpaired high surrogate (invalid utf-8)', {'x':func_Blob_bytes([0xD800])()}, compare_Object(enumerate_props(compare_Blob))); +check('Object Blob object, Blob unpaired low surrogate (invalid utf-8)', {'x':func_Blob_bytes([0xDC00])()}, compare_Object(enumerate_props(compare_Blob))); +check('Object Blob object, Blob paired surrogates (invalid utf-8)', {'x':func_Blob_bytes([0xD800, 0xDC00])() }, compare_Object(enumerate_props(compare_Blob))); +check('Object Blob object, Blob empty', {'x':func_Blob_empty()}, compare_Object(enumerate_props(compare_Blob))); +check('Object Blob object, Blob NUL', {'x':func_Blob_NUL()}, compare_Object(enumerate_props(compare_Blob))); + +async function compare_File(actual, input) { assert_true(actual instanceof File, 'instanceof File'); assert_equals(actual.name, input.name, 'name'); assert_equals(actual.lastModified, input.lastModified, 'lastModified'); - compare_Blob(actual, input, test_obj, true); + await compare_Blob(actual, input, true); } function func_File_basic() { return new File(['foo'], 'bar', {type:'text/x-bar', lastModified:42}); } check('File basic', func_File_basic, compare_File); -function compare_FileList(actual, input, test_obj) { +function compare_FileList(actual, input) { if (typeof actual === 'string') assert_unreached(actual); assert_true(actual instanceof FileList, 'instanceof FileList'); @@ -367,8 +369,6 @@ function compare_FileList(actual, input, test_obj) { assert_not_equals(actual, input); // XXX when there's a way to populate or construct a FileList, // check the items in the FileList - if (test_obj) - test_obj.done(); } function func_FileList_empty() { const input = document.createElement('input'); @@ -379,30 +379,36 @@ check('FileList empty', func_FileList_empty, compare_FileList, true); check('Array FileList object, FileList empty', () => ([func_FileList_empty()]), compare_Array(enumerate_props(compare_FileList)), true); check('Object FileList object, FileList empty', () => ({'x':func_FileList_empty()}), compare_Object(enumerate_props(compare_FileList)), true); +function compare_ArrayBuffer(actual, input) { + assert_true(actual instanceof ArrayBuffer, 'instanceof ArrayBuffer'); + assert_equals(actual.byteLength, input.byteLength, 'byteLength'); + assert_equals(actual.maxByteLength, input.maxByteLength, 'maxByteLength'); + assert_equals(actual.resizable, input.resizable, 'resizable'); + assert_equals(actual.growable, input.growable, 'growable'); +} + function compare_ArrayBufferView(view) { const Type = self[view]; - return function(actual, input, test_obj) { + return function(actual, input) { if (typeof actual === 'string') assert_unreached(actual); assert_true(actual instanceof Type, 'instanceof '+view); assert_equals(actual.length, input.length, 'length'); + assert_equals(actual.byteLength, input.byteLength, 'byteLength'); + assert_equals(actual.byteOffset, input.byteOffset, 'byteOffset'); assert_not_equals(actual.buffer, input.buffer, 'buffer'); for (let i = 0; i < actual.length; ++i) { assert_equals(actual[i], input[i], 'actual['+i+']'); } - if (test_obj) - test_obj.done(); }; } -function compare_ImageData(actual, input, test_obj) { +function compare_ImageData(actual, input) { if (typeof actual === 'string') assert_unreached(actual); assert_equals(actual.width, input.width, 'width'); assert_equals(actual.height, input.height, 'height'); assert_not_equals(actual.data, input.data, 'data'); compare_ArrayBufferView('Uint8ClampedArray')(actual.data, input.data, null); - if (test_obj) - test_obj.done(); } function func_ImageData_1x1_transparent_black() { const canvas = document.createElement('canvas'); @@ -506,6 +512,23 @@ check('Object with non-configurable property', function() { return rv; }, compare_Object(check_configurable_property('foo'))); +structuredCloneBatteryOfTests.push({ + description: 'Object with a getter that throws', + async f(runner, t) { + const exception = new Error(); + const testObject = { + get testProperty() { + throw exception; + } + }; + await promise_rejects_exactly( + t, + exception, + runner.structuredClone(testObject) + ); + } +}); + /* The tests below are inspired by @zcorpanโ€™s work but got some more substantial changed due to their previous async setup */ @@ -616,3 +639,115 @@ check('ObjectPrototype must lose its exotic-ness when cloned', assert_equals(Object.getPrototypeOf(copy), newProto); } ); + +structuredCloneBatteryOfTests.push({ + description: 'Serializing a non-serializable platform object fails', + async f(runner, t) { + const request = new Response(); + await promise_rejects_dom( + t, + "DataCloneError", + runner.structuredClone(request) + ); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'An object whose interface is deleted from the global must still deserialize', + async f(runner) { + const blob = new Blob(); + const blobInterface = globalThis.Blob; + delete globalThis.Blob; + try { + const copy = await runner.structuredClone(blob); + assert_true(copy instanceof blobInterface); + } finally { + globalThis.Blob = blobInterface; + } + } +}); + +check( + 'A subclass instance will deserialize as its closest serializable superclass', + () => { + class FileSubclass extends File {} + return new FileSubclass([], ""); + }, + (copy) => { + assert_equals(Object.getPrototypeOf(copy), File.prototype); + } +); + +check( + 'Resizable ArrayBuffer', + () => { + const ab = new ArrayBuffer(16, { maxByteLength: 1024 }); + assert_true(ab.resizable); + return ab; + }, + compare_ArrayBuffer); + +structuredCloneBatteryOfTests.push({ + description: 'Growable SharedArrayBuffer', + async f(runner) { + const sab = createBuffer('SharedArrayBuffer', 16, { maxByteLength: 1024 }); + assert_true(sab.growable); + try { + const copy = await runner.structuredClone(sab); + compare_ArrayBuffer(sab, copy); + } catch (e) { + // If we're cross-origin isolated, cloning SABs should not fail. + if (e instanceof DOMException && e.code === DOMException.DATA_CLONE_ERR) { + assert_false(self.crossOriginIsolated); + } else { + throw e; + } + } + } +}); + +check( + 'Length-tracking TypedArray', + () => { + const ab = new ArrayBuffer(16, { maxByteLength: 1024 }); + assert_true(ab.resizable); + return new Uint8Array(ab); + }, + compare_ArrayBufferView('Uint8Array')); + +check( + 'Length-tracking DataView', + () => { + const ab = new ArrayBuffer(16, { maxByteLength: 1024 }); + assert_true(ab.resizable); + return new DataView(ab); + }, + compare_ArrayBufferView('DataView')); + +structuredCloneBatteryOfTests.push({ + description: 'Serializing OOB TypedArray throws', + async f(runner, t) { + const ab = new ArrayBuffer(16, { maxByteLength: 1024 }); + const ta = new Uint8Array(ab, 8); + ab.resize(0); + await promise_rejects_dom( + t, + "DataCloneError", + runner.structuredClone(ta) + ); + } +}); + +structuredCloneBatteryOfTests.push({ + description: 'Serializing OOB DataView throws', + async f(runner, t) { + const ab = new ArrayBuffer(16, { maxByteLength: 1024 }); + const dv = new DataView(ab, 8); + ab.resize(0); + await promise_rejects_dom( + t, + "DataCloneError", + runner.structuredClone(dv) + ); + } +}); diff --git a/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-cross-realm-method.html b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-cross-realm-method.html new file mode 100644 index 00000000000000..d80010e0df78ad --- /dev/null +++ b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone-cross-realm-method.html @@ -0,0 +1,20 @@ + +self.structuredClone() uses this's relevant Realm for deserialization + + + + + + diff --git a/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone.any.js b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone.any.js index 34f96f33fdf881..1358a71fc03d4e 100644 --- a/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone.any.js +++ b/test/fixtures/wpt/html/webappapis/structured-clone/structured-clone.any.js @@ -1,9 +1,14 @@ // META: title=structuredClone() tests +// META: script=/common/sab.js // META: script=/html/webappapis/structured-clone/structured-clone-battery-of-tests.js // META: script=/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js // META: script=/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js runStructuredCloneBatteryOfTests({ - structuredClone: (obj, transfer) => self.structuredClone(obj, { transfer }), + structuredClone: (obj, transfer) => { + return new Promise(resolve => { + resolve(self.structuredClone(obj, { transfer })); + }); + }, hasDocument: typeof document !== "undefined", }); diff --git a/test/fixtures/wpt/html/webappapis/timers/clearinterval-from-callback.any.js b/test/fixtures/wpt/html/webappapis/timers/clearinterval-from-callback.any.js new file mode 100644 index 00000000000000..bf4eb7cf5ac897 --- /dev/null +++ b/test/fixtures/wpt/html/webappapis/timers/clearinterval-from-callback.any.js @@ -0,0 +1,19 @@ +async_test((t) => { + let wasPreviouslyCalled = false; + + const handle = setInterval( + t.step_func(() => { + if (!wasPreviouslyCalled) { + wasPreviouslyCalled = true; + + clearInterval(handle); + + // Make the test succeed after the callback would've run next. + setInterval(t.step_func_done(), 750); + } else { + assert_unreached(); + } + }), + 500 + ); +}, "Clearing an interval from the callback should still clear it."); diff --git a/test/fixtures/wpt/html/webappapis/timers/evil-spec-example.any.js b/test/fixtures/wpt/html/webappapis/timers/evil-spec-example.any.js new file mode 100644 index 00000000000000..17215e218a9e26 --- /dev/null +++ b/test/fixtures/wpt/html/webappapis/timers/evil-spec-example.any.js @@ -0,0 +1,12 @@ +var t = async_test("Interaction of setTimeout and WebIDL") +function finishTest() { + assert_equals(log, "ONE TWO ") + t.done() +} +var log = ''; +function logger(s) { log += s + ' '; } + +setTimeout({ toString: function () { + setTimeout("logger('ONE')", 100); + return "logger('TWO'); t.step(finishTest)"; +} }, 100); diff --git a/test/fixtures/wpt/html/webappapis/timers/setinterval-cross-realm-callback-report-exception.html b/test/fixtures/wpt/html/webappapis/timers/setinterval-cross-realm-callback-report-exception.html new file mode 100644 index 00000000000000..4a780fc93291b1 --- /dev/null +++ b/test/fixtures/wpt/html/webappapis/timers/setinterval-cross-realm-callback-report-exception.html @@ -0,0 +1,32 @@ + + +window.setInterval() reports the exception from its callback in the callback's global object + + + + + + diff --git a/test/fixtures/wpt/html/webappapis/timers/settimeout-cross-realm-callback-report-exception.html b/test/fixtures/wpt/html/webappapis/timers/settimeout-cross-realm-callback-report-exception.html new file mode 100644 index 00000000000000..b4860151a6c39f --- /dev/null +++ b/test/fixtures/wpt/html/webappapis/timers/settimeout-cross-realm-callback-report-exception.html @@ -0,0 +1,28 @@ + + +window.setTimeout() reports the exception from its callback in the callback's global object + + + + + + diff --git a/test/fixtures/wpt/interfaces/hr-time.idl b/test/fixtures/wpt/interfaces/hr-time.idl index 13aa109b6c764c..835ee8a65c84c0 100644 --- a/test/fixtures/wpt/interfaces/hr-time.idl +++ b/test/fixtures/wpt/interfaces/hr-time.idl @@ -7,7 +7,7 @@ typedef double DOMHighResTimeStamp; typedef unsigned long long EpochTimeStamp; -[Exposed=*] +[Exposed=(Window,Worker)] interface Performance : EventTarget { DOMHighResTimeStamp now(); readonly attribute DOMHighResTimeStamp timeOrigin; diff --git a/test/fixtures/wpt/interfaces/html.idl b/test/fixtures/wpt/interfaces/html.idl index d9068b4ad1e550..e4752f079ceb2e 100644 --- a/test/fixtures/wpt/interfaces/html.idl +++ b/test/fixtures/wpt/interfaces/html.idl @@ -98,7 +98,6 @@ partial interface Document { // also has obsolete members }; Document includes GlobalEventHandlers; -Document includes DocumentAndElementEventHandlers; partial interface mixin DocumentOrShadowRoot { readonly attribute Element? activeElement; @@ -131,7 +130,6 @@ interface HTMLElement : Element { }; HTMLElement includes GlobalEventHandlers; -HTMLElement includes DocumentAndElementEventHandlers; HTMLElement includes ElementContentEditable; HTMLElement includes HTMLOrSVGElement; @@ -886,7 +884,7 @@ interface HTMLInputElement : HTMLElement { [CEReactions] attribute DOMString formTarget; [CEReactions] attribute unsigned long height; attribute boolean indeterminate; - readonly attribute HTMLElement? list; + readonly attribute HTMLDataListElement? list; [CEReactions] attribute DOMString max; [CEReactions] attribute long maxLength; [CEReactions] attribute DOMString min; @@ -1364,14 +1362,7 @@ interface mixin CanvasShadowStyles { interface mixin CanvasFilters { // filters - attribute (DOMString or CanvasFilter) filter; // (default "none") -}; - -typedef record CanvasFilterInput; - -[Exposed=(Window,Worker,PaintWorklet)] -interface CanvasFilter { - constructor(optional (CanvasFilterInput or sequence) filters); + attribute DOMString filter; // (default "none") }; interface mixin CanvasRect { @@ -1641,8 +1632,19 @@ dictionary ValidityStateFlags { boolean customError = false; }; +[Exposed=Window] +interface UserActivation { + readonly attribute boolean hasBeenActive; + readonly attribute boolean isActive; +}; + +partial interface Navigator { + [SameObject] readonly attribute UserActivation userActivation; +}; + dictionary FocusOptions { boolean preventScroll = false; + boolean focusVisible; }; interface mixin ElementContentEditable { @@ -1769,20 +1771,6 @@ interface BarProp { readonly attribute boolean visible; }; -enum ScrollRestoration { "auto", "manual" }; - -[Exposed=Window] -interface History { - readonly attribute unsigned long length; - attribute ScrollRestoration scrollRestoration; - readonly attribute any state; - undefined go(optional long delta = 0); - undefined back(); - undefined forward(); - undefined pushState(any data, DOMString unused, optional USVString? url = null); - undefined replaceState(any data, DOMString unused, optional USVString? url = null); -}; - [Exposed=Window] interface Location { // but see also additional creation steps and overridden internal methods [LegacyUnforgeable] stringifier attribute USVString href; @@ -1802,6 +1790,20 @@ interface Location { // but see also additional creation steps and overridden in [LegacyUnforgeable, SameObject] readonly attribute DOMStringList ancestorOrigins; }; +enum ScrollRestoration { "auto", "manual" }; + +[Exposed=Window] +interface History { + readonly attribute unsigned long length; + attribute ScrollRestoration scrollRestoration; + readonly attribute any state; + undefined go(optional long delta = 0); + undefined back(); + undefined forward(); + undefined pushState(any data, DOMString unused, optional USVString? url = null); + undefined replaceState(any data, DOMString unused, optional USVString? url = null); +}; + [Exposed=Window] interface PopStateEvent : Event { constructor(DOMString type, optional PopStateEventInit eventInitDict = {}); @@ -1901,7 +1903,9 @@ interface mixin GlobalEventHandlers { attribute EventHandler oncontextlost; attribute EventHandler oncontextmenu; attribute EventHandler oncontextrestored; + attribute EventHandler oncopy; attribute EventHandler oncuechange; + attribute EventHandler oncut; attribute EventHandler ondblclick; attribute EventHandler ondrag; attribute EventHandler ondragend; @@ -1932,6 +1936,7 @@ interface mixin GlobalEventHandlers { attribute EventHandler onmouseout; attribute EventHandler onmouseover; attribute EventHandler onmouseup; + attribute EventHandler onpaste; attribute EventHandler onpause; attribute EventHandler onplay; attribute EventHandler onplaying; @@ -1940,6 +1945,7 @@ interface mixin GlobalEventHandlers { attribute EventHandler onreset; attribute EventHandler onresize; attribute EventHandler onscroll; + attribute EventHandler onscrollend; attribute EventHandler onsecuritypolicyviolation; attribute EventHandler onseeked; attribute EventHandler onseeking; @@ -1978,12 +1984,6 @@ interface mixin WindowEventHandlers { attribute EventHandler onunload; }; -interface mixin DocumentAndElementEventHandlers { - attribute EventHandler oncopy; - attribute EventHandler oncut; - attribute EventHandler onpaste; -}; - typedef (DOMString or Function) TimerHandler; interface mixin WindowOrWorkerGlobalScope { @@ -2132,13 +2132,13 @@ typedef (CanvasImageSource or Blob or ImageData) ImageBitmapSource; -enum ImageOrientation { "none", "flipY" }; +enum ImageOrientation { "from-image", "flipY" }; enum PremultiplyAlpha { "none", "premultiply", "default" }; enum ColorSpaceConversion { "none", "default" }; enum ResizeQuality { "pixelated", "low", "medium", "high" }; dictionary ImageBitmapOptions { - ImageOrientation imageOrientation = "none"; + ImageOrientation imageOrientation = "from-image"; PremultiplyAlpha premultiplyAlpha = "default"; ColorSpaceConversion colorSpaceConversion = "default"; [EnforceRange] unsigned long resizeWidth; diff --git a/test/fixtures/wpt/interfaces/performance-timeline.idl b/test/fixtures/wpt/interfaces/performance-timeline.idl index 9f6cc5e2e902dc..cdd8fafd8c64e6 100644 --- a/test/fixtures/wpt/interfaces/performance-timeline.idl +++ b/test/fixtures/wpt/interfaces/performance-timeline.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: Performance Timeline Level 2 (https://w3c.github.io/performance-timeline/) +// Source: Performance Timeline (https://w3c.github.io/performance-timeline/) partial interface Performance { PerformanceEntryList getEntries (); @@ -10,7 +10,7 @@ partial interface Performance { }; typedef sequence PerformanceEntryList; -[Exposed=*] +[Exposed=(Window,Worker)] interface PerformanceEntry { readonly attribute DOMString name; readonly attribute DOMString entryType; @@ -22,7 +22,7 @@ interface PerformanceEntry { callback PerformanceObserverCallback = undefined (PerformanceObserverEntryList entries, PerformanceObserver observer, optional PerformanceObserverCallbackOptions options = {}); -[Exposed=*] +[Exposed=(Window,Worker)] interface PerformanceObserver { constructor(PerformanceObserverCallback callback); undefined observe (optional PerformanceObserverInit options = {}); @@ -41,7 +41,7 @@ dictionary PerformanceObserverInit { boolean buffered; }; -[Exposed=*] +[Exposed=(Window,Worker)] interface PerformanceObserverEntryList { PerformanceEntryList getEntries(); PerformanceEntryList getEntriesByType (DOMString type); diff --git a/test/fixtures/wpt/interfaces/resource-timing.idl b/test/fixtures/wpt/interfaces/resource-timing.idl index 235963b804bf9a..242df0bd804af4 100644 --- a/test/fixtures/wpt/interfaces/resource-timing.idl +++ b/test/fixtures/wpt/interfaces/resource-timing.idl @@ -1,7 +1,7 @@ // GENERATED CONTENT - DO NOT EDIT // Content was automatically extracted by Reffy into webref // (https://github.com/w3c/webref) -// Source: Resource Timing Level 2 (https://w3c.github.io/resource-timing/) +// Source: Resource Timing (https://w3c.github.io/resource-timing/) [Exposed=(Window,Worker)] interface PerformanceResourceTiming : PerformanceEntry { @@ -22,9 +22,16 @@ interface PerformanceResourceTiming : PerformanceEntry { readonly attribute unsigned long long transferSize; readonly attribute unsigned long long encodedBodySize; readonly attribute unsigned long long decodedBodySize; + readonly attribute unsigned short responseStatus; + readonly attribute RenderBlockingStatusType renderBlockingStatus; [Default] object toJSON(); }; +enum RenderBlockingStatusType { + "blocking", + "non-blocking" +}; + partial interface Performance { undefined clearResourceTimings (); undefined setResourceTimingBufferSize (unsigned long maxSize); diff --git a/test/fixtures/wpt/interfaces/user-timing.idl b/test/fixtures/wpt/interfaces/user-timing.idl index a0b8f94710ec33..28ee8aac2b19a6 100644 --- a/test/fixtures/wpt/interfaces/user-timing.idl +++ b/test/fixtures/wpt/interfaces/user-timing.idl @@ -22,13 +22,13 @@ partial interface Performance { undefined clearMeasures(optional DOMString measureName); }; -[Exposed=*] +[Exposed=(Window,Worker)] interface PerformanceMark : PerformanceEntry { constructor(DOMString markName, optional PerformanceMarkOptions markOptions = {}); readonly attribute any detail; }; -[Exposed=*] +[Exposed=(Window,Worker)] interface PerformanceMeasure : PerformanceEntry { readonly attribute any detail; }; diff --git a/test/fixtures/wpt/interfaces/webidl.idl b/test/fixtures/wpt/interfaces/webidl.idl index 43748c5ac4c889..9993673361aad6 100644 --- a/test/fixtures/wpt/interfaces/webidl.idl +++ b/test/fixtures/wpt/interfaces/webidl.idl @@ -9,7 +9,7 @@ typedef (Int8Array or Int16Array or Int32Array or Float32Array or Float64Array or DataView) ArrayBufferView; typedef (ArrayBufferView or ArrayBuffer) BufferSource; -[Exposed=(Window,Worker), +[Exposed=*, Serializable] interface DOMException { // but see below note about ECMAScript binding constructor(optional DOMString message = "", optional DOMString name = "Error"); diff --git a/test/fixtures/wpt/performance-timeline/back-forward-cache-restoration.tentative.html b/test/fixtures/wpt/performance-timeline/back-forward-cache-restoration.tentative.html new file mode 100644 index 00000000000000..733642fd03225a --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/back-forward-cache-restoration.tentative.html @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/droppedentriescount.any.js b/test/fixtures/wpt/performance-timeline/droppedentriescount.any.js new file mode 100644 index 00000000000000..4de816bdc42bd8 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/droppedentriescount.any.js @@ -0,0 +1,81 @@ +promise_test(t => { + // This setup is required for later tests as well. + // Await for a dropped entry. + return new Promise(res => { + // Set a buffer size of 0 so that new resource entries count as dropped. + performance.setResourceTimingBufferSize(0); + // Use an observer to make sure the promise is resolved only when the + // new entry has been created. + new PerformanceObserver(res).observe({type: 'resource'}); + fetch('resources/square.png?id=1'); + }).then(() => { + return new Promise(resolve => { + new PerformanceObserver(t.step_func((entries, obs, options) => { + assert_equals(options['droppedEntriesCount'], 0); + resolve(); + })).observe({type: 'mark'}); + performance.mark('test'); + })}); +}, 'Dropped entries count is 0 when there are no dropped entries of relevant type.'); + +promise_test(async t => { + return new Promise(resolve => { + new PerformanceObserver(t.step_func((entries, obs, options) => { + assert_equals(options['droppedEntriesCount'], 1); + resolve(); + })).observe({entryTypes: ['mark', 'resource']}); + performance.mark('meow'); + }); +}, 'Dropped entries correctly counted with multiple types.'); + +promise_test(t => { + return new Promise(resolve => { + new PerformanceObserver(t.step_func((entries, obs, options) => { + assert_equals(options['droppedEntriesCount'], 1, + 'There should have been some dropped resource timing entries at this point'); + resolve(); + })).observe({type: 'resource', buffered: true}); + }); +}, 'Dropped entries counted even if observer was not registered at the time.'); + +promise_test(t => { + return new Promise(resolve => { + let callback_ran = false; + new PerformanceObserver(t.step_func((entries, obs, options) => { + if (!callback_ran) { + assert_equals(options['droppedEntriesCount'], 2, + 'There should be two dropped entries right now.'); + fetch('resources/square.png?id=3'); + callback_ran = true; + } else { + assert_equals(options['droppedEntriesCount'], undefined, + 'droppedEntriesCount should be unset after the first callback!'); + resolve(); + } + })).observe({type: 'resource'}); + fetch('resources/square.png?id=2'); + }); +}, 'Dropped entries only surfaced on the first callback.'); + + +promise_test(t => { + return new Promise(resolve => { + let callback_ran = false; + let droppedEntriesCount = -1; + new PerformanceObserver(t.step_func((entries, obs, options) => { + if (!callback_ran) { + assert_greater_than(options['droppedEntriesCount'], 0, + 'There should be several dropped entries right now.'); + droppedEntriesCount = options['droppedEntriesCount']; + callback_ran = true; + obs.observe({type: 'mark'}); + performance.mark('woof'); + } else { + assert_equals(options['droppedEntriesCount'], droppedEntriesCount, + 'There should be droppedEntriesCount due to the new observe().'); + resolve(); + } + })).observe({type: 'resource'}); + fetch('resources/square.png?id=4'); + }); +}, 'Dropped entries surfaced after an observe() call!'); diff --git a/test/fixtures/wpt/performance-timeline/idlharness-shadowrealm.window.js b/test/fixtures/wpt/performance-timeline/idlharness-shadowrealm.window.js new file mode 100644 index 00000000000000..6caaa3306132bd --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/idlharness-shadowrealm.window.js @@ -0,0 +1,2 @@ +// META: script=/resources/idlharness-shadowrealm.js +idl_test_shadowrealm(["performance-timeline"], ["hr-time", "dom"]); diff --git a/test/fixtures/wpt/performance-timeline/navigation-id-detached-frame.tentative.html b/test/fixtures/wpt/performance-timeline/navigation-id-detached-frame.tentative.html new file mode 100644 index 00000000000000..add11255af45c0 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/navigation-id-detached-frame.tentative.html @@ -0,0 +1,30 @@ + + + + + + The navigation_id Detached iframe Parent Page. + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/navigation-id-element-timing.tentative.html b/test/fixtures/wpt/performance-timeline/navigation-id-element-timing.tentative.html new file mode 100644 index 00000000000000..bc52f208b07264 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/navigation-id-element-timing.tentative.html @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/navigation-id-initial-load.tentative.html b/test/fixtures/wpt/performance-timeline/navigation-id-initial-load.tentative.html new file mode 100644 index 00000000000000..3228e12778b5d5 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/navigation-id-initial-load.tentative.html @@ -0,0 +1,43 @@ + + + + + + + +

This text is to trigger a LCP entry emission.

+ + diff --git a/test/fixtures/wpt/performance-timeline/navigation-id-long-task-task-attribution.tentative.html b/test/fixtures/wpt/performance-timeline/navigation-id-long-task-task-attribution.tentative.html new file mode 100644 index 00000000000000..662e17508b26dc --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/navigation-id-long-task-task-attribution.tentative.html @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/navigation-id-mark-measure.tentative.html b/test/fixtures/wpt/performance-timeline/navigation-id-mark-measure.tentative.html new file mode 100644 index 00000000000000..42795f94a98cd0 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/navigation-id-mark-measure.tentative.html @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/navigation-id-reset.tentative.html b/test/fixtures/wpt/performance-timeline/navigation-id-reset.tentative.html new file mode 100644 index 00000000000000..7386331d26e918 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/navigation-id-reset.tentative.html @@ -0,0 +1,49 @@ + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/navigation-id-resource-timing.tentative.html b/test/fixtures/wpt/performance-timeline/navigation-id-resource-timing.tentative.html new file mode 100644 index 00000000000000..1ec906ebbbba4a --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/navigation-id-resource-timing.tentative.html @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/navigation-id.helper.js b/test/fixtures/wpt/performance-timeline/navigation-id.helper.js new file mode 100644 index 00000000000000..53099cadb254df --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/navigation-id.helper.js @@ -0,0 +1,130 @@ +// The test functions called in the navigation-counter test. They rely on +// artifacts defined in +// '/html/browsers/browsing-the-web/back-forward-cache/resources/helper.sub.js' +// which should be included before this file to use these functions. + +// This function is to obtain navigation ids of all performance entries to +// verify. +let testInitial = () => { + return window.performance.getEntries().map(e => e.navigationId); +} + +let testMarkMeasure = (expectedNavigationId, markName, MeasureName) => { + const markName1 = 'test-mark'; + const markName2 = 'test-mark' + expectedNavigationId; + const measureName = 'test-measure' + expectedNavigationId; + + window.performance.mark(markName1); + window.performance.mark(markName2); + window.performance.measure(measureName, markName1, markName2); + return window.performance.getEntriesByName(markName2).concat( + window.performance.getEntriesByName(measureName)).map(e => e.navigationId); +} + +let testResourceTiming = async (expectedNavigationId) => { + let navigationId = -1; + + let p = new Promise(resolve => { + new PerformanceObserver((list) => { + const entry = list.getEntries().find(e => e.name.includes('json_resource') && e.navigationId == expectedNavigationId); + if (entry) { + navigationId = entry.navigationId; + resolve(); + } + }).observe({ type: 'resource' }); + }); + + const resp = await fetch('/performance-timeline/resources/json_resource.json'); + await p; + return [navigationId]; +} + +let testElementTiming = async (expectedNavigationId) => { + let navigationId = -1; + let p = new Promise(resolve => { + new PerformanceObserver((list) => { + const entry = list.getEntries().find(e => e.entryType === 'element' && e.identifier === 'test-element-timing' + expectedNavigationId); + if (entry) { + navigationId = entry.navigationId; + resolve(); + } + }).observe({ type: 'element' }); + }); + + let el = document.createElement('p'); + el.setAttribute('elementtiming', 'test-element-timing' + expectedNavigationId); + el.textContent = 'test element timing text'; + document.body.appendChild(el); + await p; + return [navigationId]; +} + +let testLongTask = async () => { + let navigationIds = []; + + let p = new Promise(resolve => { + new PerformanceObserver((list) => { + const entry = list.getEntries().find(e => e.entryType === 'longtask') + if (entry) { + navigationIds.push(entry.navigationId); + navigationIds = navigationIds.concat( + entry.attribution.map(a => a.navigationId)); + resolve(); + } + }).observe({ type: 'longtask' }); + }); + + const script = document.createElement('script'); + script.src = '/performance-timeline/resources/make_long_task.js'; + document.body.appendChild(script); + await p; + document.body.removeChild(script); + return navigationIds; +} + +const testFunctionMap = { + 'mark_measure': testMarkMeasure, + 'resource_timing': testResourceTiming, + 'element_timing': testElementTiming, + 'long_task_task_attribution': testLongTask, +}; + +function runNavigationIdTest(params, description) { + const defaultParams = { + openFunc: url => window.open(url, '_blank', 'noopener'), + scripts: [], + funcBeforeNavigation: () => { }, + targetOrigin: originCrossSite, + navigationTimes: 4, + funcAfterAssertion: () => { }, + } // Apply defaults. + params = { ...defaultParams, ...params }; + + promise_test(async t => { + const pageA = new RemoteContext(token()); + const pageB = new RemoteContext(token()); + + const urlA = executorPath + pageA.context_id; + const urlB = params.targetOrigin + executorPath + pageB.context_id; + // Open url A. + params.openFunc(urlA); + await pageA.execute_script(waitForPageShow); + + // Assert navigation id is 1 when the document is loaded first time. + let navigationIds = await pageA.execute_script(testInitial); + assert_true( + navigationIds.every(t => t === 1), 'All Navigation Ids should be 1.'); + + for (i = 1; i <= params.navigationTimes; i++) { + // Navigate away to url B and back. + await navigateAndThenBack(pageA, pageB, urlB); + + // Assert navigation id increments when the document is load from bfcache. + navigationIds = await pageA.execute_script( + testFunctionMap[params.testName], [i + 1]); + assert_true( + navigationIds.every(t => t === (i + 1)), + params.testName + ' Navigation Id should all be ' + (i + 1) + '.'); + } + }, description); +} diff --git a/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-attributes.tentative.window.js b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-attributes.tentative.window.js new file mode 100644 index 00000000000000..047e97a7521b1b --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-attributes.tentative.window.js @@ -0,0 +1,56 @@ +// META: title=RemoteContextHelper navigation using BFCache +// META: script=./test-helper.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js + +'use strict'; + +// Ensure that empty attributes are reported as empty strings and missing +// attributes are reported as null. +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + const rc1_url = await rc1.executeScript(() => { + return location.href; + }); + // Add a cross-origin iframe. + const rc1_child = await rc1.addIframe( + /*extraConfig=*/ { + origin: 'HTTP_REMOTE_ORIGIN', + scripts: [], + headers: [], + }, + /*attributes=*/ {id: '', name: ''}, + ); + // Use WebSocket to block BFCache. + await useWebSocket(rc1); + const rc1_child_url = await rc1_child.executeScript(() => { + return location.href; + }); + // Check the BFCache result and the reported reasons. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); + await assertNotRestoredReasonsEquals( + rc1, + /*blocked=*/ true, + /*url=*/ rc1_url, + /*src=*/ null, + /*id=*/ null, + /*name=*/ null, + /*reasons=*/['WebSocket'], + /*children=*/[{ + 'blocked': false, + 'url': null, + 'src': rc1_child_url, + // Id and name should be empty. + 'id': '', + 'name': '', + 'reasons': [], + 'children': [] + }]); +}); \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-bfcache-reasons-stay.tentative.window.js b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-bfcache-reasons-stay.tentative.window.js new file mode 100644 index 00000000000000..745c167e790067 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-bfcache-reasons-stay.tentative.window.js @@ -0,0 +1,48 @@ +// META: title=RemoteContextHelper navigation using BFCache +// META: script=./test-helper.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js + +'use strict'; + +// Ensure that notRestoredReasons are only updated after non BFCache navigation. +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + // Use WebSocket to block BFCache. + await useWebSocket(rc1); + const rc1_url = await rc1.executeScript(() => { + return location.href; + }); + + // Check the BFCache result and the reported reasons. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); + await assertNotRestoredReasonsEquals( + rc1, + /*blocked=*/ true, + /*url=*/ rc1_url, + /*src=*/ null, + /*id=*/ null, + /*name=*/ null, + /*reasons=*/['WebSocket'], + /*children=*/[]); + + // This time no blocking feature is used, so the page is restored + // from BFCache. Ensure that the previous reasons stay there. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ true); + await assertNotRestoredReasonsEquals( + rc1, + /*blocked=*/ true, + /*url=*/ rc1_url, + /*src=*/ null, + /*id=*/ null, + /*name=*/ null, + /*reasons=*/['WebSocket'], + /*children=*/[]); +}); diff --git a/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-bfcache.tentative.window.js b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-bfcache.tentative.window.js new file mode 100644 index 00000000000000..73197ef3ab30ae --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-bfcache.tentative.window.js @@ -0,0 +1,27 @@ +// META: title=RemoteContextHelper navigation using BFCache +// META: script=./test-helper.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js + +'use strict'; + +// Ensure that notRestoredReasons is empty for successful BFCache restore. +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + + // Check the BFCache result and verify that no reasons are recorded + // for successful restore. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ true); + assert_true(await rc1.executeScript(() => { + let reasons = + performance.getEntriesByType('navigation')[0].notRestoredReasons; + return reasons === null; + })); +}); \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-cross-origin-bfcache.tentative.window.js b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-cross-origin-bfcache.tentative.window.js new file mode 100644 index 00000000000000..bb8284bb1a9776 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-cross-origin-bfcache.tentative.window.js @@ -0,0 +1,62 @@ +// META: title=RemoteContextHelper navigation using BFCache +// META: script=./test-helper.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js + +'use strict'; + +// Ensure that cross-origin subtree's reasons are not exposed to +// notRestoredReasons. +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + const rc1_url = await rc1.executeScript(() => { + return location.href; + }); + // Add a cross-origin iframe. + const rc1_child = await rc1.addIframe( + /*extraConfig=*/ { + origin: 'HTTP_REMOTE_ORIGIN', + scripts: [], + headers: [], + }, + /*attributes=*/ {id: 'test-id'}, + ); + // Use WebSocket to block BFCache. + await useWebSocket(rc1_child); + const rc1_child_url = await rc1_child.executeScript(() => { + return location.href; + }); + // Add a child to the iframe. + const rc1_grand_child = await rc1_child.addIframe(); + const rc1_grand_child_url = await rc1_grand_child.executeScript(() => { + return location.href; + }); + + // Check the BFCache result and the reported reasons. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); + await assertNotRestoredReasonsEquals( + rc1, + /*blocked=*/ false, + /*url=*/ rc1_url, + /*src=*/ null, + /*id=*/ null, + /*name=*/ null, + /*reasons=*/[], + /*children=*/[{ + 'blocked': true, + 'url': null, + 'src': rc1_child_url, + 'id': 'test-id', + // Iframes that are generated by addIframe have an empty name. + 'name': '', + 'reasons': [], + 'children': [] + }]); +}); \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-not-bfcached.tentative.window.js b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-not-bfcached.tentative.window.js new file mode 100644 index 00000000000000..5e94a93089113d --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-not-bfcached.tentative.window.js @@ -0,0 +1,36 @@ +// META: title=RemoteContextHelper navigation using BFCache +// META: script=./test-helper.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js + +'use strict'; + +// Ensure that notRestoredReasons is populated when not restored. +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + // Use WebSocket to block BFCache. + await useWebSocket(rc1); + + const rc1_url = await rc1.executeScript(() => { + return location.href; + }); + + // Check the BFCache result and the reported reasons. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); + await assertNotRestoredReasonsEquals( + rc1, + /*blocked=*/ true, + /*url=*/ rc1_url, + /*src=*/ null, + /*id=*/ null, + /*name=*/ null, + /*reasons=*/['WebSocket'], + /*children=*/[]); +}); \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-redirect-on-history.tentative.window.js b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-redirect-on-history.tentative.window.js new file mode 100644 index 00000000000000..39e9ac9964ae6b --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-redirect-on-history.tentative.window.js @@ -0,0 +1,52 @@ +// META: title=RemoteContextHelper navigation using BFCache +// META: script=./test-helper.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js + +'use strict'; +const {ORIGIN, REMOTE_ORIGIN} = get_host_info(); + +// Ensure that notRestoredReasons reset after the server redirect. +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + // Use WebSocket to block BFCache. + await useWebSocket(rc1); + + // Create a remote context with the redirected URL. + let [rc1_redirected, saveUrl] = + await rcHelper.createContextWithUrl(/*extraConfig=*/ { + origin: 'HTTP_ORIGIN', + scripts: [], + headers: [], + }); + + const redirectUrl = + `${ORIGIN}/common/redirect.py?location=${encodeURIComponent(saveUrl)}`; + // Replace the history state. + await rc1.executeScript((url) => { + window.history.replaceState(null, '', url); + }, [redirectUrl]); + + // Navigate away. + const newRemoteContextHelper = await rc1.navigateToNew(); + + // Go back. + await newRemoteContextHelper.historyBack(); + + const navigation_entry = await rc1_redirected.executeScript(() => { + return performance.getEntriesByType('navigation')[0]; + }); + assert_equals( + navigation_entry.redirectCount, 1, 'Expected redirectCount is 1.'); + // Becauase of the redirect, notRestoredReasons is reset. + assert_equals( + navigation_entry.notRestoredReasons, null, + 'Expected notRestoredReasons is null.'); +}); diff --git a/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-bfcache.tentative.window.js b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-bfcache.tentative.window.js new file mode 100644 index 00000000000000..858d94b26d61f6 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-bfcache.tentative.window.js @@ -0,0 +1,62 @@ +// META: title=RemoteContextHelper navigation using BFCache +// META: script=./test-helper.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js + +'use strict'; + +// Ensure that same-origin subtree's reasons are exposed to notRestoredReasons. +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + const rc1_url = await rc1.executeScript(() => { + return location.href; + }); + // Add a same-origin iframe and use WebSocket. + const rc1_child = await rc1.addIframe( + /*extra_config=*/ {}, /*attributes=*/ {id: 'test-id'}); + await useWebSocket(rc1_child); + + const rc1_child_url = await rc1_child.executeScript(() => { + return location.href; + }); + // Add a child to the iframe. + const rc1_grand_child = await rc1_child.addIframe(); + const rc1_grand_child_url = await rc1_grand_child.executeScript(() => { + return location.href; + }); + + // Check the BFCache result and the reported reasons. + await assertBFCacheEligibility(rc1, /*shouldRestoreFromBFCache=*/ false); + await assertNotRestoredReasonsEquals( + rc1, + /*blocked=*/ false, + /*url=*/ rc1_url, + /*src=*/ '', + /*id=*/ '', + /*name=*/ '', + /*reasons=*/[], + /*children=*/[{ + 'blocked': true, + 'url': rc1_child_url, + 'src': rc1_child_url, + 'id': 'test-id', + 'name': '', + 'reasons': ['WebSocket'], + 'children': [{ + 'blocked': false, + 'url': rc1_grand_child_url, + 'src': rc1_grand_child_url, + 'id': '', + 'name': '', + 'reasons': [], + 'children': [] + }] + }]); +}); \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-replace.tentative.window.js b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-replace.tentative.window.js new file mode 100644 index 00000000000000..e8805be6c4b5e1 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/not-restored-reasons/performance-navigation-timing-same-origin-replace.tentative.window.js @@ -0,0 +1,46 @@ +// META: title=RemoteContextHelper navigation using BFCache +// META: script=./test-helper.js +// META: script=/common/dispatcher/dispatcher.js +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js +// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js +// META: script=/websockets/constants.sub.js + +'use strict'; + +// Ensure that notRestoredReasons are accessible after history replace. +promise_test(async t => { + const rcHelper = new RemoteContextHelper(); + // Open a window with noopener so that BFCache will work. + const rc1 = await rcHelper.addWindow( + /*config=*/ null, /*options=*/ {features: 'noopener'}); + const rc1_url = await rc1.executeScript(() => { + return location.href; + }); + + // Use WebSocket to block BFCache. + await useWebSocket(rc1); + // Navigate away. + const newRemoteContextHelper = await rc1.navigateToNew(); + // Replace the history state to a same-origin site. + await newRemoteContextHelper.executeScript((destUrl) => { + window.history.replaceState(null, '', '#'); + }); + // Go back. + await newRemoteContextHelper.historyBack(); + + const navigation_entry = await rc1.executeScript(() => { + return performance.getEntriesByType('navigation')[0]; + }); + // Reasons are not reset for same-origin replace. + await assertNotRestoredReasonsEquals( + rc1, + /*blocked=*/ true, + /*url=*/ rc1_url, + /*src=*/ null, + /*id=*/ null, + /*name=*/ null, + /*reasons=*/['WebSocket'], + /*children=*/[]); +}); diff --git a/test/fixtures/wpt/performance-timeline/not-restored-reasons/test-helper.js b/test/fixtures/wpt/performance-timeline/not-restored-reasons/test-helper.js new file mode 100644 index 00000000000000..0737f719bfbabe --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/not-restored-reasons/test-helper.js @@ -0,0 +1,47 @@ +async function assertNotRestoredReasonsEquals( + remoteContextHelper, blocked, url, src, id, name, reasons, children) { + let result = await remoteContextHelper.executeScript(() => { + return performance.getEntriesByType('navigation')[0].notRestoredReasons; + }); + assertReasonsStructEquals( + result, blocked, url, src, id, name, reasons, children); +} + +function assertReasonsStructEquals( + result, blocked, url, src, id, name, reasons, children) { + assert_equals(result.blocked, blocked); + assert_equals(result.url, url); + assert_equals(result.src, src); + assert_equals(result.id, id); + assert_equals(result.name, name); + // Reasons should match. + assert_equals(result.reasons.length, reasons.length); + reasons.sort(); + result.reasons.sort(); + for (let i = 0; i < reasons.length; i++) { + assert_equals(result.reasons[i], reasons[i]); + } + // Children should match. + assert_equals(result.children.length, children.length); + children.sort(); + result.children.sort(); + for (let j = 0; j < children.length; j++) { + assertReasonsStructEquals( + result.children[0], children[0].blocked, children[0].url, + children[0].src, children[0].id, children[0].name, children[0].reasons, + children[0].children); + } +} + +// Requires: +// - /websockets/constants.sub.js in the test file and pass the domainPort +// constant here. +async function useWebSocket(remoteContextHelper) { + let return_value = await remoteContextHelper.executeScript((domain) => { + return new Promise((resolve) => { + var webSocketInNotRestoredReasonsTests = new WebSocket(domain + '/echo'); + webSocketInNotRestoredReasonsTests.onopen = () => { resolve(42); }; + }); + }, [SCHEME_DOMAIN_PORT]); + assert_equals(return_value, 42); +} \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/resources/child-frame.html b/test/fixtures/wpt/performance-timeline/resources/child-frame.html new file mode 100644 index 00000000000000..846979358ebbca --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/resources/child-frame.html @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/FileAPI/BlobURL/support/file_test2.txt b/test/fixtures/wpt/performance-timeline/resources/empty.html similarity index 100% rename from test/fixtures/wpt/FileAPI/BlobURL/support/file_test2.txt rename to test/fixtures/wpt/performance-timeline/resources/empty.html diff --git a/test/fixtures/wpt/performance-timeline/resources/include-frames-subframe.html b/test/fixtures/wpt/performance-timeline/resources/include-frames-subframe.html new file mode 100644 index 00000000000000..73df04b046e16e --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/resources/include-frames-subframe.html @@ -0,0 +1,47 @@ + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/resources/json_resource.json b/test/fixtures/wpt/performance-timeline/resources/json_resource.json new file mode 100644 index 00000000000000..68b6ac1d56f7c2 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/resources/json_resource.json @@ -0,0 +1,4 @@ +{ + "name": "nav_id_test", + "target": "resource_timing" +} \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/resources/make_long_task.js b/test/fixtures/wpt/performance-timeline/resources/make_long_task.js new file mode 100644 index 00000000000000..a52d6d839298cc --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/resources/make_long_task.js @@ -0,0 +1,4 @@ +(function () { + let now = window.performance.now(); + while (window.performance.now() < now + 60); +}()); \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/resources/navigation-id-detached-frame-page.html b/test/fixtures/wpt/performance-timeline/resources/navigation-id-detached-frame-page.html new file mode 100644 index 00000000000000..02aafbb5c78368 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/resources/navigation-id-detached-frame-page.html @@ -0,0 +1,21 @@ + + + + + + The navigation_id Detached iframe Page. + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/performance-timeline/supportedEntryTypes-cross-realm-access.html b/test/fixtures/wpt/performance-timeline/supportedEntryTypes-cross-realm-access.html new file mode 100644 index 00000000000000..8b86a6398bfd49 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/supportedEntryTypes-cross-realm-access.html @@ -0,0 +1,18 @@ + + +Cross-realm access of supportedEntryTypes returns Array of another realm + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/detached-frame.html b/test/fixtures/wpt/performance-timeline/tentative/detached-frame.html new file mode 100644 index 00000000000000..70019223a648d9 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/detached-frame.html @@ -0,0 +1,27 @@ + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-A-A.html b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-A-A.html new file mode 100644 index 00000000000000..58480c7af1e649 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-A-A.html @@ -0,0 +1,79 @@ + + + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-A.html b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-A.html new file mode 100644 index 00000000000000..277a2376aefbcf --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-A.html @@ -0,0 +1,74 @@ + + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-AA.html b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-AA.html new file mode 100644 index 00000000000000..485e1d2a115c2d --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-AA.html @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-AB.html b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-AB.html new file mode 100644 index 00000000000000..7650a61e499d05 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-AB.html @@ -0,0 +1,56 @@ + + + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-B-A.html b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-B-A.html new file mode 100644 index 00000000000000..9b873bdde6e186 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-B-A.html @@ -0,0 +1,83 @@ + + + + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-B-B.html b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-B-B.html new file mode 100644 index 00000000000000..b182931e5dceee --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-B-B.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-B.html b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-B.html new file mode 100644 index 00000000000000..0686c9b1e5876c --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/include-frames-originA-B.html @@ -0,0 +1,48 @@ + + + + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/performance-entry-source.html b/test/fixtures/wpt/performance-timeline/tentative/performance-entry-source.html new file mode 100644 index 00000000000000..471a52ab285882 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/performance-entry-source.html @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/tentative/with-filter-options-originA.html b/test/fixtures/wpt/performance-timeline/tentative/with-filter-options-originA.html new file mode 100644 index 00000000000000..6c6643df75cf09 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/tentative/with-filter-options-originA.html @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/test/fixtures/wpt/performance-timeline/timing-removed-iframe.html b/test/fixtures/wpt/performance-timeline/timing-removed-iframe.html new file mode 100644 index 00000000000000..43988b21fbbe02 --- /dev/null +++ b/test/fixtures/wpt/performance-timeline/timing-removed-iframe.html @@ -0,0 +1,16 @@ + + + + + + + diff --git a/test/fixtures/wpt/resource-timing/body-size-cross-origin.https.html b/test/fixtures/wpt/resource-timing/body-size-cross-origin.https.html new file mode 100644 index 00000000000000..b0340139bf7f40 --- /dev/null +++ b/test/fixtures/wpt/resource-timing/body-size-cross-origin.https.html @@ -0,0 +1,63 @@ + + + + +Verify that encodedBodySize/decodedBodySize are CORS-protected rather than TAO-protected + + + + + + + + + + diff --git a/test/fixtures/wpt/resource-timing/content-type-parsing.html b/test/fixtures/wpt/resource-timing/content-type-parsing.html new file mode 100644 index 00000000000000..c0081eb4137d9f --- /dev/null +++ b/test/fixtures/wpt/resource-timing/content-type-parsing.html @@ -0,0 +1,76 @@ + + + +This test validates the parsing of content-type of resources. + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/resource-timing/content-type.html b/test/fixtures/wpt/resource-timing/content-type.html new file mode 100644 index 00000000000000..f6b1db7d9f8e71 --- /dev/null +++ b/test/fixtures/wpt/resource-timing/content-type.html @@ -0,0 +1,117 @@ + + + +This test validates the content-type of resources. + + + + + + + + + + + \ No newline at end of file diff --git a/test/fixtures/wpt/resource-timing/cross-origin-start-end-time-with-redirects.html b/test/fixtures/wpt/resource-timing/cross-origin-start-end-time-with-redirects.html index 1b107d3aef7764..8e368d13807745 100644 --- a/test/fixtures/wpt/resource-timing/cross-origin-start-end-time-with-redirects.html +++ b/test/fixtures/wpt/resource-timing/cross-origin-start-end-time-with-redirects.html @@ -19,13 +19,19 @@ const blank_page = `/resource-timing/resources/blank_page_green.htm`; const destUrl = `/common/slow-redirect.py?delay=${delay}&location=${REMOTE_ORIGIN}/${blank_page}`; -const timeBefore = performance.now() -attribute_test(load.iframe, destUrl, entry => { - assert_equals(entry.startTime, entry.fetchStart, 'startTime and fetchStart should be equal'); - assert_greater_than(entry.startTime, timeBefore, 'startTime and fetchStart should be greater than the time before fetching'); - // See https://github.com/w3c/resource-timing/issues/264 - assert_less_than(Math.round(entry.startTime - timeBefore), delay * 1000, 'startTime should not expose redirect delays'); -}, "Verify that cross-origin resources don't implicitly expose their redirect timings") +const timeBefore = performance.now(); +(async () => { + // Wait 10 ms, to ensure the difference between startTime and timeBefore is + // larger than 1 ms, to avoid flakiness in browsers that clamp timestamps to + // 1 ms. + await new Promise(r => step_timeout(r, 10)); + attribute_test(load.iframe, destUrl, entry => { + assert_equals(entry.startTime, entry.fetchStart, 'startTime and fetchStart should be equal'); + assert_greater_than(entry.startTime, timeBefore, 'startTime and fetchStart should be greater than the time before fetching'); + // See https://github.com/w3c/resource-timing/issues/264 + assert_less_than(Math.round(entry.startTime - timeBefore), delay * 1000, 'startTime should not expose redirect delays'); + }, "Verify that cross-origin resources don't implicitly expose their redirect timings") +})(); diff --git a/test/fixtures/wpt/resource-timing/delivery-type.tentative.any.js b/test/fixtures/wpt/resource-timing/delivery-type.tentative.any.js new file mode 100644 index 00000000000000..e2b408fdd74f29 --- /dev/null +++ b/test/fixtures/wpt/resource-timing/delivery-type.tentative.any.js @@ -0,0 +1,90 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=/resource-timing/resources/resource-loaders.js + +// TODO(crbug/1358591): Rename this file from "tentative" once +// `w3c/resource-timing#343` is merged. + +const {REMOTE_ORIGIN, ORIGIN} = get_host_info(); + +const redirectBase = new URL( + '/resource-timing/resources/redirect-cors.py', REMOTE_ORIGIN).href; +const cacheAndValidatedBase = new URL( + '/resource-timing/resources/cacheable-and-validated.py?content=content', + ORIGIN).href; + +const mustRevalidate = {headers: {'Cache-Control': 'max-age=0'}}; + +const fetchAndEatBody = (url, fetchOption) => { + return fetch(url, fetchOption).then(response => response.arrayBuffer()); +}; + +const accumulateEntries = () => { + return new Promise(resolve => { + const po = new PerformanceObserver(list => { + resolve(list); + }); + po.observe({type: "resource", buffered: true}); + }); +}; + +const checkDeliveryTypeBase = + (list, lookupURL, deliveryTypeForCachedResources) => { + const entries = list.getEntriesByName(lookupURL); + assert_equals(entries.length, 3, 'Wrong number of entries'); + + // 200 response (`cacheMode` is an empty string) + assert_equals(entries[0].deliveryType, "", + "Expect empty deliveryType for 200 response."); + // Cached response (`cacheMode` is "local") or 304 response (`cacheMode` is + // "validated"). + assert_equals(entries[1].deliveryType, deliveryTypeForCachedResources, + `Expect "${deliveryTypeForCachedResources}" deliveryType for a + cached response.`); + assert_equals(entries[2].deliveryType, deliveryTypeForCachedResources, + `Expect "${deliveryTypeForCachedResources}" deliveryType for a + revalidated response.`); +}; + +promise_test(() => { + // Use a different URL every time so that the cache behaviour does not depend + // on execution order. + const initialURL = load.cache_bust(cacheAndValidatedBase); + const checkDeliveryType = + list => checkDeliveryTypeBase(list, initialURL, "cache"); + return fetchAndEatBody(initialURL, {}) // 200. + .then(() => fetchAndEatBody(initialURL, {})) // Cached. + .then(() => fetchAndEatBody(initialURL, mustRevalidate)) // 304. + .then(accumulateEntries) + .then(checkDeliveryType); +}, 'PerformanceResourceTiming deliveryType test, same origin.'); + +promise_test(() => { + const cacheAndValidatedURL = load.cache_bust( + cacheAndValidatedBase + '&timing_allow_origin=*'); + const redirectURL = redirectBase + + "?timing_allow_origin=*" + + `&allow_origin=${encodeURIComponent(ORIGIN)}` + + `&location=${encodeURIComponent(cacheAndValidatedURL)}`; + const checkDeliveryType = + list => checkDeliveryTypeBase(list, redirectURL, "cache"); + return fetchAndEatBody(redirectURL, {}) // 200. + .then(() => fetchAndEatBody(redirectURL, {})) // Cached. + .then(() => fetchAndEatBody(redirectURL, mustRevalidate)) // 304. + .then(accumulateEntries) + .then(checkDeliveryType); +}, 'PerformanceResourceTiming deliveryType test, cross origin, TAO passes.'); + +promise_test(() => { + const cacheAndValidatedURL = load.cache_bust(cacheAndValidatedBase); + const redirectURL = redirectBase + + `?allow_origin=${encodeURIComponent(ORIGIN)}` + + `&location=${encodeURIComponent(cacheAndValidatedURL)}`; + const checkDeliveryType = + list => checkDeliveryTypeBase(list, redirectURL, ""); + return fetchAndEatBody(redirectURL, {}) // 200. + .then(() => fetchAndEatBody(redirectURL, {})) // Cached. + .then(() => fetchAndEatBody(redirectURL, mustRevalidate)) // 304. + .then(accumulateEntries) + .then(checkDeliveryType); +}, 'PerformanceResourceTiming deliveryType test, cross origin, TAO fails.'); diff --git a/test/fixtures/wpt/resource-timing/iframe-failed-commit.html b/test/fixtures/wpt/resource-timing/iframe-failed-commit.html index 1da207d2fbe05e..91094072a6d307 100644 --- a/test/fixtures/wpt/resource-timing/iframe-failed-commit.html +++ b/test/fixtures/wpt/resource-timing/iframe-failed-commit.html @@ -3,6 +3,7 @@ Resource Timing - test that unsuccessful iframes create entries + @@ -20,6 +21,10 @@ return load.iframe_with_attrs(path, {"csp": "default-src 'none'"}); }; +const load_iframe_with_csp_no_navigation = async path => { + return load.iframe_with_attrs(path, {"csp": "default-src 'none'"}, () => {}, true); +} + // Runs a test (labeled by the given label) to verify that loading an iframe // with the given URL generates a PerformanceResourceTiming entry and that the // entry does not expose sensitive timing attributes. @@ -54,6 +59,24 @@ invariants.assert_tao_pass_no_redirect_http_empty, label); }; +// Runs a test (labeled by the given label) to verify that loading an iframe +// with the given URL under a "default-src 'none' Content-Security-Policy +// generates a PerformanceResourceTiming entry and that the entry does not +// expose sensitive timing attributes. +const non_navigating_masked_entry_with_csp_test = (url, label) => { + return attribute_test(load_iframe_with_csp_no_navigation, url, + invariants.assert_tao_failure_resource, label); +}; + +// Runs a test (labeled by the given label) to verify that loading an iframe +// with the given URL, an empty response body and under a "default-src 'none' +// Content-Security-Policy generates a PerformanceResourceTiming entry and that +// the entry does expose sensitive timing attributes. +const non_navigating_empty_unmasked_entry_with_csp_test = (url, label) => { + return attribute_test(load_iframe_with_csp_no_navigation, url, + invariants.assert_tao_pass_no_redirect_http_empty, label); +}; + const {REMOTE_ORIGIN, ORIGINAL_HOST, HTTPS_PORT} = get_host_info(); const unhosted_url = `https://nonexistent.${ORIGINAL_HOST}:${HTTPS_PORT}/`; @@ -87,19 +110,19 @@ new URL("/resource-timing/resources/200_empty.asis", REMOTE_ORIGIN), "Cross-origin empty iframe with a 200 status gets reported"); -unmasked_entry_with_csp_test( - new URL("/resource-timing/resources/204_empty.asis"), +non_navigating_empty_unmasked_entry_with_csp_test( + new URL("/resource-timing/resources/204_empty.asis", location.origin), "Same-origin empty iframe with a 204 status gets reported"); -unmasked_entry_with_csp_test( - new URL("/resource-timing/resources/205_empty.asis"), +non_navigating_empty_unmasked_entry_with_csp_test( + new URL("/resource-timing/resources/205_empty.asis", location.origin), "Same-origin empty iframe with a 205 status gets reported"); -masked_entry_with_csp_test( +non_navigating_masked_entry_with_csp_test( new URL("/resource-timing/resources/204_empty.asis", REMOTE_ORIGIN), "Cross-origin empty iframe with a 204 status gets reported"); -masked_entry_with_csp_test( +non_navigating_masked_entry_with_csp_test( new URL("/resource-timing/resources/205_empty.asis", REMOTE_ORIGIN), "Cross-origin empty iframe with a 205 status gets reported"); diff --git a/test/fixtures/wpt/resource-timing/iframe-sequence-of-events.html b/test/fixtures/wpt/resource-timing/iframe-sequence-of-events.html index 5f99a5cab2de6b..02d1c362c9df49 100644 --- a/test/fixtures/wpt/resource-timing/iframe-sequence-of-events.html +++ b/test/fixtures/wpt/resource-timing/iframe-sequence-of-events.html @@ -4,9 +4,21 @@ + + - \ No newline at end of file + diff --git a/test/fixtures/wpt/resource-timing/initiator-type/link.html b/test/fixtures/wpt/resource-timing/initiator-type/link.html index c49576a8e650bc..43367ac3d501e2 100644 --- a/test/fixtures/wpt/resource-timing/initiator-type/link.html +++ b/test/fixtures/wpt/resource-timing/initiator-type/link.html @@ -1,38 +1,35 @@ + - -Resource Timing initiator type: link - - - - - - + + Resource Timing initiator type: link + + + + + + + + + + + + - - - - - - - -
    This content forces a font to get fetched
+ // Verify there are enries for each of nested.css' nested resources. + initiator_type_test("resource_timing_test0.css?id=n1", "css", "css resources embedded in css"); + initiator_type_test("fonts/Ahem.ttf?id=n1", "css", "font resources embedded in css"); + initiator_type_test("blue.png?id=n1", "css", "image resources embedded in css"); + initiator_type_test("resource_timing_test0.css?id=prefetch", "link", ""); + initiator_type_test("resource_timing_test0.css?id=preload", "link", ""); + initiator_type_test("manifest.json", "link", ""); + initiator_type_test("resources/empty.js?id=modulePreload", "other", "module preload"); + +
    This content forces a font to get fetched
diff --git a/test/fixtures/wpt/resource-timing/interim-response-times.h2.html b/test/fixtures/wpt/resource-timing/interim-response-times.h2.html new file mode 100644 index 00000000000000..850ee7cb5f539b --- /dev/null +++ b/test/fixtures/wpt/resource-timing/interim-response-times.h2.html @@ -0,0 +1,64 @@ + + + + +Resource Timing: PerformanceResourceTiming interim resource times + + + + + + + + diff --git a/test/fixtures/wpt/resource-timing/interim-response-times.html b/test/fixtures/wpt/resource-timing/interim-response-times.html new file mode 100644 index 00000000000000..b922590d5d32c2 --- /dev/null +++ b/test/fixtures/wpt/resource-timing/interim-response-times.html @@ -0,0 +1,62 @@ + + + + +Resource Timing: PerformanceResourceTiming interim resource times + + + + + + + + diff --git a/test/fixtures/wpt/resource-timing/nested-nav-fallback-timing.html b/test/fixtures/wpt/resource-timing/nested-nav-fallback-timing.html new file mode 100644 index 00000000000000..b8bba5614d0d12 --- /dev/null +++ b/test/fixtures/wpt/resource-timing/nested-nav-fallback-timing.html @@ -0,0 +1,33 @@ + + +Test ResourceTiming reporting for cross-origin iframe. + + + + + + + + diff --git a/test/fixtures/wpt/resource-timing/no-entries-for-cross-origin-css-fetched-memory-cache.sub.html b/test/fixtures/wpt/resource-timing/no-entries-for-cross-origin-css-fetched-memory-cache.sub.html new file mode 100644 index 00000000000000..6b60305ded2e99 --- /dev/null +++ b/test/fixtures/wpt/resource-timing/no-entries-for-cross-origin-css-fetched-memory-cache.sub.html @@ -0,0 +1,53 @@ + + +Make sure that resources fetched by cross origin CSS are not in the timeline. + + + + + + + +
    Some content
+ diff --git a/test/fixtures/wpt/resource-timing/object-not-found-after-cross-origin-redirect.html b/test/fixtures/wpt/resource-timing/object-not-found-after-cross-origin-redirect.html index 278c78e320e98c..6990c6c06082e5 100644 --- a/test/fixtures/wpt/resource-timing/object-not-found-after-cross-origin-redirect.html +++ b/test/fixtures/wpt/resource-timing/object-not-found-after-cross-origin-redirect.html @@ -4,6 +4,7 @@ This test validates the values in resource timing for cross-origin redirects. + @@ -15,20 +16,26 @@ diff --git a/test/fixtures/wpt/resource-timing/queue-entry-regardless-buffer-size.html b/test/fixtures/wpt/resource-timing/queue-entry-regardless-buffer-size.html new file mode 100644 index 00000000000000..ea47ae3a7950a2 --- /dev/null +++ b/test/fixtures/wpt/resource-timing/queue-entry-regardless-buffer-size.html @@ -0,0 +1,38 @@ + + + + + +This test validates that resource timing entires should always be queued regardless the size of the buffer. + + + + + + + diff --git a/test/fixtures/wpt/resource-timing/resource-timing-level1.js b/test/fixtures/wpt/resource-timing/resource-timing-level1.js index 95b5cdfb1ed0ca..6167777fe68fa3 100644 --- a/test/fixtures/wpt/resource-timing/resource-timing-level1.js +++ b/test/fixtures/wpt/resource-timing/resource-timing-level1.js @@ -235,47 +235,6 @@ window.onload = }); }); - // Test that responseStart uses the timing of 1XX responses by - // synthesizing a delay between a 100 and 200 status, and verifying that - // this delay is included before responseEnd. If the delay is not - // included, this implies that the 200 status line was (incorrectly) used - // for responseStart timing, despite the 100 response arriving earlier. - // - // Source: "In the case where more than one response is available for a - // request, due to an Informational 1xx response, the reported - // responseStart value is that of the first response to the last - // request." - [ - { initiator: "iframe", response: "(done)", mime: mimeHtml }, - { initiator: "xmlhttprequest", response: "(done)", mime: mimeText }, - { initiator: "script", response: '"";', mime: mimeScript }, - { initiator: "link", response: ".unused{}", mime: mimeCss }, - ] - .forEach(function (template) { - testCases.push({ - description: "'" + template.initiator + " responseStart uses 1XX (first) response timings'", - test: function (test) { - initiateFetch( - test, - template.initiator, - getSyntheticUrl("status:100" - + "&flush" - + "&" + serverStepDelay + "ms" - + "&status:200" - + "&mime:" + template.mime - + "&send:" + encodeURIComponent(template.response)), - function (initiator, entry) { - assert_greater_than_equal( - entry.responseEnd, - entry.responseStart + serverStepDelay, - "HTTP/1.1 1XX (first) response should determine 'responseStart' timing."); - - test.done(); - }); - } - }); - }); - // Function to run the next case in the queue. var currentTestIndex = -1; function runNextCase() { diff --git a/test/fixtures/wpt/resource-timing/resources/delay-load.html b/test/fixtures/wpt/resource-timing/resources/delay-load.html new file mode 100644 index 00000000000000..4898c1be8ebfff --- /dev/null +++ b/test/fixtures/wpt/resource-timing/resources/delay-load.html @@ -0,0 +1,4 @@ + + + + diff --git a/test/fixtures/wpt/resource-timing/resources/entry-invariants.js b/test/fixtures/wpt/resource-timing/resources/entry-invariants.js index 4bef9496103ca6..bbc913b7229030 100644 --- a/test/fixtures/wpt/resource-timing/resources/entry-invariants.js +++ b/test/fixtures/wpt/resource-timing/resources/entry-invariants.js @@ -1,3 +1,19 @@ +const await_with_timeout = async (delay, message, promise, cleanup = ()=>{}) => { + let timeout_id; + const timeout = new Promise((_, reject) => { + timeout_id = step_timeout(() => + reject(new DOMException(message, "TimeoutError")), delay) + }); + let result = null; + try { + result = await Promise.race([promise, timeout]); + clearTimeout(timeout_id); + } finally { + cleanup(); + } + return result; +}; + // Asserts that the given attributes are present in 'entry' and hold equal // values. const assert_all_equal_ = (entry, attributes) => { @@ -75,8 +91,6 @@ const invariants = { assert_positive_(entry, [ "fetchStart", "transferSize", - "encodedBodySize", - "decodedBodySize", ]); }, @@ -98,8 +112,6 @@ const invariants = { "secureConnectionStart", "redirectStart", "redirectEnd", - "encodedBodySize", - "decodedBodySize", ]); assert_not_negative_(entry, [ @@ -139,8 +151,6 @@ const invariants = { assert_positive_(entry, [ "fetchStart", "transferSize", - "encodedBodySize", - "decodedBodySize", ]); }, @@ -172,8 +182,6 @@ const invariants = { assert_positive_(entry, [ "fetchStart", "transferSize", - "encodedBodySize", - "decodedBodySize", ]); }, @@ -196,8 +204,6 @@ const invariants = { "secureConnectionStart", "redirectStart", "redirectEnd", - "encodedBodySize", - "decodedBodySize", ]); assert_not_negative_(entry, [ @@ -229,8 +235,6 @@ const invariants = { "workerStart", "redirectStart", "redirectEnd", - "encodedBodySize", - "decodedBodySize", ]); assert_not_negative_(entry, [ @@ -405,8 +409,6 @@ const invariants = { "requestStart", "responseStart", "transferSize", - "encodedBodySize", - "decodedBodySize", ]); assert_ordered_(entry, [ @@ -440,8 +442,6 @@ const invariants = { "requestStart", "responseStart", "transferSize", - "encodedBodySize", - "decodedBodySize", ]); } @@ -467,7 +467,10 @@ const attribute_test_internal = (loader, path, validator, run_test, test_label) }); await loader(path, validator); - const entry = await(loaded_entry); + const entry = await await_with_timeout(2000, + "Timeout was reached before entry fired", + loaded_entry); + assert_not_equals(entry, null, 'No entry was received'); run_test(entry); }, test_label); }; diff --git a/test/fixtures/wpt/resource-timing/resources/frame-timing.js b/test/fixtures/wpt/resource-timing/resources/frame-timing.js index e0c364e9b2c3e2..019bd424b55065 100644 --- a/test/fixtures/wpt/resource-timing/resources/frame-timing.js +++ b/test/fixtures/wpt/resource-timing/resources/frame-timing.js @@ -1,48 +1,63 @@ function test_frame_timing_before_load_event(type) { - promise_test(async t => { - const {document, performance} = type === 'frame' ? window.parent : window; - const delay = 500; - const frame = document.createElement(type); - t.add_cleanup(() => frame.remove()); - await new Promise(resolve => { - frame.addEventListener('load', resolve); - frame.src = `resources/iframe-with-delay.sub.html?delay=${delay}`; - (type === 'frame' ? document.querySelector('frameset') : document.body).appendChild(frame); - }); + promise_test(async t => { + const {document, performance} = type === 'frame' ? window.parent : window; + const delay = 500; + const frame = document.createElement(type); + t.add_cleanup(() => frame.remove()); + await new Promise(resolve => { + frame.addEventListener('load', resolve); + frame.src = `/resource-timing/resources/iframe-with-delay.sub.html?delay=${delay}`; + (type === 'frame' ? document.querySelector('frameset') : document.body).appendChild(frame); + }); - const entries = performance.getEntriesByName(frame.src); - const navigationEntry = frame.contentWindow.performance.getEntriesByType('navigation')[0]; - assert_equals(entries.length, 1); - assert_equals(entries[0].initiatorType, type); - assert_greater_than(performance.now(), entries[0].responseEnd + delay); - const domContentLoadedEventAbsoluteTime = navigationEntry.domContentLoadedEventStart + frame.contentWindow.performance.timeOrigin; - const frameResponseEndAbsoluteTime = entries[0].responseEnd + performance.timeOrigin; - assert_greater_than(domContentLoadedEventAbsoluteTime, frameResponseEndAbsoluteTime); - }, `A ${type} should report its RT entry when the response is done and before it is completely loaded`); + const entries = performance.getEntriesByName(frame.src); + const navigationEntry = frame.contentWindow.performance.getEntriesByType('navigation')[0]; + assert_equals(entries.length, 1); + assert_equals(entries[0].initiatorType, type); + assert_greater_than(performance.now(), entries[0].responseEnd + delay); + const domContentLoadedEventAbsoluteTime = + navigationEntry.domContentLoadedEventStart + + frame.contentWindow.performance.timeOrigin; + const frameResponseEndAbsoluteTime = entries[0].responseEnd + performance.timeOrigin; + assert_greater_than(domContentLoadedEventAbsoluteTime, frameResponseEndAbsoluteTime); + }, `A ${type} should report its RT entry when the response is done and before it is completely loaded`); } -function test_frame_timing_change_src(type) { - promise_test(async t => { - const {document, performance} = type === 'frame' ? window.parent : window; - const frame = document.createElement(type); - t.add_cleanup(() => frame.remove()); - await new Promise(resolve => { - const done = () => { - resolve(); - frame.removeEventListener('load', done); - } - frame.addEventListener('load', done); - frame.src = 'resources/green.html?1'; - (type === 'frame' ? document.querySelector('frameset') : document.body).appendChild(frame); - }); +function test_frame_timing_change_src(type, + origin1 = document.origin, + origin2 = document.origin, + tao = false, label = '') { + const uid = token(); + promise_test(async t => { + const {document, performance} = type === 'frame' ? window.parent : window; + const frame = document.createElement(type); + t.add_cleanup(() => frame.remove()); + function createURL(origin) { + const url = new URL(`${origin}/resource-timing/resources/green.html`, location.href); + url.searchParams.set("uid", uid); + if (tao) + url.searchParams.set("pipe", "header(Timing-Allow-Origin, *)"); + return url.toString(); + } - await new Promise(resolve => { - frame.addEventListener('load', resolve); - frame.src = 'resources/green.html?2'; - }); + await new Promise(resolve => { + const done = () => { + resolve(); + frame.removeEventListener('load', done); + } + frame.addEventListener('load', done); + frame.src = createURL(origin1); + const root = type === 'frame' ? document.querySelector('frameset') : document.body; + root.appendChild(frame); + }); - const entries = performance.getEntries().filter(e => e.name.includes('green.html')); - assert_equals(entries.length, 2); - }, `A ${type} should report separate RT entries if its src changed from the outside`); -} \ No newline at end of file + await new Promise(resolve => { + frame.addEventListener('load', resolve); + frame.src = createURL(origin2); + }); + + const entries = performance.getEntries().filter(e => e.name.includes(uid)); + assert_equals(entries.length, 2); + }, label || `A ${type} should report separate RT entries if its src changed from the outside`); +} diff --git a/test/fixtures/wpt/resource-timing/resources/nested-contexts.js b/test/fixtures/wpt/resource-timing/resources/nested-contexts.js index c0822943e86a68..31337ae5da2e18 100644 --- a/test/fixtures/wpt/resource-timing/resources/nested-contexts.js +++ b/test/fixtures/wpt/resource-timing/resources/nested-contexts.js @@ -22,22 +22,12 @@ const post_refresh_url = const setup_navigate_or_refresh = (type, pre, post) => { const verify_document_navigate_not_observable = () => { - const entries = performance.getEntriesByType("resource"); - let found_first_document = false; - for (entry of entries) { - if (entry.name == pre) { - found_first_document = true; - } - if (entry.name == post) { - opener.postMessage(`FAIL - ${type} document should not be observable`, - `*`); - return; - } - } - if (!found_first_document) { - opener.postMessage("FAIL - initial document should be observable", "*"); - return; + if (performance.getEntriesByName(post).length) { + opener.postMessage(`FAIL - ${type} document should not be observable`, + `*`); + } + opener.postMessage("PASS", "*"); } window.addEventListener("message", e => { @@ -57,21 +47,8 @@ const setup_refresh_test = () => { const setup_back_navigation = pushed_url => { const verify_document_navigate_not_observable = navigated_back => { - const entries = performance.getEntriesByType("resource"); - let found_first_document = false; - for (entry of entries) { - if (entry.name == pre_navigate_url) { - found_first_document = true; - } - if (entry.name == post_navigate_url) { - opener.postMessage("FAIL - navigated document exposed", "*"); - return; - } - } - if (!found_first_document) { - opener.postMessage(`FAIL - first document not exposed. navigated_back ` + - `is ${navigated_back}`, "*"); - return; + if (performance.getEntriesByName(post_navigate_url).length) { + opener.postMessage("FAIL - navigated document exposed", "*"); } if (navigated_back) { opener.postMessage("PASS", "*"); diff --git a/test/fixtures/wpt/resource-timing/resources/no-entries-for-cross-origin-css-fetched-memory-cache-iframe.sub.html b/test/fixtures/wpt/resource-timing/resources/no-entries-for-cross-origin-css-fetched-memory-cache-iframe.sub.html new file mode 100644 index 00000000000000..f47913468b68eb --- /dev/null +++ b/test/fixtures/wpt/resource-timing/resources/no-entries-for-cross-origin-css-fetched-memory-cache-iframe.sub.html @@ -0,0 +1,8 @@ + + + + + + +
    Some content
+ diff --git a/test/fixtures/wpt/resource-timing/resources/resource-loaders.js b/test/fixtures/wpt/resource-timing/resources/resource-loaders.js index 99a2c28d2b3b27..37fea16b1750fa 100644 --- a/test/fixtures/wpt/resource-timing/resources/resource-loaders.js +++ b/test/fixtures/wpt/resource-timing/resources/resource-loaders.js @@ -1,23 +1,38 @@ const load = { - _cache_bust_value: Math.random().toString().substr(2), cache_bust: path => { let url = new URL(path, location.origin); url.href += (url.href.includes("?")) ? '&' : '?'; - url.href += "unique=" + load._cache_bust_value++ + // The `Number` type in Javascript, when interpreted as an integer, can only + // safely represent [-2^53 + 1, 2^53 - 1] without the loss of precision [1]. + // We do not generate a global value and increment from it, as the increment + // might not have enough precision to be reflected. + // + // [1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number + url.href += "unique=" + Math.random().toString().substring(2); return url.href; }, - // Returns a promise that settles once the given path has been fetched as an - // image resource. - image: path => { + image_with_attrs: async (path, attribute_map) => { return new Promise(resolve => { const img = new Image(); + for (const key in attribute_map) + img[key] = attribute_map[key]; img.onload = img.onerror = resolve; img.src = load.cache_bust(path); }); }, + // Returns a promise that settles once the given path has been fetched as an + // image resource. + image: path => { + return load.image_with_attrs(path, undefined); + }, + + // Returns a promise that settles once the given path has been fetched as an + // image resource. + image_cors: path => load.image_with_attrs(path, {crossOrigin: "anonymous"}), + // Returns a promise that settles once the given path has been fetched as a // font resource. font: path => { @@ -37,10 +52,13 @@ const load = { }); }, - // Returns a promise that settles once the given path has been fetched as a - // stylesheet resource. - stylesheet: async path => { + stylesheet_with_attrs: async (path, attribute_map) => { const link = document.createElement("link"); + if (attribute_map instanceof Object) { + for (const [key, value] of Object.entries(attribute_map)) { + link[key] = value; + } + } link.rel = "stylesheet"; link.type = "text/css"; link.href = load.cache_bust(path); @@ -54,7 +72,13 @@ const load = { document.head.removeChild(link); }, - iframe_with_attrs: async (path, attribute_map, validator) => { + // Returns a promise that settles once the given path has been fetched as a + // stylesheet resource. + stylesheet: async path => { + return load.stylesheet_with_attrs(path, undefined); + }, + + iframe_with_attrs: async (path, attribute_map, validator, skip_wait_for_navigation) => { const frame = document.createElement("iframe"); if (attribute_map instanceof Object) { for (const [key, value] of Object.entries(attribute_map)) { @@ -66,11 +90,17 @@ const load = { }); frame.src = load.cache_bust(path); document.body.appendChild(frame); - await loaded; + if ( !skip_wait_for_navigation ) { + await loaded; + } if (validator instanceof Function) { validator(frame); } - document.body.removeChild(frame); + // since we skipped the wait for load animation, we cannot + // remove the iframe here since the request could get cancelled + if ( !skip_wait_for_navigation ) { + document.body.removeChild(frame); + } }, // Returns a promise that settles once the given path has been fetched as an @@ -79,10 +109,13 @@ const load = { return load.iframe_with_attrs(path, undefined, validator); }, - // Returns a promise that settles once the given path has been fetched as a - // script. - script: async path => { + script_with_attrs: async (path, attribute_map) => { const script = document.createElement("script"); + if (attribute_map instanceof Object) { + for (const [key, value] of Object.entries(attribute_map)) { + script[key] = value; + } + } const loaded = new Promise(resolve => { script.onload = script.onerror = resolve; }); @@ -92,21 +125,29 @@ const load = { document.body.removeChild(script); }, + // Returns a promise that settles once the given path has been fetched as a + // script. + script: async path => { + return load.script_with_attrs(path, undefined); + }, + // Returns a promise that settles once the given path has been fetched as an // object. object: async (path, type) => { const object = document.createElement("object"); - const loaded = new Promise(resolve => { + const object_load_settled = new Promise(resolve => { object.onload = object.onerror = resolve; }); object.data = load.cache_bust(path); if (type) { object.type = type; } - object.style = "width: 0px; height: 0px"; document.body.appendChild(object); - await loaded; - document.body.removeChild(object); + await await_with_timeout(2000, + "Timeout was reached before load or error events fired", + object_load_settled, + () => { document.body.removeChild(object) } + ); }, // Returns a promise that settles once the given path has been fetched diff --git a/test/fixtures/wpt/resource-timing/response-status-code.html b/test/fixtures/wpt/resource-timing/response-status-code.html new file mode 100644 index 00000000000000..3a184c6f016b28 --- /dev/null +++ b/test/fixtures/wpt/resource-timing/response-status-code.html @@ -0,0 +1,165 @@ + + + + +This test validates the response status of resources. + + + + + + + + + + + diff --git a/test/fixtures/wpt/resource-timing/sizes-redirect-img.html b/test/fixtures/wpt/resource-timing/sizes-redirect-img.html index 786018d0c4634c..e440029782b5d5 100644 --- a/test/fixtures/wpt/resource-timing/sizes-redirect-img.html +++ b/test/fixtures/wpt/resource-timing/sizes-redirect-img.html @@ -18,7 +18,7 @@ const redirectUrl = (redirectSourceOrigin, targetUrl) => { return redirectSourceOrigin + - '/resource-timing/resources/redirect-cors.py?timing_allow_origin=*' + + '/resource-timing/resources/redirect-cors.py?allow_origin=*&timing_allow_origin=*' + '&location=' + encodeURIComponent(targetUrl); }; @@ -35,18 +35,18 @@ verify_entry, "PerformanceResourceTiming sizes redirect image - same origin redirect"); -attribute_test(load.image, +attribute_test(load.image_cors, redirectUrl(hostInfo.HTTP_REMOTE_ORIGIN, baseUrl), verify_entry, "PerformanceResourceTiming sizes redirect image - cross origin redirect"); -attribute_test(load.image, +attribute_test(load.image_cors, redirectUrl(hostInfo.HTTP_REMOTE_ORIGIN, redirectUrl(hostInfo.HTTP_ORIGIN, baseUrl)), verify_entry, "PerformanceResourceTiming sizes redirect image - cross origin to same origin redirect"); -attribute_test(load.image, +attribute_test(load.image_cors, redirectUrl(hostInfo.HTTP_ORIGIN, redirectUrl(hostInfo.HTTP_REMOTE_ORIGIN, redirectUrl(hostInfo.HTTP_ORIGIN, diff --git a/test/fixtures/wpt/resource-timing/tojson.html b/test/fixtures/wpt/resource-timing/tojson.html index 7a6187d3d55bcd..2564b855dffb8e 100644 --- a/test/fixtures/wpt/resource-timing/tojson.html +++ b/test/fixtures/wpt/resource-timing/tojson.html @@ -47,7 +47,9 @@ 'transferSize', 'encodedBodySize', 'decodedBodySize', - 'renderBlockingStatus' + 'renderBlockingStatus', + 'responseStatus', + 'contentType', ]; for (const key of performanceResourceTimingKeys) { try { 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..dfb0a1e053c054 --- /dev/null +++ b/test/fixtures/wpt/resources/declarative-shadow-dom-polyfill.js @@ -0,0 +1,16 @@ +/* + * Polyfill for attaching shadow trees for declarative Shadow DOM for implementations that do not support + * declarative Shadow DOM. + * + * root: The root of the subtree to perform the attachments in + */ + +function polyfill_declarative_shadow_dom(root) { + root.querySelectorAll("template[shadowroot]").forEach(template => { + const mode = template.getAttribute("shadowroot"); + 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..bed115a99cefba 100644 --- a/test/fixtures/wpt/resources/testharness.js +++ b/test/fixtures/wpt/resources/testharness.js @@ -2246,7 +2246,8 @@ ReadOnlyError: 0, VersionError: 0, OperationError: 0, - NotAllowedError: 0 + NotAllowedError: 0, + OptOutError: 0 }; var code_name_map = {}; @@ -2477,6 +2478,10 @@ this._user_defined_cleanup_count = 0; this._done_callbacks = []; + if (typeof AbortController === "function") { + this._abortController = new AbortController(); + } + // Tests declared following harness completion are likely an indication // of a programming error, but they cannot be reported // deterministically. @@ -2953,6 +2958,10 @@ this.phase = this.phases.CLEANING; + if (this._abortController) { + this._abortController.abort("Test cleanup"); + } + forEach(this.cleanup_callbacks, function(cleanup_callback) { var result; @@ -3046,6 +3055,16 @@ test._done_callbacks.length = 0; } + /** + * Gives an AbortSignal that will be aborted when the test finishes. + */ + Test.prototype.get_signal = function() { + if (!this._abortController) { + throw new Error("AbortController is not supported in this browser"); + } + return this._abortController.signal; + } + /** * A RemoteTest object mirrors a Test object on a remote worker. The * associated RemoteWorker updates the RemoteTest object in response to @@ -3822,7 +3841,9 @@ return; } - this.pending_remotes.push(this.create_remote_window(remote)); + var remoteContext = this.create_remote_window(remote); + this.pending_remotes.push(remoteContext); + return remoteContext.done; }; /** @@ -3837,7 +3858,7 @@ * @param {Window} window - The window to fetch tests from. */ function fetch_tests_from_window(window) { - tests.fetch_tests_from_window(window); + return tests.fetch_tests_from_window(window); } expose(fetch_tests_from_window, 'fetch_tests_from_window'); @@ -3871,7 +3892,7 @@ */ function begin_shadow_realm_tests(postMessage) { if (!(test_environment instanceof ShadowRealmTestEnvironment)) { - throw new Error("beign_shadow_realm_tests called in non-Shadow Realm environment"); + throw new Error("begin_shadow_realm_tests called in non-Shadow Realm environment"); } test_environment.begin(function (msg) { diff --git a/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js b/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js index 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/url/failure.html b/test/fixtures/wpt/url/failure.html index c22357b6c10749..67873ea2115738 100644 --- a/test/fixtures/wpt/url/failure.html +++ b/test/fixtures/wpt/url/failure.html @@ -5,7 +5,6 @@
-