summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/streams/transferable
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/streams/transferable
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/streams/transferable')
-rw-r--r--testing/web-platform/tests/streams/transferable/deserialize-error.window.js39
-rw-r--r--testing/web-platform/tests/streams/transferable/gc-crash.html17
-rw-r--r--testing/web-platform/tests/streams/transferable/readable-stream.html260
-rw-r--r--testing/web-platform/tests/streams/transferable/reason.html132
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/create-wasm-module.js11
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/deserialize-error-frame.html39
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/echo-iframe.html7
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/echo-worker.js2
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/helpers.js132
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/receiving-shared-worker.js11
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/receiving-worker.js7
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/sending-shared-worker.js12
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/sending-worker.js5
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/service-worker-iframe.html39
-rw-r--r--testing/web-platform/tests/streams/transferable/resources/service-worker.js30
-rw-r--r--testing/web-platform/tests/streams/transferable/service-worker.https.html28
-rw-r--r--testing/web-platform/tests/streams/transferable/shared-worker.html25
-rw-r--r--testing/web-platform/tests/streams/transferable/transfer-with-messageport.window.js219
-rw-r--r--testing/web-platform/tests/streams/transferable/transform-stream-members.any.js18
-rw-r--r--testing/web-platform/tests/streams/transferable/transform-stream.html108
-rw-r--r--testing/web-platform/tests/streams/transferable/window.html55
-rw-r--r--testing/web-platform/tests/streams/transferable/worker.html76
-rw-r--r--testing/web-platform/tests/streams/transferable/writable-stream.html146
23 files changed, 1418 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..05914e12cc
--- /dev/null
+++ b/testing/web-platform/tests/streams/transferable/transform-stream-members.any.js
@@ -0,0 +1,18 @@
+// META: global=window,dedicatedworker,shadowrealm
+
+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>