diff --git a/common/sab.js b/common/sab.js index a3ea610e165d0d..565d81eb566258 100644 --- a/common/sab.js +++ b/common/sab.js @@ -1,4 +1,4 @@ -const createBuffer = (() => { +globalThis.createBuffer = (() => { // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` let sabConstructor; try { diff --git a/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js b/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js index 00a86fa74b9a96..5e420ca212a068 100644 --- a/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js +++ b/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js @@ -19,14 +19,16 @@ function runStructuredCloneBatteryOfTests(runner) { preTest() {}, postTest() {}, teardown() {}, - hasDocument: true + hasDocument: true, + hasBlob: true, }; runner = Object.assign({}, defaultRunner, runner); let setupPromise = runner.setup(); const allTests = structuredCloneBatteryOfTests.map(test => { - if (!runner.hasDocument && test.requiresDocument) { + if ((!runner.hasDocument && test.requiresDocument) || + (!runner.hasBlob && test.requiresBlob)) { return; } @@ -43,3 +45,4 @@ function runStructuredCloneBatteryOfTests(runner) { }); Promise.all(allTests).then(_ => runner.teardown()); } +globalThis.runStructuredCloneBatteryOfTests = runStructuredCloneBatteryOfTests; diff --git a/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js b/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js index 23cf4f651ac70a..310a0878d51b5b 100644 --- a/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js +++ b/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js @@ -1,3 +1,30 @@ +// Feature detection. +// MessagePort is not Exposed=*, so skip MessagePort-specific tests if it is not +// present. +// Streams, e.g. ReadableStream, are both Exposed=* and Transferable, but the +// transferability is not widely implemented. +// For tests that need _any_ transferable object, use one or the other in order +// to make the test as useful as possible. If neither is available, skip the +// test since transferability of streams is not specifically being tested here. +// There are tests specifically for the transferability of streams in +// streams/transferable/. + +const environmentHasMessagePort = typeof globalThis.MessagePort !== 'undefined' && + typeof globalThis.MessageChannel !== 'undefined'; + +function readableStreamIsTransferable() { + try { + const stream = new ReadableStream(); + structuredClone(stream, [stream]); + return true; + } catch(err) { + if (err instanceof DOMException && err.code === DOMException.DATA_CLONE_ERR) { + return false; + } + throw err; + } +} + structuredCloneBatteryOfTests.push({ description: 'ArrayBuffer', async f(runner) { @@ -8,16 +35,18 @@ structuredCloneBatteryOfTests.push({ } }); -structuredCloneBatteryOfTests.push({ - description: 'MessagePort', - async f(runner) { - const {port1, port2} = new MessageChannel(); - const copy = await runner.structuredClone(port2, [port2]); - const msg = new Promise(resolve => port1.onmessage = resolve); - copy.postMessage('ohai'); - assert_equals((await msg).data, 'ohai'); - } -}); +if (environmentHasMessagePort) { + structuredCloneBatteryOfTests.push({ + description: 'MessagePort', + async f(runner) { + const {port1, port2} = new MessageChannel(); + const copy = await runner.structuredClone(port2, [port2]); + const msg = new Promise(resolve => port1.onmessage = resolve); + copy.postMessage('ohai'); + assert_equals((await msg).data, 'ohai'); + } + }); +} // TODO: ImageBitmap @@ -37,12 +66,19 @@ structuredCloneBatteryOfTests.push({ structuredCloneBatteryOfTests.push({ description: 'A detached platform object cannot be transferred', async f(runner, t) { - const {port1} = new MessageChannel(); - await runner.structuredClone(port1, [port1]); + let xferable; + if (environmentHasMessagePort) { + xferable = new MessageChannel().port1; + } else if (readableStreamIsTransferable()) { + xferable = new ReadableStream(); + } else { + throw new OptionalFeatureUnsupportedError('No suitable exposed and transferable platform object to test'); + } + await runner.structuredClone(xferable, [xferable]); await promise_rejects_dom( t, "DataCloneError", - runner.structuredClone(port1, [port1]) + runner.structuredClone(xferable, [xferable]) ); } }); @@ -50,26 +86,36 @@ structuredCloneBatteryOfTests.push({ structuredCloneBatteryOfTests.push({ description: 'Transferring a non-transferable platform object fails', async f(runner, t) { - const blob = new Blob(); + const exc = new DOMException(); await promise_rejects_dom( t, "DataCloneError", - runner.structuredClone(blob, [blob]) + runner.structuredClone(exc, [exc]) ); - } + }, }); 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; + let xferable, iface, globalPropName; + if (environmentHasMessagePort) { + xferable = new MessageChannel().port1; + iface = globalThis.MessagePort; + globalPropName = 'MessagePort'; + } else if (readableStreamIsTransferable()) { + xferable = new ReadableStream(); + iface = globalThis.ReadableStream; + globalPropName = 'ReadableStream'; + } else { + throw new OptionalFeatureUnsupportedError('No suitable exposed and transferable platform object to test'); + } + delete globalThis[globalPropName]; try { - const transfer = await runner.structuredClone(port1, [port1]); - assert_true(transfer instanceof messagePortInterface); + const transfer = await runner.structuredClone(xferable, [xferable]); + assert_true(transfer instanceof iface); } finally { - globalThis.MessagePort = messagePortInterface; + globalThis[globalPropName] = iface; } } }); @@ -79,16 +125,8 @@ structuredCloneBatteryOfTests.push({ 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; - } + if (!readableStreamIsTransferable()) { + throw new OptionalFeatureUnsupportedError('No suitable subclassable, exposed, and transferable platform object to test'); } class ReadableStreamSubclass extends ReadableStream {} diff --git a/html/webappapis/structured-clone/structured-clone-battery-of-tests.js b/html/webappapis/structured-clone/structured-clone-battery-of-tests.js index 923ac9dc164918..a59fc6063a4ce9 100644 --- a/html/webappapis/structured-clone/structured-clone-battery-of-tests.js +++ b/html/webappapis/structured-clone/structured-clone-battery-of-tests.js @@ -1,8 +1,8 @@ /* This file is mostly a remix of @zcorpanā€™s web worker test suite */ -structuredCloneBatteryOfTests = []; +globalThis.structuredCloneBatteryOfTests = []; -function check(description, input, callback, requiresDocument = false) { +function check(description, input, callback, requiresDocument = false, requiresBlob = false) { structuredCloneBatteryOfTests.push({ description, async f(runner) { @@ -13,7 +13,8 @@ function check(description, input, callback, requiresDocument = false) { const copy = await runner.structuredClone(newInput); await callback(copy, newInput); }, - requiresDocument + requiresDocument, + requiresBlob, }); } @@ -296,7 +297,7 @@ async function compare_Blob(actual, input, expect_File) { function func_Blob_basic() { return new Blob(['foo'], {type:'text/x-bar'}); } -check('Blob basic', func_Blob_basic, compare_Blob); +check('Blob basic', func_Blob_basic, compare_Blob, false, true); function b(str) { return parseInt(str, 2); @@ -322,33 +323,33 @@ function func_Blob_bytes(arr) { return new Blob([view]); }; } -check('Blob unpaired high surrogate (invalid utf-8)', func_Blob_bytes(encode_cesu8([0xD800])), compare_Blob); -check('Blob unpaired low surrogate (invalid utf-8)', func_Blob_bytes(encode_cesu8([0xDC00])), compare_Blob); -check('Blob paired surrogates (invalid utf-8)', func_Blob_bytes(encode_cesu8([0xD800, 0xDC00])), compare_Blob); +check('Blob unpaired high surrogate (invalid utf-8)', func_Blob_bytes(encode_cesu8([0xD800])), compare_Blob, false, true); +check('Blob unpaired low surrogate (invalid utf-8)', func_Blob_bytes(encode_cesu8([0xDC00])), compare_Blob, false, true); +check('Blob paired surrogates (invalid utf-8)', func_Blob_bytes(encode_cesu8([0xD800, 0xDC00])), compare_Blob, false, true); function func_Blob_empty() { return new Blob(['']); } -check('Blob empty', func_Blob_empty , compare_Blob); +check('Blob empty', func_Blob_empty , compare_Blob, false, true); function func_Blob_NUL() { return new Blob(['\u0000']); } -check('Blob NUL', func_Blob_NUL, compare_Blob); +check('Blob NUL', func_Blob_NUL, compare_Blob, false, true); -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('Array Blob object, Blob basic', () => [func_Blob_basic()], compare_Array(enumerate_props(compare_Blob)), false, true); +check('Array Blob object, Blob unpaired high surrogate (invalid utf-8)', () => [func_Blob_bytes([0xD800])()], compare_Array(enumerate_props(compare_Blob)), false, true); +check('Array Blob object, Blob unpaired low surrogate (invalid utf-8)', () => [func_Blob_bytes([0xDC00])()], compare_Array(enumerate_props(compare_Blob)), false, true); +check('Array Blob object, Blob paired surrogates (invalid utf-8)', () => [func_Blob_bytes([0xD800, 0xDC00])()], compare_Array(enumerate_props(compare_Blob)), false, true); +check('Array Blob object, Blob empty', () => [func_Blob_empty()], compare_Array(enumerate_props(compare_Blob)), false, true); +check('Array Blob object, Blob NUL', () => [func_Blob_NUL()], compare_Array(enumerate_props(compare_Blob)), false, true); +check('Array Blob object, two Blobs', () => [func_Blob_basic(), func_Blob_empty()], compare_Array(enumerate_props(compare_Blob)), false, true); -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))); +check('Object Blob object, Blob basic', () => ({'x':func_Blob_basic()}), compare_Object(enumerate_props(compare_Blob)), false, true); +check('Object Blob object, Blob unpaired high surrogate (invalid utf-8)', () => ({'x':func_Blob_bytes([0xD800])()}), compare_Object(enumerate_props(compare_Blob)), false, true); +check('Object Blob object, Blob unpaired low surrogate (invalid utf-8)', () => ({'x':func_Blob_bytes([0xDC00])()}), compare_Object(enumerate_props(compare_Blob)), false, true); +check('Object Blob object, Blob paired surrogates (invalid utf-8)', () => ({'x':func_Blob_bytes([0xD800, 0xDC00])()}), compare_Object(enumerate_props(compare_Blob)), false, true); +check('Object Blob object, Blob empty', () => ({'x':func_Blob_empty()}), compare_Object(enumerate_props(compare_Blob)), false, true); +check('Object Blob object, Blob NUL', () => ({'x':func_Blob_NUL()}), compare_Object(enumerate_props(compare_Blob)), false, true); async function compare_File(actual, input) { assert_true(actual instanceof File, 'instanceof File'); @@ -359,7 +360,7 @@ async function compare_File(actual, input) { function func_File_basic() { return new File(['foo'], 'bar', {type:'text/x-bar', lastModified:42}); } -check('File basic', func_File_basic, compare_File); +check('File basic', func_File_basic, compare_File, false, true); function compare_FileList(actual, input) { if (typeof actual === 'string') @@ -643,11 +644,11 @@ check('ObjectPrototype must lose its exotic-ness when cloned', structuredCloneBatteryOfTests.push({ description: 'Serializing a non-serializable platform object fails', async f(runner, t) { - const request = new Response(); + const ac = new AbortController(); await promise_rejects_dom( t, "DataCloneError", - runner.structuredClone(request) + runner.structuredClone(ac) ); } }); @@ -655,27 +656,40 @@ structuredCloneBatteryOfTests.push({ 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; + const domException = new DOMException(); + const domExceptionInterface = globalThis.DOMException; + delete globalThis.DOMException; try { - const copy = await runner.structuredClone(blob); - assert_true(copy instanceof blobInterface); + const copy = await runner.structuredClone(domException); + assert_true(copy instanceof domExceptionInterface); } finally { - globalThis.Blob = blobInterface; + globalThis.DOMException = domExceptionInterface; } - } + }, }); check( 'A subclass instance will deserialize as its closest serializable superclass', + () => { + class DOMExceptionSubclass extends DOMException {} + return new DOMExceptionSubclass([], ""); + }, + (copy) => { + assert_equals(Object.getPrototypeOf(copy), DOMException.prototype); + } +); + +check( + 'A subclass instance will deserialize as its closest serializable superclass when there are multiple serializable superclasses', () => { class FileSubclass extends File {} return new FileSubclass([], ""); }, (copy) => { - assert_equals(Object.getPrototypeOf(copy), File.prototype); - } + assert_equals(Object.getPrototypeOf(copy), File.prototype, "Prototype is File, not Blob"); + }, + false, + true ); check( diff --git a/html/webappapis/structured-clone/structured-clone.any.js b/html/webappapis/structured-clone/structured-clone.any.js index 1358a71fc03d4e..781f7c32332557 100644 --- a/html/webappapis/structured-clone/structured-clone.any.js +++ b/html/webappapis/structured-clone/structured-clone.any.js @@ -1,4 +1,5 @@ // META: title=structuredClone() tests +// META: global=window,dedicatedworker,shadowrealm // 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 @@ -11,4 +12,5 @@ runStructuredCloneBatteryOfTests({ }); }, hasDocument: typeof document !== "undefined", + hasBlob: typeof Blob !== "undefined", });