diff --git a/Tests/LibWeb/TestConfig.ini b/Tests/LibWeb/TestConfig.ini index 051969bd91b..b64f31d714a 100644 --- a/Tests/LibWeb/TestConfig.ini +++ b/Tests/LibWeb/TestConfig.ini @@ -34,3 +34,7 @@ Text/input/wpt-import/html/syntax/parsing/named-character-references.html Text/input/wpt-import/html/syntax/parsing/support/no-doctype-name-eof.html Text/input/wpt-import/html/syntax/parsing/support/no-doctype-name-line.html Text/input/wpt-import/html/syntax/parsing/support/no-doctype-name-space.html +Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.html +Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-iframe.html +Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/iframe-resizable-arraybuffer-helper.html +Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/post-parent-type-error.html diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.txt b/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.txt new file mode 100644 index 00000000000..9b3116bd0f4 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.txt @@ -0,0 +1,12 @@ +Summary + +Harness status: OK + +Rerun + +Found 2 tests + +2 Pass +Details +Result Test Name MessagePass Throwing name getter fails serialization +Pass Errors sent across realms should preserve their type \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.txt b/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.txt new file mode 100644 index 00000000000..d57ff0e90af --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.txt @@ -0,0 +1,52 @@ +Summary + +Harness status: OK + +Rerun + +Found 41 tests + +36 Pass +5 Fail +Details +Result Test Name MessagePass Primitive string is cloned +Pass Primitive integer is cloned +Pass Primitive floating point is cloned +Pass Primitive floating point (negative) is cloned +Pass Primitive number (hex) is cloned +Pass Primitive number (scientific) is cloned +Pass Primitive boolean is cloned +Pass Instance of Boolean is cloned +Pass Instance of Number is cloned +Pass Instance of String is cloned +Pass Instance of Date is cloned +Pass Instance of RegExp is cloned +Pass Value 'null' is cloned +Pass Value 'undefined' is cloned +Pass Object properties are cloned +Pass Prototype chains are not walked. +Pass Property descriptors of Objects are not cloned +Pass Cycles are preserved in Objects +Fail Identity of duplicates is preserved +Pass Property order is preserved +Pass Enumerable properties of Arrays are cloned +Pass Property descriptors of Arrays are not cloned +Pass Cycles are preserved in Arrays +Fail ImageData object can be cloned Cannot serialize platform objects +Fail ImageData expandos are not cloned Cannot serialize platform objects +Pass Window objects cannot be cloned +Pass Document objects cannot be cloned +Pass Empty Error objects can be cloned +Pass Error objects can be cloned +Pass EvalError objects can be cloned +Pass RangeError objects can be cloned +Pass ReferenceError objects can be cloned +Pass SyntaxError objects can be cloned +Pass TypeError objects can be cloned +Pass URIError objects can be cloned +Pass URIError objects from other realms are treated as URIError +Pass Cloning a modified Error +Pass Error.message: getter is ignored when cloning +Pass Error.message: undefined property is stringified +Fail DOMException objects can be cloned Cannot serialize platform objects +Fail DOMException objects created by the UA can be cloned Cannot serialize platform objects \ No newline at end of file diff --git a/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.txt b/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.txt new file mode 100644 index 00000000000..cb88bc26157 --- /dev/null +++ b/Tests/LibWeb/Text/expected/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.txt @@ -0,0 +1,162 @@ +Summary + +Harness status: OK + +Rerun + +Found 150 tests + +124 Pass +25 Fail +1 Optional Feature Unsupported +Details +Result Test Name MessagePass primitive undefined +Pass primitive null +Pass primitive true +Pass primitive false +Pass primitive string, empty string +Pass primitive string, lone high surrogate +Pass primitive string, lone low surrogate +Pass primitive string, NUL +Pass primitive string, astral character +Pass primitive number, 0.2 +Pass primitive number, 0 +Pass primitive number, -0 +Pass primitive number, NaN +Pass primitive number, Infinity +Pass primitive number, -Infinity +Pass primitive number, 9007199254740992 +Pass primitive number, -9007199254740992 +Pass primitive number, 9007199254740994 +Pass primitive number, -9007199254740994 +Pass primitive BigInt, 0n +Pass primitive BigInt, -0n +Pass primitive BigInt, -9007199254740994000n +Pass primitive BigInt, -9007199254740994000900719925474099400090071992547409940009007199254740994000n +Pass Array primitives +Pass Object primitives +Pass Boolean true +Pass Boolean false +Pass Array Boolean objects +Pass Object Boolean objects +Pass String empty string +Pass String lone high surrogate +Pass String lone low surrogate +Pass String NUL +Pass String astral character +Pass Array String objects +Pass Object String objects +Pass Number 0.2 +Pass Number 0 +Pass Number -0 +Pass Number NaN +Pass Number Infinity +Pass Number -Infinity +Pass Number 9007199254740992 +Pass Number -9007199254740992 +Pass Number 9007199254740994 +Pass Number -9007199254740994 +Pass BigInt -9007199254740994n +Pass Array Number objects +Pass Object Number objects +Pass Date 0 +Pass Date -0 +Pass Date -8.64e15 +Pass Date 8.64e15 +Pass Array Date objects +Pass Object Date objects +Pass RegExp flags and lastIndex +Pass RegExp sticky flag +Pass RegExp unicode flag +Pass RegExp empty +Pass RegExp slash +Pass RegExp new line +Pass Array RegExp object, RegExp flags and lastIndex +Pass Array RegExp object, RegExp sticky flag +Pass Array RegExp object, RegExp unicode flag +Pass Array RegExp object, RegExp empty +Pass Array RegExp object, RegExp slash +Pass Array RegExp object, RegExp new line +Pass Object RegExp object, RegExp flags and lastIndex +Pass Object RegExp object, RegExp sticky flag +Pass Object RegExp object, RegExp unicode flag +Pass Object RegExp object, RegExp empty +Pass Object RegExp object, RegExp slash +Pass Object RegExp object, RegExp new line +Pass Empty Error object +Pass Error object +Pass EvalError object +Pass RangeError object +Pass ReferenceError object +Pass SyntaxError object +Pass TypeError object +Pass URIError object +Pass Blob basic +Pass Blob unpaired high surrogate (invalid utf-8) +Pass Blob unpaired low surrogate (invalid utf-8) +Pass Blob paired surrogates (invalid utf-8) +Pass Blob empty +Pass Blob NUL +Pass Array Blob object, Blob basic +Pass Array Blob object, Blob unpaired high surrogate (invalid utf-8) +Pass Array Blob object, Blob unpaired low surrogate (invalid utf-8) +Pass Array Blob object, Blob paired surrogates (invalid utf-8) +Pass Array Blob object, Blob empty +Pass Array Blob object, Blob NUL +Pass Array Blob object, two Blobs +Pass Object Blob object, Blob basic +Pass Object Blob object, Blob unpaired high surrogate (invalid utf-8) +Pass Object Blob object, Blob unpaired low surrogate (invalid utf-8) +Pass Object Blob object, Blob paired surrogates (invalid utf-8) +Pass Object Blob object, Blob empty +Pass Object Blob object, Blob NUL +Pass File basic +Pass FileList empty +Pass Array FileList object, FileList empty +Pass Object FileList object, FileList empty +Fail ImageData 1x1 transparent black +Fail ImageData 1x1 non-transparent non-black +Fail Array ImageData object, ImageData 1x1 transparent black +Fail Array ImageData object, ImageData 1x1 non-transparent non-black +Fail Object ImageData object, ImageData 1x1 transparent black +Fail Object ImageData object, ImageData 1x1 non-transparent non-black +Pass Array sparse +Pass Array with non-index property +Pass Object with index property and length +Fail Array with circular reference +Fail Object with circular reference +Fail Array with identical property values +Fail Object with identical property values +Pass Object with property on prototype +Pass Object with non-enumerable property +Pass Object with non-writable property +Pass Object with non-configurable property +Pass Object with a getter that throws +Fail ImageBitmap 1x1 transparent black +Fail ImageBitmap 1x1 non-transparent non-black +Fail Array ImageBitmap object, ImageBitmap 1x1 transparent black +Fail Array ImageBitmap object, ImageBitmap 1x1 transparent non-black +Fail Object ImageBitmap object, ImageBitmap 1x1 transparent black +Fail Object ImageBitmap object, ImageBitmap 1x1 transparent non-black +Pass ObjectPrototype must lose its exotic-ness when cloned +Pass Serializing a non-serializable platform object fails +Pass An object whose interface is deleted from the global must still deserialize +Pass A subclass instance will deserialize as its closest serializable superclass +Fail Resizable ArrayBuffer +Fail Growable SharedArrayBuffer +Pass Length-tracking TypedArray +Pass Length-tracking DataView +Pass Serializing OOB TypedArray throws +Pass Serializing OOB DataView throws +Fail ArrayBuffer +Fail MessagePort +Fail A detached ArrayBuffer cannot be transferred +Pass A detached platform object cannot be transferred +Pass Transferring a non-transferable platform object fails +Fail An object whose interface is deleted from the global object must still be received +Optional Feature Unsupported A subclass instance will be received as its closest transferable superclass ReadableStream isn't transferable +Fail Resizable ArrayBuffer is transferable +Fail Length-tracking TypedArray is transferable +Fail Length-tracking DataView is transferable +Pass Transferring OOB TypedArray throws +Pass Transferring OOB DataView throws \ No newline at end of file diff --git a/Tests/LibWeb/Text/input/wpt-import/common/sab.js b/Tests/LibWeb/Text/input/wpt-import/common/sab.js new file mode 100644 index 00000000000..a3ea610e165 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/common/sab.js @@ -0,0 +1,21 @@ +const createBuffer = (() => { + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + let sabConstructor; + try { + sabConstructor = new WebAssembly.Memory({ shared:true, initial:0, maximum:0 }).buffer.constructor; + } catch(e) { + sabConstructor = null; + } + return (type, length, opts) => { + if (type === "ArrayBuffer") { + 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, opts); + } else { + throw new Error("type has to be ArrayBuffer or SharedArrayBuffer"); + } + } +})(); diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/messagechannel.any.js b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/messagechannel.any.js new file mode 100644 index 00000000000..6ba17f7a89a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/messagechannel.any.js @@ -0,0 +1,16 @@ +// META: global=window,worker +// 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(data, transfer) { + return new Promise(resolve => { + const channel = new MessageChannel(); + channel.port2.onmessage = ev => resolve(ev.data.data); + channel.port1.postMessage({data, transfer}, transfer); + }); + }, + hasDocument : self.GLOBAL.isWindow() +}); diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-iframe.html b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-iframe.html new file mode 100644 index 00000000000..c4fd5824a1c --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-iframe.html @@ -0,0 +1,11 @@ + + +A test page that echos back anything postMessaged to it to its parent + + diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-iframe.html.headers b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-iframe.html.headers new file mode 100644 index 00000000000..4e798cd9f5d --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-iframe.html.headers @@ -0,0 +1,2 @@ +Cross-Origin-Embedder-Policy: require-corp +Cross-Origin-Resource-Policy: cross-origin diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-worker.js b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-worker.js new file mode 100644 index 00000000000..cbbde8a73c8 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-worker.js @@ -0,0 +1,5 @@ +"use strict"; + +self.onmessage = ({ data }) => { + self.postMessage(data); +}; diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-worker.js.headers b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-worker.js.headers new file mode 100644 index 00000000000..6604450991a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/echo-worker.js.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy: require-corp diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/iframe-resizable-arraybuffer-helper.html b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/iframe-resizable-arraybuffer-helper.html new file mode 100644 index 00000000000..378c953fbe8 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/iframe-resizable-arraybuffer-helper.html @@ -0,0 +1,11 @@ + + diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/post-parent-type-error.html b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/post-parent-type-error.html new file mode 100644 index 00000000000..d6713c4192e --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/resources/post-parent-type-error.html @@ -0,0 +1,9 @@ + + +Helper that posts its parent a TypeError + + diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.html b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.html new file mode 100644 index 00000000000..260579a230c --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-extra.html @@ -0,0 +1,46 @@ + + +Structured cloning of Error objects: extra tests + + + + + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-stack-optional.sub.window.js b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-stack-optional.sub.window.js new file mode 100644 index 00000000000..cbc6a73d518 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structured-cloning-error-stack-optional.sub.window.js @@ -0,0 +1,106 @@ +// META: script=/common/utils.js + +// .stack properties on errors are unspecified, but are present in most +// browsers, most of the time. https://github.com/tc39/proposal-error-stacks/ tracks standardizing them. +// Tests will pass automatically if the .stack property isn't present. + +stackTests(() => { + return new Error('some message'); +}, 'page-created Error'); + +stackTests(() => { + return new DOMException('InvalidStateError', 'some message'); +}, 'page-created DOMException'); + +stackTests(() => { + try { + Object.defineProperty(); + } catch (e) { + return e; + } +}, 'JS-engine-created TypeError'); + +stackTests(() => { + try { + HTMLParagraphElement.prototype.align; + } catch (e) { + return e; + } +}, 'web API-created TypeError'); + +stackTests(() => { + try { + document.createElement(''); + } catch (e) { + return e; + } +}, 'web API-created DOMException'); + +function stackTests(errorFactory, description) { + test(t => { + const error = errorFactory(); + const originalStack = error.stack; + + if (!originalStack) { + return; + } + + const clonedError = structuredClone(error); + assert_equals(clonedError.stack, originalStack); + }, description + ' (structuredClone())'); + + async_test(t => { + const error = errorFactory(); + const originalStack = error.stack; + + if (!originalStack) { + t.done(); + return; + } + + const worker = new Worker('resources/echo-worker.js'); + worker.onmessage = t.step_func_done(e => { + assert_equals(e.data.stack, originalStack); + }); + + worker.postMessage(error); + }, description + ' (worker)'); + + let iframeTest = (t, url) => { + const thisTestId = token(); + + const error = errorFactory(); + const originalStack = error.stack; + + if (!originalStack) { + t.done(); + return; + } + + const iframe = document.createElement('iframe'); + window.addEventListener('message', t.step_func(e => { + if (e.data.testId === thisTestId) { + assert_equals(e.data.error.stack, originalStack); + t.done(); + } + })); + + iframe.onload = t.step_func(() => { + iframe.contentWindow.postMessage({ error, testId: thisTestId }, "*"); + }); + + iframe.src = url; + document.body.append(iframe); + } + + async_test(t => { + const crossSiteURL = new URL('resources/echo-iframe.html', location.href); + crossSiteURL.hostname = '{{hosts[alt][www1]}}'; + iframeTest(t, crossSiteURL); + }, description + ' (cross-site iframe)'); + + async_test(t => { + const sameOriginURL = new URL('resources/echo-iframe.html', location.href); + iframeTest(t, sameOriginURL); + }, description + ' (same-origin iframe)') +} diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.html b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.html new file mode 100644 index 00000000000..319c4b5dc79 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/structuredclone_0.html @@ -0,0 +1,637 @@ + + + + + 2.8 Common DOM interfaces - Structured Clone Algorithm + + + + + +
+ + + + + diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/transfer-errors.window.js b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/transfer-errors.window.js new file mode 100644 index 00000000000..b3ecd86b40a --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/transfer-errors.window.js @@ -0,0 +1,47 @@ +function assert_transfer_error(transferList) { + assert_throws_dom("DataCloneError", () => self.postMessage({ get whatever() { throw new Error("You should not have gotten to this point") } }, "*", transferList)); +} + +test(() => { + [self, self.document, new Image()].forEach(val => { + assert_transfer_error([val]); + }); +}, "Cannot transfer all objects"); + +function transfer_tests(name, create) { + promise_test(async () => { + const transferable = await create(); + assert_transfer_error([transferable, transferable]); + }, `Cannot transfer the same ${name} twice`); + + promise_test(async () => { + const transferable = await create(); + self.postMessage(null, "*", [transferable]); + assert_throws_dom("DataCloneError", () => self.postMessage(null, "*", [transferable])); + }, `Serialize should make the ${name} detached, so it cannot be transferred again`); + + promise_test(async () => { + const transferable = await create(), + customError = new Error("hi"); + self.postMessage(null, "*", [transferable]); + assert_throws_exactly(customError, () => self.postMessage({ get whatever() { throw customError } }, "*", [transferable])); + }, `Serialize should throw before a detached ${name} is found`); + + promise_test(async () => { + const transferable = await create(); + let seen = false; + const message = { + get a() { + self.postMessage(null, '*', [transferable]); + seen = true; + } + }; + assert_throws_dom("DataCloneError", () => self.postMessage(message, "*", [transferable])); + assert_true(seen); + }, `Cannot transfer ${name} detached while the message was serialized`); +} + +transfer_tests("ArrayBuffer", () => new ArrayBuffer(1)); +transfer_tests("MessagePort", () => new MessageChannel().port1); +transfer_tests("ImageBitmap", () => self.createImageBitmap(document.createElement("canvas"))); +transfer_tests("OffscreenCanvas", () => new OffscreenCanvas(1, 1)); diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.html b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.html new file mode 100644 index 00000000000..352d6d0b950 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.html @@ -0,0 +1,11 @@ + + + + + + + + + +
+ diff --git a/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.js b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.js new file mode 100644 index 00000000000..2a46d790b87 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/infrastructure/safe-passing-of-structured-data/window-postmessage.window.js @@ -0,0 +1,16 @@ +// 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(data, transfer) { + return new Promise(resolve => { + window.addEventListener('message', function f(ev) { + window.removeEventListener('message', f); + resolve(ev.data.data); + }); + window.postMessage({data, transfer}, "/", transfer); + }); + } +}); diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js new file mode 100644 index 00000000000..00a86fa74b9 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/structured-clone/structured-clone-battery-of-tests-harness.js @@ -0,0 +1,45 @@ +/** + * Runs a collection of tests that determine if an API implements structured clone + * correctly. + * + * The `runner` parameter has the following properties: + * - `setup()`: An optional function run once before testing starts + * - `teardown()`: An option function run once after all tests are done + * - `preTest()`: An optional, async function run before a test + * - `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. + */ + +function runStructuredCloneBatteryOfTests(runner) { + const defaultRunner = { + setup() {}, + preTest() {}, + postTest() {}, + teardown() {}, + hasDocument: true + }; + runner = Object.assign({}, defaultRunner, runner); + + let setupPromise = runner.setup(); + const allTests = structuredCloneBatteryOfTests.map(test => { + + if (!runner.hasDocument && test.requiresDocument) { + return; + } + + return new Promise(resolve => { + promise_test(async t => { + test = await test; + await setupPromise; + await runner.preTest(test); + await test.f(runner, t) + await runner.postTest(test); + resolve(); + }, test.description); + }).catch(_ => {}); + }); + Promise.all(allTests).then(_ => runner.teardown()); +} diff --git a/Tests/LibWeb/Text/input/wpt-import/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js new file mode 100644 index 00000000000..23cf4f651ac --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/structured-clone/structured-clone-battery-of-tests-with-transferables.js @@ -0,0 +1,169 @@ +structuredCloneBatteryOfTests.push({ + description: 'ArrayBuffer', + async f(runner) { + const buffer = new Uint8Array([1]).buffer; + const copy = await runner.structuredClone(buffer, [buffer]); + assert_equals(buffer.byteLength, 0); + assert_equals(copy.byteLength, 1); + } +}); + +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 + +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/Tests/LibWeb/Text/input/wpt-import/html/webappapis/structured-clone/structured-clone-battery-of-tests.js b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/structured-clone/structured-clone-battery-of-tests.js new file mode 100644 index 00000000000..923ac9dc164 --- /dev/null +++ b/Tests/LibWeb/Text/input/wpt-import/html/webappapis/structured-clone/structured-clone-battery-of-tests.js @@ -0,0 +1,753 @@ +/* This file is mostly a remix of @zcorpan’s web worker test suite */ + +structuredCloneBatteryOfTests = []; + +function check(description, input, callback, requiresDocument = false) { + structuredCloneBatteryOfTests.push({ + description, + async f(runner) { + let newInput = input; + if (typeof input === 'function') { + newInput = input(); + } + const copy = await runner.structuredClone(newInput); + await callback(copy, newInput); + }, + requiresDocument + }); +} + +function compare_primitive(actual, input) { + assert_equals(actual, input); +} +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'); + await callback(actual, input); + } +} + +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); + await callback(actual, input); + } +} + +function enumerate_props(compare_func) { + return async function(actual, input) { + for (const x in input) { + await compare_func(actual[x], input[x]); + } + }; +} + +check('primitive undefined', undefined, compare_primitive); +check('primitive null', null, compare_primitive); +check('primitive true', true, compare_primitive); +check('primitive false', false, compare_primitive); +check('primitive string, empty string', '', compare_primitive); +check('primitive string, lone high surrogate', '\uD800', compare_primitive); +check('primitive string, lone low surrogate', '\uDC00', compare_primitive); +check('primitive string, NUL', '\u0000', compare_primitive); +check('primitive string, astral character', '\uDBFF\uDFFD', compare_primitive); +check('primitive number, 0.2', 0.2, compare_primitive); +check('primitive number, 0', 0, compare_primitive); +check('primitive number, -0', -0, compare_primitive); +check('primitive number, NaN', NaN, compare_primitive); +check('primitive number, Infinity', Infinity, compare_primitive); +check('primitive number, -Infinity', -Infinity, compare_primitive); +check('primitive number, 9007199254740992', 9007199254740992, compare_primitive); +check('primitive number, -9007199254740992', -9007199254740992, compare_primitive); +check('primitive number, 9007199254740994', 9007199254740994, compare_primitive); +check('primitive number, -9007199254740994', -9007199254740994, compare_primitive); +check('primitive BigInt, 0n', 0n, compare_primitive); +check('primitive BigInt, -0n', -0n, compare_primitive); +check('primitive BigInt, -9007199254740994000n', -9007199254740994000n, compare_primitive); +check('primitive BigInt, -9007199254740994000900719925474099400090071992547409940009007199254740994000n', -9007199254740994000900719925474099400090071992547409940009007199254740994000n, compare_primitive); + +check('Array primitives', [undefined, + null, + true, + false, + '', + '\uD800', + '\uDC00', + '\u0000', + '\uDBFF\uDFFD', + 0.2, + 0, + -0, + NaN, + Infinity, + -Infinity, + 9007199254740992, + -9007199254740992, + 9007199254740994, + -9007199254740994, + -12n, + -0n, + 0n], compare_Array(enumerate_props(compare_primitive))); +check('Object primitives', {'undefined':undefined, + 'null':null, + 'true':true, + 'false':false, + 'empty':'', + 'high surrogate':'\uD800', + 'low surrogate':'\uDC00', + 'nul':'\u0000', + 'astral':'\uDBFF\uDFFD', + '0.2':0.2, + '0':0, + '-0':-0, + 'NaN':NaN, + 'Infinity':Infinity, + '-Infinity':-Infinity, + '9007199254740992':9007199254740992, + '-9007199254740992':-9007199254740992, + '9007199254740994':9007199254740994, + '-9007199254740994':-9007199254740994}, compare_Object(enumerate_props(compare_primitive))); + +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); +} +check('Boolean true', new Boolean(true), compare_Boolean); +check('Boolean false', new Boolean(false), compare_Boolean); +check('Array Boolean objects', [new Boolean(true), new Boolean(false)], compare_Array(enumerate_props(compare_Boolean))); +check('Object Boolean objects', {'true':new Boolean(true), 'false':new Boolean(false)}, compare_Object(enumerate_props(compare_Boolean))); + +function compare_obj(what) { + const Type = self[what]; + 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); + }; +} +check('String empty string', new String(''), compare_obj('String')); +check('String lone high surrogate', new String('\uD800'), compare_obj('String')); +check('String lone low surrogate', new String('\uDC00'), compare_obj('String')); +check('String NUL', new String('\u0000'), compare_obj('String')); +check('String astral character', new String('\uDBFF\uDFFD'), compare_obj('String')); +check('Array String objects', [new String(''), + new String('\uD800'), + new String('\uDC00'), + new String('\u0000'), + new String('\uDBFF\uDFFD')], compare_Array(enumerate_props(compare_obj('String')))); +check('Object String objects', {'empty':new String(''), + 'high surrogate':new String('\uD800'), + 'low surrogate':new String('\uDC00'), + 'nul':new String('\u0000'), + 'astral':new String('\uDBFF\uDFFD')}, compare_Object(enumerate_props(compare_obj('String')))); + +check('Number 0.2', new Number(0.2), compare_obj('Number')); +check('Number 0', new Number(0), compare_obj('Number')); +check('Number -0', new Number(-0), compare_obj('Number')); +check('Number NaN', new Number(NaN), compare_obj('Number')); +check('Number Infinity', new Number(Infinity), compare_obj('Number')); +check('Number -Infinity', new Number(-Infinity), compare_obj('Number')); +check('Number 9007199254740992', new Number(9007199254740992), compare_obj('Number')); +check('Number -9007199254740992', new Number(-9007199254740992), compare_obj('Number')); +check('Number 9007199254740994', new Number(9007199254740994), compare_obj('Number')); +check('Number -9007199254740994', new Number(-9007199254740994), compare_obj('Number')); +// BigInt does not have a non-throwing constructor +check('BigInt -9007199254740994n', Object(-9007199254740994n), compare_obj('BigInt')); + +check('Array Number objects', [new Number(0.2), + new Number(0), + new Number(-0), + new Number(NaN), + new Number(Infinity), + new Number(-Infinity), + new Number(9007199254740992), + new Number(-9007199254740992), + new Number(9007199254740994), + new Number(-9007199254740994)], compare_Array(enumerate_props(compare_obj('Number')))); +check('Object Number objects', {'0.2':new Number(0.2), + '0':new Number(0), + '-0':new Number(-0), + 'NaN':new Number(NaN), + 'Infinity':new Number(Infinity), + '-Infinity':new Number(-Infinity), + '9007199254740992':new Number(9007199254740992), + '-9007199254740992':new Number(-9007199254740992), + '9007199254740994':new Number(9007199254740994), + '-9007199254740994':new Number(-9007199254740994)}, compare_Object(enumerate_props(compare_obj('Number')))); + +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); +} +check('Date 0', new Date(0), compare_Date); +check('Date -0', new Date(-0), compare_Date); +check('Date -8.64e15', new Date(-8.64e15), compare_Date); +check('Date 8.64e15', new Date(8.64e15), compare_Date); +check('Array Date objects', [new Date(0), + new Date(-0), + new Date(-8.64e15), + new Date(8.64e15)], compare_Array(enumerate_props(compare_Date))); +check('Object Date objects', {'0':new Date(0), + '-0':new Date(-0), + '-8.64e15':new Date(-8.64e15), + '8.64e15':new Date(8.64e15)}, compare_Object(enumerate_props(compare_Date))); + +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) { + if (typeof actual === 'string') + assert_unreached(actual); + assert_true(actual instanceof RegExp, 'instanceof RegExp'); + assert_equals(actual.global, input.global, 'global'); + assert_equals(actual.ignoreCase, input.ignoreCase, 'ignoreCase'); + assert_equals(actual.multiline, input.multiline, 'multiline'); + assert_equals(actual.source, expected_source, 'source'); + assert_equals(actual.sticky, input.sticky, 'sticky'); + assert_equals(actual.unicode, input.unicode, 'unicode'); + assert_equals(actual.lastIndex, 0, 'lastIndex'); + assert_not_equals(actual, input); + } +} +function func_RegExp_flags_lastIndex() { + const r = /foo/gim; + r.lastIndex = 2; + return r; +} +function func_RegExp_sticky() { + return new RegExp('foo', 'y'); +} +function func_RegExp_unicode() { + return new RegExp('foo', 'u'); +} +check('RegExp flags and lastIndex', func_RegExp_flags_lastIndex, compare_RegExp('foo')); +check('RegExp sticky flag', func_RegExp_sticky, compare_RegExp('foo')); +check('RegExp unicode flag', func_RegExp_unicode, compare_RegExp('foo')); +check('RegExp empty', new RegExp(''), compare_RegExp('(?:)')); +check('RegExp slash', new RegExp('/'), compare_RegExp('\\/')); +check('RegExp new line', new RegExp('\n'), compare_RegExp('\\n')); +check('Array RegExp object, RegExp flags and lastIndex', [func_RegExp_flags_lastIndex()], compare_Array(enumerate_props(compare_RegExp('foo')))); +check('Array RegExp object, RegExp sticky flag', function() { return [func_RegExp_sticky()]; }, compare_Array(enumerate_props(compare_RegExp('foo')))); +check('Array RegExp object, RegExp unicode flag', function() { return [func_RegExp_unicode()]; }, compare_Array(enumerate_props(compare_RegExp('foo')))); +check('Array RegExp object, RegExp empty', [new RegExp('')], compare_Array(enumerate_props(compare_RegExp('(?:)')))); +check('Array RegExp object, RegExp slash', [new RegExp('/')], compare_Array(enumerate_props(compare_RegExp('\\/')))); +check('Array RegExp object, RegExp new line', [new RegExp('\n')], compare_Array(enumerate_props(compare_RegExp('\\n')))); +check('Object RegExp object, RegExp flags and lastIndex', {'x':func_RegExp_flags_lastIndex()}, compare_Object(enumerate_props(compare_RegExp('foo')))); +check('Object RegExp object, RegExp sticky flag', function() { return {'x':func_RegExp_sticky()}; }, compare_Object(enumerate_props(compare_RegExp('foo')))); +check('Object RegExp object, RegExp unicode flag', function() { return {'x':func_RegExp_unicode()}; }, compare_Object(enumerate_props(compare_RegExp('foo')))); +check('Object RegExp object, RegExp empty', {'x':new RegExp('')}, compare_Object(enumerate_props(compare_RegExp('(?:)')))); +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')))); + +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'); + if (!expect_File) + assert_false(actual instanceof File, 'instanceof File'); + assert_equals(actual.size, input.size, 'size'); + assert_equals(actual.type, input.type, 'type'); + assert_not_equals(actual, input); + const ab1 = await new Response(actual).arrayBuffer(); + const ab2 = await new Response(input).arrayBuffer(); + assert_equals(ab1.byteLength, ab2.byteLength, 'byteLength'); + const ta1 = new Uint8Array(ab1); + const ta2 = new Uint8Array(ab2); + for(let i = 0; i < ta1.size; i++) { + assert_equals(ta1[i], ta2[i]); + } +} +function func_Blob_basic() { + return new Blob(['foo'], {type:'text/x-bar'}); +} +check('Blob basic', func_Blob_basic, compare_Blob); + +function b(str) { + return parseInt(str, 2); +} +function encode_cesu8(codeunits) { + // http://www.unicode.org/reports/tr26/ section 2.2 + // only the 3-byte form is supported + const rv = []; + codeunits.forEach(function(codeunit) { + rv.push(b('11100000') + ((codeunit & b('1111000000000000')) >> 12)); + rv.push(b('10000000') + ((codeunit & b('0000111111000000')) >> 6)); + rv.push(b('10000000') + (codeunit & b('0000000000111111'))); + }); + return rv; +} +function func_Blob_bytes(arr) { + return function() { + const buffer = new ArrayBuffer(arr.length); + const view = new DataView(buffer); + for (let i = 0; i < arr.length; ++i) { + view.setUint8(i, arr[i]); + } + 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); + +function func_Blob_empty() { + return new Blob(['']); +} +check('Blob empty', func_Blob_empty , compare_Blob); +function func_Blob_NUL() { + return new Blob(['\u0000']); +} +check('Blob NUL', func_Blob_NUL, compare_Blob); + +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'); + 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) { + if (typeof actual === 'string') + assert_unreached(actual); + assert_true(actual instanceof FileList, 'instanceof FileList'); + assert_equals(actual.length, input.length, 'length'); + assert_not_equals(actual, input); + // XXX when there's a way to populate or construct a FileList, + // check the items in the FileList +} +function func_FileList_empty() { + const input = document.createElement('input'); + input.type = 'file'; + return input.files; +} +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) { + 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+']'); + } + }; +} +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); +} +function func_ImageData_1x1_transparent_black() { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + return ctx.createImageData(1, 1); +} +check('ImageData 1x1 transparent black', func_ImageData_1x1_transparent_black, compare_ImageData, true); +function func_ImageData_1x1_non_transparent_non_black() { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const imagedata = ctx.createImageData(1, 1); + imagedata.data[0] = 100; + imagedata.data[1] = 101; + imagedata.data[2] = 102; + imagedata.data[3] = 103; + return imagedata; +} +check('ImageData 1x1 non-transparent non-black', func_ImageData_1x1_non_transparent_non_black, compare_ImageData, true); +check('Array ImageData object, ImageData 1x1 transparent black', () => ([func_ImageData_1x1_transparent_black()]), compare_Array(enumerate_props(compare_ImageData)), true); +check('Array ImageData object, ImageData 1x1 non-transparent non-black', () => ([func_ImageData_1x1_non_transparent_non_black()]), compare_Array(enumerate_props(compare_ImageData)), true); +check('Object ImageData object, ImageData 1x1 transparent black', () => ({'x':func_ImageData_1x1_transparent_black()}), compare_Object(enumerate_props(compare_ImageData)), true); +check('Object ImageData object, ImageData 1x1 non-transparent non-black', () => ({'x':func_ImageData_1x1_non_transparent_non_black()}), compare_Object(enumerate_props(compare_ImageData)), true); + + +check('Array sparse', new Array(10), compare_Array(enumerate_props(compare_primitive))); +check('Array with non-index property', function() { + const rv = []; + rv.foo = 'bar'; + return rv; +}, compare_Array(enumerate_props(compare_primitive))); +check('Object with index property and length', {'0':'foo', 'length':1}, compare_Object(enumerate_props(compare_primitive))); +function check_circular_property(prop) { + return function(actual) { + assert_equals(actual[prop], actual); + }; +} +check('Array with circular reference', function() { + const rv = []; + rv[0] = rv; + return rv; +}, compare_Array(check_circular_property('0'))); +check('Object with circular reference', function() { + const rv = {}; + rv['x'] = rv; + return rv; +}, compare_Object(check_circular_property('x'))); +function check_identical_property_values(prop1, prop2) { + return function(actual) { + assert_equals(actual[prop1], actual[prop2]); + }; +} +check('Array with identical property values', function() { + const obj = {} + return [obj, obj]; +}, compare_Array(check_identical_property_values('0', '1'))); +check('Object with identical property values', function() { + const obj = {} + return {'x':obj, 'y':obj}; +}, compare_Object(check_identical_property_values('x', 'y'))); + +function check_absent_property(prop) { + return function(actual) { + assert_false(prop in actual); + }; +} +check('Object with property on prototype', function() { + const Foo = function() {}; + Foo.prototype = {'foo':'bar'}; + return new Foo(); +}, compare_Object(check_absent_property('foo'))); + +check('Object with non-enumerable property', function() { + const rv = {}; + Object.defineProperty(rv, 'foo', {value:'bar', enumerable:false, writable:true, configurable:true}); + return rv; +}, compare_Object(check_absent_property('foo'))); + +function check_writable_property(prop) { + return function(actual, input) { + assert_equals(actual[prop], input[prop]); + actual[prop] += ' baz'; + assert_equals(actual[prop], input[prop] + ' baz'); + }; +} +check('Object with non-writable property', function() { + const rv = {}; + Object.defineProperty(rv, 'foo', {value:'bar', enumerable:true, writable:false, configurable:true}); + return rv; +}, compare_Object(check_writable_property('foo'))); + +function check_configurable_property(prop) { + return function(actual, input) { + assert_equals(actual[prop], input[prop]); + delete actual[prop]; + assert_false('prop' in actual); + }; +} +check('Object with non-configurable property', function() { + const rv = {}; + Object.defineProperty(rv, 'foo', {value:'bar', enumerable:true, writable:true, configurable:false}); + 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 */ + +function get_canvas_1x1_transparent_black() { + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + return canvas; +} + +function get_canvas_1x1_non_transparent_non_black() { + const canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + const ctx = canvas.getContext('2d'); + const imagedata = ctx.getImageData(0, 0, 1, 1); + imagedata.data[0] = 100; + imagedata.data[1] = 101; + imagedata.data[2] = 102; + imagedata.data[3] = 103; + return canvas; +} + +function compare_ImageBitmap(actual, input) { + if (typeof actual === 'string') + assert_unreached(actual); + assert_true(actual instanceof ImageBitmap, 'instanceof ImageBitmap'); + assert_not_equals(actual, input); + // XXX paint the ImageBitmap on a canvas and check the data +} + +structuredCloneBatteryOfTests.push({ + description: 'ImageBitmap 1x1 transparent black', + async f(runner) { + const canvas = get_canvas_1x1_transparent_black(); + const bm = await createImageBitmap(canvas); + const copy = await runner.structuredClone(bm); + compare_ImageBitmap(bm, copy); + }, + requiresDocument: true +}); + +structuredCloneBatteryOfTests.push({ + description: 'ImageBitmap 1x1 non-transparent non-black', + async f(runner) { + const canvas = get_canvas_1x1_non_transparent_non_black(); + const bm = await createImageBitmap(canvas); + const copy = await runner.structuredClone(bm); + compare_ImageBitmap(bm, copy); + }, + requiresDocument: true +}); + +structuredCloneBatteryOfTests.push({ + description: 'Array ImageBitmap object, ImageBitmap 1x1 transparent black', + async f(runner) { + const canvas = get_canvas_1x1_transparent_black(); + const bm = [await createImageBitmap(canvas)]; + const copy = await runner.structuredClone(bm); + compare_Array(enumerate_props(compare_ImageBitmap))(bm, copy); + }, + requiresDocument: true +}); + +structuredCloneBatteryOfTests.push({ + description: 'Array ImageBitmap object, ImageBitmap 1x1 transparent non-black', + async f(runner) { + const canvas = get_canvas_1x1_non_transparent_non_black(); + const bm = [await createImageBitmap(canvas)]; + const copy = await runner.structuredClone(bm); + compare_Array(enumerate_props(compare_ImageBitmap))(bm, copy); + }, + requiresDocument: true +}); + +structuredCloneBatteryOfTests.push({ + description: 'Object ImageBitmap object, ImageBitmap 1x1 transparent black', + async f(runner) { + const canvas = get_canvas_1x1_transparent_black(); + const bm = {x: await createImageBitmap(canvas)}; + const copy = await runner.structuredClone(bm); + compare_Object(enumerate_props(compare_ImageBitmap))(bm, copy); + }, + requiresDocument: true +}); + +structuredCloneBatteryOfTests.push({ + description: 'Object ImageBitmap object, ImageBitmap 1x1 transparent non-black', + async f(runner) { + const canvas = get_canvas_1x1_non_transparent_non_black(); + const bm = {x: await createImageBitmap(canvas)}; + const copy = await runner.structuredClone(bm); + compare_Object(enumerate_props(compare_ImageBitmap))(bm, copy); + }, + requiresDocument: true +}); + +check('ObjectPrototype must lose its exotic-ness when cloned', + () => Object.prototype, + (copy, original) => { + assert_not_equals(copy, original); + assert_true(copy instanceof Object); + + const newProto = { some: 'proto' }; + // Must not throw: + Object.setPrototypeOf(copy, newProto); + + 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/Userland/Libraries/LibJS/Runtime/DataView.h b/Userland/Libraries/LibJS/Runtime/DataView.h index 0e2a1ca2fc9..cbf45864256 100644 --- a/Userland/Libraries/LibJS/Runtime/DataView.h +++ b/Userland/Libraries/LibJS/Runtime/DataView.h @@ -24,7 +24,7 @@ public: ArrayBuffer* viewed_array_buffer() const { return m_viewed_array_buffer; } ByteLength const& byte_length() const { return m_byte_length; } - size_t byte_offset() const { return m_byte_offset; } + u32 byte_offset() const { return m_byte_offset; } private: DataView(ArrayBuffer*, ByteLength byte_length, size_t byte_offset, Object& prototype);