diff options
Diffstat (limited to 'testing/web-platform/tests/streams/transferable')
23 files changed, 1416 insertions, 0 deletions
diff --git a/testing/web-platform/tests/streams/transferable/deserialize-error.window.js b/testing/web-platform/tests/streams/transferable/deserialize-error.window.js new file mode 100644 index 0000000000..64cf2bbfb1 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/deserialize-error.window.js @@ -0,0 +1,39 @@ +// META: script=/common/get-host-info.sub.js +// META: script=resources/create-wasm-module.js +// META: timeout=long + +const { HTTPS_NOTSAMESITE_ORIGIN } = get_host_info(); +const iframe = document.createElement('iframe'); +iframe.src = `${HTTPS_NOTSAMESITE_ORIGIN}/streams/transferable/resources/deserialize-error-frame.html`; + +window.addEventListener('message', async evt => { + // Tests are serialized to make the results deterministic. + switch (evt.data) { + case 'init done': { + const ws = new WritableStream(); + iframe.contentWindow.postMessage(ws, '*', [ws]); + return; + } + + case 'ws done': { + const module = await createWasmModule(); + const rs = new ReadableStream({ + start(controller) { + controller.enqueue(module); + } + }); + iframe.contentWindow.postMessage(rs, '*', [rs]); + return; + } + + case 'rs done': { + iframe.remove(); + } + } +}); + +// Need to do this after adding the listener to ensure we catch the first +// message. +document.body.appendChild(iframe); + +fetch_tests_from_window(iframe.contentWindow); diff --git a/testing/web-platform/tests/streams/transferable/gc-crash.html b/testing/web-platform/tests/streams/transferable/gc-crash.html new file mode 100644 index 0000000000..0d331e6be0 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/gc-crash.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html class="test-wait"> +<script src="/common/gc.js"></script> +<script type="module"> + const b = new ReadableStream({ + start(c) { + c.enqueue({}) // the value we will transfer + }, + }) + const transferred = structuredClone(b, { transfer: [b] }) + // Here we request a read, triggering a message transfer + transferred.getReader().read() + // And immediately trigger GC without waiting for the read, + // causing the actual transfer to be done after GC + await garbageCollect() + document.documentElement.classList.remove("test-wait") +</script> diff --git a/testing/web-platform/tests/streams/transferable/readable-stream.html b/testing/web-platform/tests/streams/transferable/readable-stream.html new file mode 100644 index 0000000000..b1ede4695b --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/readable-stream.html @@ -0,0 +1,260 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> +<script src="../resources/recording-streams.js"></script> +<script src="../resources/test-utils.js"></script> +<script> +'use strict'; + +promise_test(async () => { + const rs = await createTransferredReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.close(); + } + }); + const reader = rs.getReader(); + { + const {value, done} = await reader.read(); + assert_false(done, 'should not be done yet'); + assert_equals(value, 'a', 'first chunk should be a'); + } + { + const {done} = await reader.read(); + assert_true(done, 'should be done now'); + } +}, 'sending one chunk through a transferred stream should work'); + +promise_test(async () => { + let controller; + const rs = await createTransferredReadableStream({ + start(c) { + controller = c; + } + }); + for (let i = 0; i < 10; ++i) { + controller.enqueue(i); + } + controller.close(); + const reader = rs.getReader(); + for (let i = 0; i < 10; ++i) { + const {value, done} = await reader.read(); + assert_false(done, 'should not be done yet'); + assert_equals(value, i, 'chunk content should match index'); + } + const {done} = await reader.read(); + assert_true(done, 'should be done now'); +}, 'sending ten chunks through a transferred stream should work'); + +promise_test(async () => { + let controller; + const rs = await createTransferredReadableStream({ + start(c) { + controller = c; + } + }); + const reader = rs.getReader(); + for (let i = 0; i < 10; ++i) { + controller.enqueue(i); + const {value, done} = await reader.read(); + assert_false(done, 'should not be done yet'); + assert_equals(value, i, 'chunk content should match index'); + } + controller.close(); + const {done} = await reader.read(); + assert_true(done, 'should be done now'); +}, 'sending ten chunks one at a time should work'); + +promise_test(async () => { + let controller; + const rs = await createTransferredReadableStream({ + start() { + this.counter = 0; + }, + pull(controller) { + controller.enqueue(this.counter); + ++this.counter; + if (this.counter === 10) + controller.close(); + } + }); + const reader = rs.getReader(); + for (let i = 0; i < 10; ++i) { + const {value, done} = await reader.read(); + assert_false(done, 'should not be done yet'); + assert_equals(value, i, 'chunk content should match index'); + } + const {done} = await reader.read(); + assert_true(done, 'should be done now'); +}, 'sending ten chunks on demand should work'); + +promise_test(async () => { + const rs = recordingReadableStream({}, { highWaterMark: 0 }); + await delay(0); + assert_array_equals(rs.events, [], 'pull() should not have been called'); + // Eat the message so it can't interfere with other tests. + addEventListener('message', () => {}, {once: true}); + // The transfer is done manually to verify that it is posting the stream that + // relieves backpressure, not receiving it. + postMessage(rs, '*', [rs]); + await delay(0); + assert_array_equals(rs.events, ['pull'], 'pull() should have been called'); +}, 'transferring a stream should relieve backpressure'); + +promise_test(async () => { + const rs = await recordingTransferredReadableStream({ + pull(controller) { + controller.enqueue('a'); + } + }, { highWaterMark: 2 }); + await delay(0); + assert_array_equals(rs.events, ['pull', 'pull', 'pull'], + 'pull() should have been called three times'); +}, 'transferring a stream should add one chunk to the queue size'); + +promise_test(async () => { + const rs = await recordingTransferredReadableStream({ + start(controller) { + controller.enqueue(new Uint8Array(1024)); + controller.enqueue(new Uint8Array(1024)); + } + }, new ByteLengthQueuingStrategy({highWaterMark: 512})); + await delay(0); + // At this point the queue contains 1024/512 bytes and 1/1 chunk, so it's full + // and pull() is not called. + assert_array_equals(rs.events, [], 'pull() should not have been called'); + const reader = rs.getReader(); + const {value, done} = await reader.read(); + assert_false(done, 'we should not be done'); + assert_equals(value.byteLength, 1024, 'expected chunk should be returned'); + // Now the queue contains 0/512 bytes and 1/1 chunk, so pull() is called. If + // the implementation erroneously counted the extra queue space in bytes, then + // the queue would contain 1024/513 bytes and pull() wouldn't be called. + assert_array_equals(rs.events, ['pull'], 'pull() should have been called'); +}, 'the extra queue from transferring is counted in chunks'); + +async function transferredReadableStreamWithCancelPromise() { + let resolveCancelCalled; + const cancelCalled = new Promise(resolve => { + resolveCancelCalled = resolve; + }); + const rs = await recordingTransferredReadableStream({ + cancel() { + resolveCancelCalled(); + } + }); + return { rs, cancelCalled }; +} + +promise_test(async () => { + const { rs, cancelCalled } = await transferredReadableStreamWithCancelPromise(); + rs.cancel('message'); + await cancelCalled; + assert_array_equals(rs.events, ['pull', 'cancel', 'message'], + 'cancel() should have been called'); + const reader = rs.getReader(); + // Check the stream really got closed. + await reader.closed; +}, 'cancel should be propagated to the original'); + +promise_test(async () => { + const { rs, cancelCalled } = await transferredReadableStreamWithCancelPromise(); + const reader = rs.getReader(); + const readPromise = reader.read(); + reader.cancel('done'); + const { done } = await readPromise; + assert_true(done, 'should be done'); + await cancelCalled; + assert_array_equals(rs.events, ['pull', 'cancel', 'done'], + 'events should match'); +}, 'cancel should abort a pending read()'); + +promise_test(async () => { + let cancelComplete = false; + const rs = await createTransferredReadableStream({ + async cancel() { + await flushAsyncEvents(); + cancelComplete = true; + } + }); + await rs.cancel(); + assert_false(cancelComplete, + 'cancel() on the underlying sink should not have completed'); +}, 'stream cancel should not wait for underlying source cancel'); + +promise_test(async t => { + const rs = await recordingTransferredReadableStream(); + const reader = rs.getReader(); + let serializationHappened = false; + rs.controller.enqueue({ + get getter() { + serializationHappened = true; + return 'a'; + } + }); + await flushAsyncEvents(); + assert_false(serializationHappened, + 'serialization should not have happened yet'); + const {value, done} = await reader.read(); + assert_false(done, 'should not be done'); + assert_equals(value.getter, 'a', 'getter should be a'); + assert_true(serializationHappened, + 'serialization should have happened'); +}, 'serialization should not happen until the value is read'); + +promise_test(async t => { + const rs = await recordingTransferredReadableStream(); + const reader = rs.getReader(); + rs.controller.enqueue(new ReadableStream()); + await promise_rejects_dom(t, 'DataCloneError', reader.read(), + 'closed promise should reject'); + assert_throws_js(TypeError, () => rs.controller.enqueue(), + 'original stream should be errored'); +}, 'transferring a non-serializable chunk should error both sides'); + +promise_test(async t => { + const rs = await createTransferredReadableStream({ + start(controller) { + controller.error('foo'); + } + }); + const reader = rs.getReader(); + return promise_rejects_exactly(t, 'foo', reader.read(), + 'error should be passed through'); +}, 'errors should be passed through'); + +promise_test(async () => { + const rs = await recordingTransferredReadableStream(); + await delay(0); + const reader = rs.getReader(); + reader.cancel(); + rs.controller.error(); + const {done} = await reader.read(); + assert_true(done, 'should be done'); + assert_throws_js(TypeError, () => rs.controller.enqueue(), + 'enqueue should throw'); +}, 'race between cancel() and error() should leave sides in different states'); + +promise_test(async () => { + const rs = await recordingTransferredReadableStream(); + await delay(0); + const reader = rs.getReader(); + reader.cancel(); + rs.controller.close(); + const {done} = await reader.read(); + assert_true(done, 'should be done'); +}, 'race between cancel() and close() should be benign'); + +promise_test(async () => { + const rs = await recordingTransferredReadableStream(); + await delay(0); + const reader = rs.getReader(); + reader.cancel(); + rs.controller.enqueue('a'); + const {done} = await reader.read(); + assert_true(done, 'should be done'); +}, 'race between cancel() and enqueue() should be benign'); + +</script> diff --git a/testing/web-platform/tests/streams/transferable/reason.html b/testing/web-platform/tests/streams/transferable/reason.html new file mode 100644 index 0000000000..4251aa85b8 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/reason.html @@ -0,0 +1,132 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> +<script> +'use strict'; + +// Chrome used to special-case the reason for cancel() and abort() in order to +// handle exceptions correctly. This is no longer necessary. These tests are +// retained to avoid regressions. + +async function getTransferredReason(originalReason) { + let resolvePromise; + const rv = new Promise(resolve => { + resolvePromise = resolve; + }); + const rs = await createTransferredReadableStream({ + cancel(reason) { + resolvePromise(reason); + } + }); + await rs.cancel(originalReason); + return rv; +} + +for (const value of ['hi', '\t\r\n', 7, 3.0, undefined, null, true, false, + NaN, Infinity]) { + promise_test(async () => { + const reason = await getTransferredReason(value); + assert_equals(reason, value, 'reason should match'); + }, `reason with a simple value of '${value}' should be preserved`); +} + +for (const badType of [Symbol('hi'), _ => 'hi']) { + promise_test(async t => { + return promise_rejects_dom(t, 'DataCloneError', + getTransferredReason(badType), + 'cancel() should reject'); + }, `reason with a type of '${typeof badType}' should be rejected and ` + + `error the stream`); +} + +promise_test(async () => { + const reasonAsJson = + `{"foo":[1,"col"],"bar":{"hoge":0.2,"baz":{},"shan":null}}`; + const reason = await getTransferredReason(JSON.parse(reasonAsJson)); + assert_equals(JSON.stringify(reason), reasonAsJson, + 'object should be preserved'); +}, 'objects that can be completely expressed in JSON should be preserved'); + +promise_test(async () => { + const circularObject = {}; + circularObject.self = circularObject; + const reason = await getTransferredReason(circularObject); + assert_true(reason instanceof Object, 'an Object should be output'); + assert_equals(reason.self, reason, + 'the object should have a circular reference'); +}, 'objects that cannot be expressed in JSON should also be preserved'); + +promise_test(async () => { + const originalReason = new TypeError('hi'); + const reason = await getTransferredReason(originalReason); + assert_true(reason instanceof TypeError, + 'type should be preserved'); + assert_equals(reason.message, originalReason.message, + 'message should be preserved'); +}, 'the type and message of a TypeError should be preserved'); + +promise_test(async () => { + const originalReason = new TypeError('hi'); + originalReason.foo = 'bar'; + const reason = await getTransferredReason(originalReason); + assert_false('foo' in reason, + 'foo should not be preserved'); +}, 'other attributes of a TypeError should not be preserved'); + +promise_test(async () => { + const originalReason = new TypeError(); + originalReason.message = [1, 2, 3]; + const reason = await getTransferredReason(originalReason); + assert_equals(reason.message, '1,2,3', 'message should be stringified'); +}, 'a TypeError message should be converted to a string'); + +promise_test(async () => { + const originalReason = new TypeError(); + Object.defineProperty(originalReason, 'message', { + get() { return 'words'; } + }); + const reason = await getTransferredReason(originalReason); + assert_equals(reason.message, '', 'message should not be preserved'); +}, 'a TypeError message should not be preserved if it is a getter'); + +promise_test(async () => { + const originalReason = new TypeError(); + delete originalReason.message; + TypeError.prototype.message = 'inherited message'; + const reason = await getTransferredReason(originalReason); + delete TypeError.prototype.message; + assert_equals(reason.message, '', 'message should not be preserved'); +}, 'a TypeError message should not be preserved if it is inherited'); + +promise_test(async () => { + const originalReason = new DOMException('yes', 'AbortError'); + const reason = await getTransferredReason(originalReason); + assert_true(reason instanceof DOMException, + 'reason should be a DOMException'); + assert_equals(reason.message, originalReason.message, + 'the messages should match'); + assert_equals(reason.name, originalReason.name, + 'the names should match'); +}, 'DOMException errors should be preserved'); + +for (const errorConstructor of [EvalError, RangeError, + ReferenceError, SyntaxError, TypeError, + URIError]) { + promise_test(async () => { + const originalReason = new errorConstructor('nope'); + const reason = await getTransferredReason(originalReason); + assert_equals(typeof reason, 'object', 'reason should have type object'); + assert_true(reason instanceof errorConstructor, + `reason should inherit ${errorConstructor.name}`); + assert_true(reason instanceof Error, 'reason should inherit Error'); + assert_equals(reason.constructor, errorConstructor, + 'reason should have the right constructor'); + assert_equals(reason.name, errorConstructor.name, + `name should match constructor name`); + assert_equals(reason.message, 'nope', 'message should match'); + }, `${errorConstructor.name} should be preserved`); +} + +</script> diff --git a/testing/web-platform/tests/streams/transferable/resources/create-wasm-module.js b/testing/web-platform/tests/streams/transferable/resources/create-wasm-module.js new file mode 100644 index 0000000000..37064af95c --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/create-wasm-module.js @@ -0,0 +1,11 @@ +// There aren't many cloneable types that will cause an error on +// deserialization. WASM modules have the property that it's an error to +// deserialize them cross-site, which works for our purposes. +async function createWasmModule() { + // It doesn't matter what the module is, so we use one from another + // test. + const response = + await fetch("/wasm/serialization/module/resources/incrementer.wasm"); + const ab = await response.arrayBuffer(); + return WebAssembly.compile(ab); +} diff --git a/testing/web-platform/tests/streams/transferable/resources/deserialize-error-frame.html b/testing/web-platform/tests/streams/transferable/resources/deserialize-error-frame.html new file mode 100644 index 0000000000..5ec2fcda2c --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/deserialize-error-frame.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="create-wasm-module.js"></script> +<script> +async_test(t => { + parent.postMessage('init done', '*'); + window.addEventListener('message', async evt => { + if (evt.data.constructor.name !== 'WritableStream') { + return; + } + const ws = evt.data; + const writer = ws.getWriter(); + const module = await createWasmModule(); + writer.write(module); + await promise_rejects_dom(t, 'DataCloneError', writer.closed, + 'should reject with a DataCloneError'); + t.done(); + // Signal that this test is done. When both tests are done the iframe will + // be removed. + parent.postMessage('ws done', '*'); + }); +}, 'a WritableStream deserialization failure should result in a DataCloneError'); + +async_test(t => { + window.addEventListener('message', async evt => { + if (evt.data.constructor.name !== 'ReadableStream') { + return; + } + const rs = evt.data; + const reader = rs.getReader(); + await promise_rejects_dom(t, 'DataCloneError', reader.read(), + 'should reject with a DataCloneError'); + t.done(); + // Signal that this test is done. When both tests are done the iframe will + // be removed. + parent.postMessage('rs done', '*'); + }); +}, 'a ReadableStream deserialization failure should result in a DataCloneError'); +</script> diff --git a/testing/web-platform/tests/streams/transferable/resources/echo-iframe.html b/testing/web-platform/tests/streams/transferable/resources/echo-iframe.html new file mode 100644 index 0000000000..68f6850343 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/echo-iframe.html @@ -0,0 +1,7 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script> +addEventListener('message', evt => { + evt.source.postMessage(evt.data, '*', [evt.data]); +}); +</script> diff --git a/testing/web-platform/tests/streams/transferable/resources/echo-worker.js b/testing/web-platform/tests/streams/transferable/resources/echo-worker.js new file mode 100644 index 0000000000..806c237108 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/echo-worker.js @@ -0,0 +1,2 @@ +// A worker that just transfers back any message that is sent to it. +onmessage = evt => postMessage(evt.data, [evt.data]); diff --git a/testing/web-platform/tests/streams/transferable/resources/helpers.js b/testing/web-platform/tests/streams/transferable/resources/helpers.js new file mode 100644 index 0000000000..12504537f9 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/helpers.js @@ -0,0 +1,132 @@ +'use strict'; + +(() => { + // Create a ReadableStream that will pass the tests in + // testTransferredReadableStream(), below. + function createOriginalReadableStream() { + return new ReadableStream({ + start(controller) { + controller.enqueue('a'); + controller.close(); + } + }); + } + + // Common tests to roughly determine that |rs| is a correctly transferred + // version of a stream created by createOriginalReadableStream(). + function testTransferredReadableStream(rs) { + assert_equals(rs.constructor, ReadableStream, + 'rs should be a ReadableStream in this realm'); + assert_true(rs instanceof ReadableStream, + 'instanceof check should pass'); + + // Perform a brand-check on |rs| in the process of calling getReader(). + const reader = ReadableStream.prototype.getReader.call(rs); + + return reader.read().then(({value, done}) => { + assert_false(done, 'done should be false'); + assert_equals(value, 'a', 'value should be "a"'); + return reader.read(); + }).then(({done}) => { + assert_true(done, 'done should be true'); + }); + } + + function testMessage(msg) { + assert_array_equals(msg.ports, [], 'there should be no ports in the event'); + return testTransferredReadableStream(msg.data); + } + + function testMessageEvent(target) { + return new Promise((resolve, reject) => { + target.addEventListener('message', ev => { + try { + resolve(testMessage(ev)); + } catch (e) { + reject(e); + } + }, {once: true}); + }); + } + + function testMessageEventOrErrorMessage(target) { + return new Promise((resolve, reject) => { + target.addEventListener('message', ev => { + if (typeof ev.data === 'string') { + // Assume it's an error message and reject with it. + reject(ev.data); + return; + } + + try { + resolve(testMessage(ev)); + } catch (e) { + reject(e); + } + }, {once: true}); + }); + } + + function checkTestResults(target) { + return new Promise((resolve, reject) => { + target.onmessage = msg => { + // testharness.js sends us objects which we need to ignore. + if (typeof msg.data !== 'string') + return; + + if (msg.data === 'OK') { + resolve(); + } else { + reject(msg.data); + } + }; + }); + } + + // These tests assume that a transferred ReadableStream will behave the same + // regardless of how it was transferred. This enables us to simply transfer the + // stream to ourselves. + function createTransferredReadableStream(underlyingSource) { + const original = new ReadableStream(underlyingSource); + const promise = new Promise((resolve, reject) => { + addEventListener('message', msg => { + const rs = msg.data; + if (rs instanceof ReadableStream) { + resolve(rs); + } else { + reject(new Error(`what is this thing: "${rs}"?`)); + } + }, {once: true}); + }); + postMessage(original, '*', [original]); + return promise; + } + + function recordingTransferredReadableStream(underlyingSource, strategy) { + const original = recordingReadableStream(underlyingSource, strategy); + const promise = new Promise((resolve, reject) => { + addEventListener('message', msg => { + const rs = msg.data; + if (rs instanceof ReadableStream) { + rs.events = original.events; + rs.eventsWithoutPulls = original.eventsWithoutPulls; + rs.controller = original.controller; + resolve(rs); + } else { + reject(new Error(`what is this thing: "${rs}"?`)); + } + }, {once: true}); + }); + postMessage(original, '*', [original]); + return promise; + } + + self.createOriginalReadableStream = createOriginalReadableStream; + self.testMessage = testMessage; + self.testMessageEvent = testMessageEvent; + self.testMessageEventOrErrorMessage = testMessageEventOrErrorMessage; + self.checkTestResults = checkTestResults; + self.createTransferredReadableStream = createTransferredReadableStream; + self.recordingTransferredReadableStream = recordingTransferredReadableStream; + +})(); diff --git a/testing/web-platform/tests/streams/transferable/resources/receiving-shared-worker.js b/testing/web-platform/tests/streams/transferable/resources/receiving-shared-worker.js new file mode 100644 index 0000000000..84f779c3db --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/receiving-shared-worker.js @@ -0,0 +1,11 @@ +'use strict'; +importScripts('/resources/testharness.js', 'helpers.js'); + +onconnect = evt => { + const port = evt.source; + const promise = testMessageEvent(port); + port.start(); + promise + .then(() => port.postMessage('OK')) + .catch(err => port.postMessage(`BAD: ${err}`)); +}; diff --git a/testing/web-platform/tests/streams/transferable/resources/receiving-worker.js b/testing/web-platform/tests/streams/transferable/resources/receiving-worker.js new file mode 100644 index 0000000000..4ebb9c5f8f --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/receiving-worker.js @@ -0,0 +1,7 @@ +'use strict'; +importScripts('/resources/testharness.js', 'helpers.js'); + +const promise = testMessageEvent(self); +promise + .then(() => postMessage('OK')) + .catch(err => postMessage(`BAD: ${err}`)); diff --git a/testing/web-platform/tests/streams/transferable/resources/sending-shared-worker.js b/testing/web-platform/tests/streams/transferable/resources/sending-shared-worker.js new file mode 100644 index 0000000000..e579077894 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/sending-shared-worker.js @@ -0,0 +1,12 @@ +'use strict'; +importScripts('helpers.js'); + +onconnect = msg => { + const port = msg.source; + const orig = createOriginalReadableStream(); + try { + port.postMessage(orig, [orig]); + } catch (e) { + port.postMessage(e.message); + } +}; diff --git a/testing/web-platform/tests/streams/transferable/resources/sending-worker.js b/testing/web-platform/tests/streams/transferable/resources/sending-worker.js new file mode 100644 index 0000000000..0b79733f74 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/sending-worker.js @@ -0,0 +1,5 @@ +'use strict'; +importScripts('helpers.js'); + +const orig = createOriginalReadableStream(); +postMessage(orig, [orig]); diff --git a/testing/web-platform/tests/streams/transferable/resources/service-worker-iframe.html b/testing/web-platform/tests/streams/transferable/resources/service-worker-iframe.html new file mode 100644 index 0000000000..348d067c92 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/service-worker-iframe.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="helpers.js"></script> +<script> +'use strict'; + +setup({ + explicit_done: true +}); + +function startTests() { + promise_test(() => { + const orig = createOriginalReadableStream(); + const promise = checkTestResults(navigator.serviceWorker); + navigator.serviceWorker.controller.postMessage(orig, [orig]); + assert_true(orig.locked, 'the original stream should be locked'); + return promise; + }, 'serviceWorker.controller.postMessage should be able to transfer a ' + + 'ReadableStream'); + + promise_test(() => { + const promise = testMessageEventOrErrorMessage(navigator.serviceWorker); + navigator.serviceWorker.controller.postMessage('SEND'); + return promise; + }, 'postMessage in a service worker should be able to transfer ReadableStream'); + + done(); +} + +// Delay running the tests until we get a message from the page telling us to. +// This is to work around an issue where testharness.js doesn't detect +// completion of the tests if they fail too early. +onmessage = msg => { + if (msg.data === 'explicit trigger') + startTests(); +}; + +</script> diff --git a/testing/web-platform/tests/streams/transferable/resources/service-worker.js b/testing/web-platform/tests/streams/transferable/resources/service-worker.js new file mode 100644 index 0000000000..af76b6c11b --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/resources/service-worker.js @@ -0,0 +1,30 @@ +'use strict'; +importScripts('/resources/testharness.js', 'helpers.js'); + +onmessage = msg => { + const client = msg.source; + if (msg.data === 'SEND') { + sendingTest(client); + } else { + receivingTest(msg, client); + } +}; + +function sendingTest(client) { + const orig = createOriginalReadableStream(); + try { + client.postMessage(orig, [orig]); + } catch (e) { + client.postMessage(e.message); + } +} + +function receivingTest(msg, client) { + try { + msg.waitUntil(testMessage(msg) + .then(() => client.postMessage('OK')) + .catch(e => client.postMessage(`BAD: ${e}`))); + } catch (e) { + client.postMessage(`BAD: ${e}`); + } +} diff --git a/testing/web-platform/tests/streams/transferable/service-worker.https.html b/testing/web-platform/tests/streams/transferable/service-worker.https.html new file mode 100644 index 0000000000..2ca7f19c91 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/service-worker.https.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script> +<script> +'use strict'; + +const kServiceWorkerUrl = 'resources/service-worker.js'; +const kIframeUrl = 'resources/service-worker-iframe.html'; + +// A dummy test so that we can use the test-helpers.sub.js functions +const test = async_test('service-worker'); + +async function registerAndStart() { + const reg = await service_worker_unregister_and_register( + test, kServiceWorkerUrl, kIframeUrl); + await wait_for_state(test, reg.installing, 'activated'); + const iframe = await with_iframe(kIframeUrl); + fetch_tests_from_window(iframe.contentWindow); + add_completion_callback(() => iframe.remove()); + iframe.contentWindow.postMessage('explicit trigger', '*'); + return service_worker_unregister_and_done(test, kIframeUrl); +} + +onload = registerAndStart; + +</script> diff --git a/testing/web-platform/tests/streams/transferable/shared-worker.html b/testing/web-platform/tests/streams/transferable/shared-worker.html new file mode 100644 index 0000000000..cd0415402d --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/shared-worker.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> +<script> +'use strict'; + +promise_test(t => { + const orig = createOriginalReadableStream(); + const w = new SharedWorker('resources/receiving-shared-worker.js'); + const promise = checkTestResults(w.port); + w.port.postMessage(orig, [orig]); + assert_true(orig.locked, 'the original stream should be locked'); + return promise; +}, 'worker.postMessage should be able to transfer a ReadableStream'); + +promise_test(t => { + const w = new SharedWorker('resources/sending-shared-worker.js'); + const promise = testMessageEventOrErrorMessage(w.port); + w.port.start(); + return promise; +}, 'postMessage in a worker should be able to transfer a ReadableStream'); + +</script> diff --git a/testing/web-platform/tests/streams/transferable/transfer-with-messageport.window.js b/testing/web-platform/tests/streams/transferable/transfer-with-messageport.window.js new file mode 100644 index 0000000000..37f8c9df16 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/transfer-with-messageport.window.js @@ -0,0 +1,219 @@ +"use strict"; + +function receiveEventOnce(target, name) { + return new Promise(resolve => { + target.addEventListener( + name, + ev => { + resolve(ev); + }, + { once: true } + ); + }); +} + +async function postAndTestMessageEvent(data, transfer, title) { + postMessage(data, "*", transfer); + const messagePortCount = transfer.filter(i => i instanceof MessagePort) + .length; + const ev = await receiveEventOnce(window, "message"); + assert_equals( + ev.ports.length, + messagePortCount, + `Correct number of ports ${title}` + ); + for (const [i, port] of ev.ports.entries()) { + assert_true( + port instanceof MessagePort, + `ports[${i}] include MessagePort ${title}` + ); + } + for (const [key, value] of Object.entries(data)) { + assert_true( + ev.data[key] instanceof value.constructor, + `data.${key} has correct interface ${value.constructor.name} ${title}` + ); + } +} + +async function transferMessagePortWithOrder1(stream) { + const channel = new MessageChannel(); + await postAndTestMessageEvent( + { stream, port2: channel.port2 }, + [stream, channel.port2], + `when transferring [${stream.constructor.name}, MessagePort]` + ); +} + +async function transferMessagePortWithOrder2(stream) { + const channel = new MessageChannel(); + await postAndTestMessageEvent( + { stream, port2: channel.port2 }, + [channel.port2, stream], + `when transferring [MessagePort, ${stream.constructor.name}]` + ); +} + +async function transferMessagePortWithOrder3(stream) { + const channel = new MessageChannel(); + await postAndTestMessageEvent( + { port1: channel.port1, stream, port2: channel.port2 }, + [channel.port1, stream, channel.port2], + `when transferring [MessagePort, ${stream.constructor.name}, MessagePort]` + ); +} + +async function transferMessagePortWithOrder4(stream) { + const channel = new MessageChannel(); + await postAndTestMessageEvent( + {}, + [channel.port1, stream, channel.port2], + `when transferring [MessagePort, ${stream.constructor.name}, MessagePort] but with empty data` + ); +} + +async function transferMessagePortWithOrder5(stream) { + const channel = new MessageChannel(); + await postAndTestMessageEvent( + { port2: channel.port2, port1: channel.port1, stream }, + [channel.port1, stream, channel.port2], + `when transferring [MessagePort, ${stream.constructor.name}, MessagePort] but with data having different order` + ); +} + +async function transferMessagePortWithOrder6(stream) { + const channel = new MessageChannel(); + await postAndTestMessageEvent( + { port2: channel.port2, port1: channel.port1 }, + [channel.port1, stream, channel.port2], + `when transferring [MessagePort, ${stream.constructor.name}, MessagePort] but with stream not being in the data` + ); +} + +async function transferMessagePortWithOrder7(stream) { + const channel = new MessageChannel(); + await postAndTestMessageEvent( + { stream }, + [channel.port1, stream, channel.port2], + `when transferring [MessagePort, ${stream.constructor.name}, MessagePort] but with ports not being in the data` + ); +} + +async function transferMessagePortWith(constructor) { + await transferMessagePortWithOrder1(new constructor()); + await transferMessagePortWithOrder2(new constructor()); + await transferMessagePortWithOrder3(new constructor()); +} + +async function advancedTransferMesagePortWith(constructor) { + await transferMessagePortWithOrder4(new constructor()); + await transferMessagePortWithOrder5(new constructor()); + await transferMessagePortWithOrder6(new constructor()); + await transferMessagePortWithOrder7(new constructor()); +} + +async function mixedTransferMessagePortWithOrder1() { + const channel = new MessageChannel(); + const readable = new ReadableStream(); + const writable = new WritableStream(); + const transform = new TransformStream(); + await postAndTestMessageEvent( + { + readable, + writable, + transform, + port1: channel.port1, + port2: channel.port2, + }, + [readable, writable, transform, channel.port1, channel.port2], + `when transferring [ReadableStream, WritableStream, TransformStream, MessagePort, MessagePort]` + ); +} + +async function mixedTransferMessagePortWithOrder2() { + const channel = new MessageChannel(); + const readable = new ReadableStream(); + const writable = new WritableStream(); + const transform = new TransformStream(); + await postAndTestMessageEvent( + { readable, writable, transform }, + [transform, channel.port1, readable, channel.port2, writable], + `when transferring [TransformStream, MessagePort, ReadableStream, MessagePort, WritableStream]` + ); +} + +async function mixedTransferMessagePortWithOrder3() { + const channel = new MessageChannel(); + const readable1 = new ReadableStream(); + const readable2 = new ReadableStream(); + const writable1 = new WritableStream(); + const writable2 = new WritableStream(); + const transform1 = new TransformStream(); + const transform2 = new TransformStream(); + await postAndTestMessageEvent( + { readable1, writable1, transform1, readable2, writable2, transform2 }, + [ + transform2, + channel.port1, + readable1, + channel.port2, + writable2, + readable2, + writable1, + transform1, + ], + `when transferring [TransformStream, MessagePort, ReadableStream, MessagePort, WritableStream, ReadableStream, WritableStream, TransformStream] but with the data having different order` + ); +} + +async function mixedTransferMesagePortWith() { + await mixedTransferMessagePortWithOrder1(); + await mixedTransferMessagePortWithOrder2(); + await mixedTransferMessagePortWithOrder3(); +} + +promise_test(async t => { + await transferMessagePortWith(ReadableStream); +}, "Transferring a MessagePort with a ReadableStream should set `.ports`"); + +promise_test(async t => { + await transferMessagePortWith(WritableStream); +}, "Transferring a MessagePort with a WritableStream should set `.ports`"); + +promise_test(async t => { + await transferMessagePortWith(TransformStream); +}, "Transferring a MessagePort with a TransformStream should set `.ports`"); + +promise_test(async t => { + await transferMessagePortWith(ReadableStream); +}, "Transferring a MessagePort with a ReadableStream should set `.ports`, advanced"); + +promise_test(async t => { + await transferMessagePortWith(WritableStream); +}, "Transferring a MessagePort with a WritableStream should set `.ports`, advanced"); + +promise_test(async t => { + await transferMessagePortWith(TransformStream); +}, "Transferring a MessagePort with a TransformStream should set `.ports`, advanced"); + +promise_test(async t => { + await mixedTransferMesagePortWith(); +}, "Transferring a MessagePort with multiple streams should set `.ports`"); + +test(() => { + assert_throws_dom("DataCloneError", () => + postMessage({ stream: new ReadableStream() }, "*") + ); +}, "ReadableStream must not be serializable"); + +test(() => { + assert_throws_dom("DataCloneError", () => + postMessage({ stream: new WritableStream() }, "*") + ); +}, "WritableStream must not be serializable"); + +test(() => { + assert_throws_dom("DataCloneError", () => + postMessage({ stream: new TransformStream() }, "*") + ); +}, "TransformStream must not be serializable"); diff --git a/testing/web-platform/tests/streams/transferable/transform-stream-members.any.js b/testing/web-platform/tests/streams/transferable/transform-stream-members.any.js new file mode 100644 index 0000000000..fca060b0c0 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/transform-stream-members.any.js @@ -0,0 +1,16 @@ +const combinations = [ + (t => [t, t.readable])(new TransformStream()), + (t => [t.readable, t])(new TransformStream()), + (t => [t, t.writable])(new TransformStream()), + (t => [t.writable, t])(new TransformStream()), +]; + +for (const combination of combinations) { + test(() => { + assert_throws_dom( + "DataCloneError", + () => structuredClone(combination, { transfer: combination }), + "structuredClone should throw" + ); + }, `Transferring ${combination} should fail`); +} diff --git a/testing/web-platform/tests/streams/transferable/transform-stream.html b/testing/web-platform/tests/streams/transferable/transform-stream.html new file mode 100644 index 0000000000..355d5d8074 --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/transform-stream.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="../resources/test-utils.js"></script> +<script> +'use strict'; + +promise_test(t => { + const orig = new TransformStream(); + const promise = new Promise(resolve => { + addEventListener('message', t.step_func(evt => { + const transferred = evt.data; + assert_equals(transferred.constructor, TransformStream, + 'transferred should be a TransformStream in this realm'); + assert_true(transferred instanceof TransformStream, + 'instanceof check should pass'); + + // Perform a brand-check on |transferred|. + const readableGetter = Object.getOwnPropertyDescriptor( + TransformStream.prototype, 'readable').get; + assert_true(readableGetter.call(transferred) instanceof ReadableStream, + 'brand check should pass and readable stream should result'); + const writableGetter = Object.getOwnPropertyDescriptor( + TransformStream.prototype, 'writable').get; + assert_true(writableGetter.call(transferred) instanceof WritableStream, + 'brand check should pass and writable stream should result'); + resolve(); + }), {once: true}); + }); + postMessage(orig, '*', [orig]); + assert_true(orig.readable.locked, 'the readable side should be locked'); + assert_true(orig.writable.locked, 'the writable side should be locked'); + return promise; +}, 'window.postMessage should be able to transfer a TransformStream'); + +test(() => { + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + assert_throws_dom('DataCloneError', () => postMessage(ts, '*', [ts]), + 'postMessage should throw'); + assert_false(ts.readable.locked, 'readable side should not get locked'); +}, 'a TransformStream with a locked writable should not be transferable'); + +test(() => { + const ts = new TransformStream(); + const reader = ts.readable.getReader(); + assert_throws_dom('DataCloneError', () => postMessage(ts, '*', [ts]), + 'postMessage should throw'); + assert_false(ts.writable.locked, 'writable side should not get locked'); +}, 'a TransformStream with a locked readable should not be transferable'); + +test(() => { + const ts = new TransformStream(); + const reader = ts.readable.getReader(); + const writer = ts.writable.getWriter(); + assert_throws_dom('DataCloneError', () => postMessage(ts, '*', [ts]), + 'postMessage should throw'); +}, 'a TransformStream with both sides locked should not be transferable'); + +promise_test(t => { + const source = new ReadableStream({ + start(controller) { + controller.enqueue('hello '); + controller.enqueue('there '); + controller.close(); + } + }); + let resolve; + const ready = new Promise(r => resolve = r); + let result = ''; + const sink = new WritableStream({ + write(chunk) { + if (result) { + resolve(); + } + result += chunk; + } + }); + const transform1 = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk.toUpperCase()); + } + }); + const transform2 = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk + chunk); + } + }); + const promise = new Promise(resolve => { + addEventListener('message', t.step_func(evt => { + const data = evt.data; + resolve(data.source + .pipeThrough(data.transform1) + .pipeThrough(data.transform2) + .pipeTo(data.sink)); + })); + }); + postMessage({source, sink, transform1, transform2}, '*', + [source, transform1, sink, transform2]); + return ready + .then(() => { + assert_equals(result, 'HELLO HELLO THERE THERE ', + 'transforms should have been applied'); + }); +}, 'piping through transferred transforms should work'); + +</script> diff --git a/testing/web-platform/tests/streams/transferable/window.html b/testing/web-platform/tests/streams/transferable/window.html new file mode 100644 index 0000000000..11c868356b --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/window.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> +<script> +'use strict'; + +promise_test(t => { + const orig = createOriginalReadableStream(); + const promise = testMessageEvent(window); + postMessage(orig, '*', [orig]); + assert_true(orig.locked, 'the original stream should be locked'); + return promise; +}, 'window.postMessage should be able to transfer a ReadableStream'); + +promise_test(t => { + const orig = createOriginalReadableStream(); + const mc = new MessageChannel(); + const promise = testMessageEvent(mc.port1); + mc.port1.start(); + + mc.port2.postMessage(orig, [orig]); + mc.port2.close(); + assert_true(orig.locked, 'the original stream should be locked'); + return promise; +}, 'port.postMessage should be able to transfer a ReadableStream'); + +promise_test(t => { + const orig = createOriginalReadableStream(); + const promise = new Promise(resolve => { + addEventListener('message', t.step_func(evt => { + const [rs1, rs2] = evt.data; + assert_equals(rs1, rs2, 'both ReadableStreams should be the same object'); + resolve(); + }), {once: true}); + }); + postMessage([orig, orig], '*', [orig]); + return promise; +}, 'the same ReadableStream posted multiple times should arrive together'); + +const onloadPromise = new Promise(resolve => onload = resolve); + +promise_test(() => { + const orig = createOriginalReadableStream(); + const promise = testMessageEvent(window); + return onloadPromise.then(() => { + const echoIframe = document.querySelector('#echo'); + echoIframe.contentWindow.postMessage(orig, '*', [orig]); + return promise; + }); +}, 'transfer to and from an iframe should work'); +</script> + +<iframe id=echo src="resources/echo-iframe.html" style="display:none"></iframe> diff --git a/testing/web-platform/tests/streams/transferable/worker.html b/testing/web-platform/tests/streams/transferable/worker.html new file mode 100644 index 0000000000..c5dc9fc62f --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/worker.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> +<script src="../resources/test-utils.js"></script> +<script> +'use strict'; + +promise_test(t => { + const orig = createOriginalReadableStream(); + const w = new Worker('resources/receiving-worker.js'); + t.add_cleanup(() => { + w.terminate(); + }); + const promise = new Promise((resolve, reject) => { + checkTestResults(w).then(resolve, reject); + w.onerror = () => reject('error in worker'); + }); + w.postMessage(orig, [orig]); + assert_true(orig.locked, 'the original stream should be locked'); + return promise; +}, 'worker.postMessage should be able to transfer a ReadableStream'); + +promise_test(t => { + const w = new Worker('resources/sending-worker.js'); + t.add_cleanup(() => { + w.terminate(); + }); + return new Promise((resolve, reject) => { + testMessageEvent(w).then(resolve, reject); + w.onerror = () => reject('error in worker'); + }); +}, 'postMessage in a worker should be able to transfer a ReadableStream'); + +promise_test(async t => { + const w = new Worker('resources/echo-worker.js'); + let controller; + const orig = new ReadableStream({ + start(c) { + controller = c; + } + }); + const targetStream = await new Promise((resolve, reject) => { + w.onmessage = evt => resolve(evt.data); + w.onerror = () => reject('error in worker'); + w.postMessage(orig, [orig]); + }); + const reader = targetStream.getReader(); + const reads = []; + // Place a lot of chunks "in transit". This should increase the likelihood + // that they is a chunk at each relevant step when the worker is terminated. + for (let i = 0; i < 50; ++i) { + await delay(0); + controller.enqueue(i); + const expected = i; + reads.push(reader.read().then(({value, done}) => { + assert_false(done, 'we should not be done'); + assert_equals(value, expected, 'value should match expectation'); + })); + } + w.terminate(); + for (let i = 50; i < 60; ++i) { + controller.enqueue(i); + reads.push( + reader.read().then(t.unreached_func('read() should not resolve'))); + await delay(0); + } + // We don't expect every read() to complete, but we want to give them a chance + // to reject if they're going to. + return Promise.race([ + Promise.all(reads), + flushAsyncEvents() + ]); +}, 'terminating a worker should not error the stream'); +</script> diff --git a/testing/web-platform/tests/streams/transferable/writable-stream.html b/testing/web-platform/tests/streams/transferable/writable-stream.html new file mode 100644 index 0000000000..7e25dad94d --- /dev/null +++ b/testing/web-platform/tests/streams/transferable/writable-stream.html @@ -0,0 +1,146 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="resources/helpers.js"></script> +<script src="../resources/test-utils.js"></script> +<script src="../resources/recording-streams.js"></script> +<script> +'use strict'; + +promise_test(t => { + const orig = new WritableStream(); + const promise = new Promise(resolve => { + addEventListener('message', t.step_func(evt => { + const transferred = evt.data; + assert_equals(transferred.constructor, WritableStream, + 'transferred should be a WritableStream in this realm'); + assert_true(transferred instanceof WritableStream, + 'instanceof check should pass'); + + // Perform a brand-check on |transferred|. + const writer = WritableStream.prototype.getWriter.call(transferred); + resolve(); + }), {once: true}); + }); + postMessage(orig, '*', [orig]); + assert_true(orig.locked, 'the original stream should be locked'); + return promise; +}, 'window.postMessage should be able to transfer a WritableStream'); + +test(() => { + const ws = new WritableStream(); + const writer = ws.getWriter(); + assert_throws_dom('DataCloneError', () => postMessage(ws, '*', [ws]), + 'postMessage should throw'); +}, 'a locked WritableStream should not be transferable'); + +promise_test(t => { + const {writable, readable} = new TransformStream(); + const promise = new Promise(resolve => { + addEventListener('message', t.step_func(async evt => { + const {writable, readable} = evt.data; + const reader = readable.getReader(); + const writer = writable.getWriter(); + const writerPromises = Promise.all([ + writer.write('hi'), + writer.close(), + ]); + const {value, done} = await reader.read(); + assert_false(done, 'we should not be done'); + assert_equals(value, 'hi', 'chunk should have been delivered'); + const readResult = await reader.read(); + assert_true(readResult.done, 'readable should be closed'); + await writerPromises; + resolve(); + }), {once: true}); + }); + postMessage({writable, readable}, '*', [writable, readable]); + return promise; +}, 'window.postMessage should be able to transfer a {readable, writable} pair'); + +function transfer(stream) { + return new Promise(resolve => { + addEventListener('message', evt => resolve(evt.data), { once: true }); + postMessage(stream, '*', [stream]); + }); +} + +promise_test(async () => { + const orig = new WritableStream( + {}, new ByteLengthQueuingStrategy({ highWaterMark: 65536 })); + const transferred = await transfer(orig); + const writer = transferred.getWriter(); + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); +}, 'desiredSize for a newly-transferred stream should be 1'); + +promise_test(async () => { + const orig = new WritableStream({ + write() { + return new Promise(() => {}); + } + }); + const transferred = await transfer(orig); + const writer = transferred.getWriter(); + await writer.write('a'); + assert_equals(writer.desiredSize, 1, 'desiredSize should be 1'); +}, 'effective queue size of a transferred writable should be 2'); + +promise_test(async () => { + const [writeCalled, resolveWriteCalled] = makePromiseAndResolveFunc(); + let resolveWrite; + const orig = new WritableStream({ + write() { + resolveWriteCalled(); + return new Promise(resolve => { + resolveWrite = resolve; + }); + } + }); + const transferred = await transfer(orig); + const writer = transferred.getWriter(); + await writer.write('a'); + let writeDone = false; + const writePromise = writer.write('b').then(() => { + writeDone = true; + }); + await writeCalled; + assert_false(writeDone, 'second write should not have resolved yet'); + resolveWrite(); + await writePromise; // (makes sure this resolves) +}, 'second write should wait for first underlying write to complete'); + +async function transferredWritableStreamWithAbortPromise() { + const [abortCalled, resolveAbortCalled] = makePromiseAndResolveFunc(); + const orig = recordingWritableStream({ + abort() { + resolveAbortCalled(); + } + }); + const transferred = await transfer(orig); + return { orig, transferred, abortCalled }; +} + +promise_test(async t => { + const { orig, transferred, abortCalled } = await transferredWritableStreamWithAbortPromise(); + transferred.abort('p'); + await abortCalled; + assert_array_equals(orig.events, ['abort', 'p'], + 'abort() should have been called'); +}, 'abort() should work'); + +promise_test(async t => { + const { orig, transferred, abortCalled } = await transferredWritableStreamWithAbortPromise(); + const writer = transferred.getWriter(); + // A WritableStream object cannot be cloned. + await promise_rejects_dom(t, 'DataCloneError', writer.write(new WritableStream()), + 'the write should reject'); + await promise_rejects_dom(t, 'DataCloneError', writer.closed, + 'the stream should be errored'); + await abortCalled; + assert_equals(orig.events.length, 2, 'abort should have been called'); + assert_equals(orig.events[0], 'abort', 'first event should be abort'); + assert_equals(orig.events[1].name, 'DataCloneError', + 'reason should be a DataCloneError'); +}, 'writing a unclonable object should error the stream'); +</script> |