Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable structuredClone testing in ShadowRealm #49282

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/sab.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const createBuffer = (() => {
globalThis.createBuffer = (() => {
// See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()`
let sabConstructor;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -43,3 +45,4 @@ function runStructuredCloneBatteryOfTests(runner) {
});
Promise.all(allTests).then(_ => runner.teardown());
}
globalThis.runStructuredCloneBatteryOfTests = runStructuredCloneBatteryOfTests;
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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

Expand All @@ -37,39 +66,56 @@ 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])
);
}
});

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;
}
}
});
Expand All @@ -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 {}
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -13,7 +13,8 @@ function check(description, input, callback, requiresDocument = false) {
const copy = await runner.structuredClone(newInput);
await callback(copy, newInput);
},
requiresDocument
requiresDocument,
requiresBlob,
});
}

Expand Down Expand Up @@ -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);
Expand All @@ -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');
Expand All @@ -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')
Expand Down Expand Up @@ -643,39 +644,52 @@ 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)
);
}
});

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 {}
ptomato marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand Down
2 changes: 2 additions & 0 deletions html/webappapis/structured-clone/structured-clone.any.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,4 +12,5 @@ runStructuredCloneBatteryOfTests({
});
},
hasDocument: typeof document !== "undefined",
hasBlob: typeof Blob !== "undefined",
});