summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/streams/readable-streams
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /testing/web-platform/tests/streams/readable-streams
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/streams/readable-streams')
-rw-r--r--testing/web-platform/tests/streams/readable-streams/async-iterator.any.js650
-rw-r--r--testing/web-platform/tests/streams/readable-streams/bad-strategies.any.js159
-rw-r--r--testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.any.js400
-rw-r--r--testing/web-platform/tests/streams/readable-streams/cancel.any.js236
-rw-r--r--testing/web-platform/tests/streams/readable-streams/constructor.any.js17
-rw-r--r--testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.any.js208
-rw-r--r--testing/web-platform/tests/streams/readable-streams/crashtests/empty.js0
-rw-r--r--testing/web-platform/tests/streams/readable-streams/crashtests/strategy-worker-terminate.html10
-rw-r--r--testing/web-platform/tests/streams/readable-streams/crashtests/strategy-worker.js4
-rw-r--r--testing/web-platform/tests/streams/readable-streams/cross-realm-crash.window.js13
-rw-r--r--testing/web-platform/tests/streams/readable-streams/default-reader.any.js539
-rw-r--r--testing/web-platform/tests/streams/readable-streams/floating-point-total-queue-size.any.js116
-rw-r--r--testing/web-platform/tests/streams/readable-streams/garbage-collection.any.js71
-rw-r--r--testing/web-platform/tests/streams/readable-streams/general.any.js840
-rw-r--r--testing/web-platform/tests/streams/readable-streams/global.html162
-rw-r--r--testing/web-platform/tests/streams/readable-streams/owning-type-message-port.any.js49
-rw-r--r--testing/web-platform/tests/streams/readable-streams/owning-type-video-frame.any.js128
-rw-r--r--testing/web-platform/tests/streams/readable-streams/owning-type.any.js91
-rw-r--r--testing/web-platform/tests/streams/readable-streams/patched-global.any.js142
-rw-r--r--testing/web-platform/tests/streams/readable-streams/read-task-handling.window.js46
-rw-r--r--testing/web-platform/tests/streams/readable-streams/reentrant-strategies.any.js264
-rw-r--r--testing/web-platform/tests/streams/readable-streams/tee.any.js479
-rw-r--r--testing/web-platform/tests/streams/readable-streams/templated.any.js143
23 files changed, 4767 insertions, 0 deletions
diff --git a/testing/web-platform/tests/streams/readable-streams/async-iterator.any.js b/testing/web-platform/tests/streams/readable-streams/async-iterator.any.js
new file mode 100644
index 0000000000..3ccaca17bc
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/async-iterator.any.js
@@ -0,0 +1,650 @@
+// META: global=window,worker
+// META: script=../resources/rs-utils.js
+// META: script=../resources/test-utils.js
+// META: script=../resources/recording-streams.js
+'use strict';
+
+const error1 = new Error('error1');
+
+function assert_iter_result(iterResult, value, done, message) {
+ const prefix = message === undefined ? '' : `${message} `;
+ assert_equals(typeof iterResult, 'object', `${prefix}type is object`);
+ assert_equals(Object.getPrototypeOf(iterResult), Object.prototype, `${prefix}[[Prototype]]`);
+ assert_array_equals(Object.getOwnPropertyNames(iterResult).sort(), ['done', 'value'], `${prefix}property names`);
+ assert_equals(iterResult.value, value, `${prefix}value`);
+ assert_equals(iterResult.done, done, `${prefix}done`);
+}
+
+test(() => {
+ const s = new ReadableStream();
+ const it = s.values();
+ const proto = Object.getPrototypeOf(it);
+
+ const AsyncIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(async function* () {}).prototype);
+ assert_equals(Object.getPrototypeOf(proto), AsyncIteratorPrototype, 'prototype should extend AsyncIteratorPrototype');
+
+ const methods = ['next', 'return'].sort();
+ assert_array_equals(Object.getOwnPropertyNames(proto).sort(), methods, 'should have all the correct methods');
+
+ for (const m of methods) {
+ const propDesc = Object.getOwnPropertyDescriptor(proto, m);
+ assert_true(propDesc.enumerable, 'method should be enumerable');
+ assert_true(propDesc.configurable, 'method should be configurable');
+ assert_true(propDesc.writable, 'method should be writable');
+ assert_equals(typeof it[m], 'function', 'method should be a function');
+ assert_equals(it[m].name, m, 'method should have the correct name');
+ }
+
+ assert_equals(it.next.length, 0, 'next should have no parameters');
+ assert_equals(it.return.length, 1, 'return should have 1 parameter');
+ assert_equals(typeof it.throw, 'undefined', 'throw should not exist');
+}, 'Async iterator instances should have the correct list of properties');
+
+promise_test(async () => {
+ const s = new ReadableStream({
+ start(c) {
+ c.enqueue(1);
+ c.enqueue(2);
+ c.enqueue(3);
+ c.close();
+ }
+ });
+
+ const chunks = [];
+ for await (const chunk of s) {
+ chunks.push(chunk);
+ }
+ assert_array_equals(chunks, [1, 2, 3]);
+}, 'Async-iterating a push source');
+
+promise_test(async () => {
+ let i = 1;
+ const s = new ReadableStream({
+ pull(c) {
+ c.enqueue(i);
+ if (i >= 3) {
+ c.close();
+ }
+ i += 1;
+ }
+ });
+
+ const chunks = [];
+ for await (const chunk of s) {
+ chunks.push(chunk);
+ }
+ assert_array_equals(chunks, [1, 2, 3]);
+}, 'Async-iterating a pull source');
+
+promise_test(async () => {
+ const s = new ReadableStream({
+ start(c) {
+ c.enqueue(undefined);
+ c.enqueue(undefined);
+ c.enqueue(undefined);
+ c.close();
+ }
+ });
+
+ const chunks = [];
+ for await (const chunk of s) {
+ chunks.push(chunk);
+ }
+ assert_array_equals(chunks, [undefined, undefined, undefined]);
+}, 'Async-iterating a push source with undefined values');
+
+promise_test(async () => {
+ let i = 1;
+ const s = new ReadableStream({
+ pull(c) {
+ c.enqueue(undefined);
+ if (i >= 3) {
+ c.close();
+ }
+ i += 1;
+ }
+ });
+
+ const chunks = [];
+ for await (const chunk of s) {
+ chunks.push(chunk);
+ }
+ assert_array_equals(chunks, [undefined, undefined, undefined]);
+}, 'Async-iterating a pull source with undefined values');
+
+promise_test(async () => {
+ let i = 1;
+ const s = recordingReadableStream({
+ pull(c) {
+ c.enqueue(i);
+ if (i >= 3) {
+ c.close();
+ }
+ i += 1;
+ },
+ }, new CountQueuingStrategy({ highWaterMark: 0 }));
+
+ const it = s.values();
+ assert_array_equals(s.events, []);
+
+ const read1 = await it.next();
+ assert_iter_result(read1, 1, false);
+ assert_array_equals(s.events, ['pull']);
+
+ const read2 = await it.next();
+ assert_iter_result(read2, 2, false);
+ assert_array_equals(s.events, ['pull', 'pull']);
+
+ const read3 = await it.next();
+ assert_iter_result(read3, 3, false);
+ assert_array_equals(s.events, ['pull', 'pull', 'pull']);
+
+ const read4 = await it.next();
+ assert_iter_result(read4, undefined, true);
+ assert_array_equals(s.events, ['pull', 'pull', 'pull']);
+}, 'Async-iterating a pull source manually');
+
+promise_test(async () => {
+ const s = new ReadableStream({
+ start(c) {
+ c.error('e');
+ },
+ });
+
+ try {
+ for await (const chunk of s) {}
+ assert_unreached();
+ } catch (e) {
+ assert_equals(e, 'e');
+ }
+}, 'Async-iterating an errored stream throws');
+
+promise_test(async () => {
+ const s = new ReadableStream({
+ start(c) {
+ c.close();
+ }
+ });
+
+ for await (const chunk of s) {
+ assert_unreached();
+ }
+}, 'Async-iterating a closed stream never executes the loop body, but works fine');
+
+promise_test(async () => {
+ const s = new ReadableStream();
+
+ const loop = async () => {
+ for await (const chunk of s) {
+ assert_unreached();
+ }
+ assert_unreached();
+ };
+
+ await Promise.race([
+ loop(),
+ flushAsyncEvents()
+ ]);
+}, 'Async-iterating an empty but not closed/errored stream never executes the loop body and stalls the async function');
+
+promise_test(async () => {
+ const s = new ReadableStream({
+ start(c) {
+ c.enqueue(1);
+ c.enqueue(2);
+ c.enqueue(3);
+ c.close();
+ },
+ });
+
+ const reader = s.getReader();
+ const readResult = await reader.read();
+ assert_iter_result(readResult, 1, false);
+ reader.releaseLock();
+
+ const chunks = [];
+ for await (const chunk of s) {
+ chunks.push(chunk);
+ }
+ assert_array_equals(chunks, [2, 3]);
+}, 'Async-iterating a partially consumed stream');
+
+for (const type of ['throw', 'break', 'return']) {
+ for (const preventCancel of [false, true]) {
+ promise_test(async () => {
+ const s = recordingReadableStream({
+ start(c) {
+ c.enqueue(0);
+ }
+ });
+
+ // use a separate function for the loop body so return does not stop the test
+ const loop = async () => {
+ for await (const c of s.values({ preventCancel })) {
+ if (type === 'throw') {
+ throw new Error();
+ } else if (type === 'break') {
+ break;
+ } else if (type === 'return') {
+ return;
+ }
+ }
+ };
+
+ try {
+ await loop();
+ } catch (e) {}
+
+ if (preventCancel) {
+ assert_array_equals(s.events, ['pull'], `cancel() should not be called`);
+ } else {
+ assert_array_equals(s.events, ['pull', 'cancel', undefined], `cancel() should be called`);
+ }
+ }, `Cancellation behavior when ${type}ing inside loop body; preventCancel = ${preventCancel}`);
+ }
+}
+
+for (const preventCancel of [false, true]) {
+ promise_test(async () => {
+ const s = recordingReadableStream({
+ start(c) {
+ c.enqueue(0);
+ }
+ });
+
+ const it = s.values({ preventCancel });
+ await it.return();
+
+ if (preventCancel) {
+ assert_array_equals(s.events, [], `cancel() should not be called`);
+ } else {
+ assert_array_equals(s.events, ['cancel', undefined], `cancel() should be called`);
+ }
+ }, `Cancellation behavior when manually calling return(); preventCancel = ${preventCancel}`);
+}
+
+promise_test(async t => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ if (timesPulled === 0) {
+ c.enqueue(0);
+ ++timesPulled;
+ } else {
+ c.error(error1);
+ }
+ }
+ });
+
+ const it = s[Symbol.asyncIterator]();
+
+ const iterResult1 = await it.next();
+ assert_iter_result(iterResult1, 0, false, '1st next()');
+
+ await promise_rejects_exactly(t, error1, it.next(), '2nd next()');
+}, 'next() rejects if the stream errors');
+
+promise_test(async () => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ if (timesPulled === 0) {
+ c.enqueue(0);
+ ++timesPulled;
+ } else {
+ c.error(error1);
+ }
+ }
+ });
+
+ const it = s[Symbol.asyncIterator]();
+
+ const iterResult = await it.return('return value');
+ assert_iter_result(iterResult, 'return value', true);
+}, 'return() does not rejects if the stream has not errored yet');
+
+promise_test(async t => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ // Do not error in start() because doing so would prevent acquiring a reader/async iterator.
+ c.error(error1);
+ }
+ });
+
+ const it = s[Symbol.asyncIterator]();
+
+ await flushAsyncEvents();
+ await promise_rejects_exactly(t, error1, it.return('return value'));
+}, 'return() rejects if the stream has errored');
+
+promise_test(async t => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ if (timesPulled === 0) {
+ c.enqueue(0);
+ ++timesPulled;
+ } else {
+ c.error(error1);
+ }
+ }
+ });
+
+ const it = s[Symbol.asyncIterator]();
+
+ const iterResult1 = await it.next();
+ assert_iter_result(iterResult1, 0, false, '1st next()');
+
+ await promise_rejects_exactly(t, error1, it.next(), '2nd next()');
+
+ const iterResult3 = await it.next();
+ assert_iter_result(iterResult3, undefined, true, '3rd next()');
+}, 'next() that succeeds; next() that reports an error; next()');
+
+promise_test(async () => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ if (timesPulled === 0) {
+ c.enqueue(0);
+ ++timesPulled;
+ } else {
+ c.error(error1);
+ }
+ }
+ });
+
+ const it = s[Symbol.asyncIterator]();
+
+ const iterResults = await Promise.allSettled([it.next(), it.next(), it.next()]);
+
+ assert_equals(iterResults[0].status, 'fulfilled', '1st next() promise status');
+ assert_iter_result(iterResults[0].value, 0, false, '1st next()');
+
+ assert_equals(iterResults[1].status, 'rejected', '2nd next() promise status');
+ assert_equals(iterResults[1].reason, error1, '2nd next() rejection reason');
+
+ assert_equals(iterResults[2].status, 'fulfilled', '3rd next() promise status');
+ assert_iter_result(iterResults[2].value, undefined, true, '3rd next()');
+}, 'next() that succeeds; next() that reports an error(); next() [no awaiting]');
+
+promise_test(async t => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ if (timesPulled === 0) {
+ c.enqueue(0);
+ ++timesPulled;
+ } else {
+ c.error(error1);
+ }
+ }
+ });
+
+ const it = s[Symbol.asyncIterator]();
+
+ const iterResult1 = await it.next();
+ assert_iter_result(iterResult1, 0, false, '1st next()');
+
+ await promise_rejects_exactly(t, error1, it.next(), '2nd next()');
+
+ const iterResult3 = await it.return('return value');
+ assert_iter_result(iterResult3, 'return value', true, 'return()');
+}, 'next() that succeeds; next() that reports an error(); return()');
+
+promise_test(async () => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ if (timesPulled === 0) {
+ c.enqueue(0);
+ ++timesPulled;
+ } else {
+ c.error(error1);
+ }
+ }
+ });
+
+ const it = s[Symbol.asyncIterator]();
+
+ const iterResults = await Promise.allSettled([it.next(), it.next(), it.return('return value')]);
+
+ assert_equals(iterResults[0].status, 'fulfilled', '1st next() promise status');
+ assert_iter_result(iterResults[0].value, 0, false, '1st next()');
+
+ assert_equals(iterResults[1].status, 'rejected', '2nd next() promise status');
+ assert_equals(iterResults[1].reason, error1, '2nd next() rejection reason');
+
+ assert_equals(iterResults[2].status, 'fulfilled', 'return() promise status');
+ assert_iter_result(iterResults[2].value, 'return value', true, 'return()');
+}, 'next() that succeeds; next() that reports an error(); return() [no awaiting]');
+
+promise_test(async () => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ c.enqueue(timesPulled);
+ ++timesPulled;
+ }
+ });
+ const it = s[Symbol.asyncIterator]();
+
+ const iterResult1 = await it.next();
+ assert_iter_result(iterResult1, 0, false, 'next()');
+
+ const iterResult2 = await it.return('return value');
+ assert_iter_result(iterResult2, 'return value', true, 'return()');
+
+ assert_equals(timesPulled, 2);
+}, 'next() that succeeds; return()');
+
+promise_test(async () => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ c.enqueue(timesPulled);
+ ++timesPulled;
+ }
+ });
+ const it = s[Symbol.asyncIterator]();
+
+ const iterResults = await Promise.allSettled([it.next(), it.return('return value')]);
+
+ assert_equals(iterResults[0].status, 'fulfilled', 'next() promise status');
+ assert_iter_result(iterResults[0].value, 0, false, 'next()');
+
+ assert_equals(iterResults[1].status, 'fulfilled', 'return() promise status');
+ assert_iter_result(iterResults[1].value, 'return value', true, 'return()');
+
+ assert_equals(timesPulled, 2);
+}, 'next() that succeeds; return() [no awaiting]');
+
+promise_test(async () => {
+ const rs = new ReadableStream();
+ const it = rs.values();
+
+ const iterResult1 = await it.return('return value');
+ assert_iter_result(iterResult1, 'return value', true, 'return()');
+
+ const iterResult2 = await it.next();
+ assert_iter_result(iterResult2, undefined, true, 'next()');
+}, 'return(); next()');
+
+promise_test(async () => {
+ const rs = new ReadableStream();
+ const it = rs.values();
+
+ const iterResults = await Promise.allSettled([it.return('return value'), it.next()]);
+
+ assert_equals(iterResults[0].status, 'fulfilled', 'return() promise status');
+ assert_iter_result(iterResults[0].value, 'return value', true, 'return()');
+
+ assert_equals(iterResults[1].status, 'fulfilled', 'next() promise status');
+ assert_iter_result(iterResults[1].value, undefined, true, 'next()');
+}, 'return(); next() [no awaiting]');
+
+promise_test(async () => {
+ const rs = new ReadableStream();
+ const it = rs.values();
+
+ const iterResult1 = await it.return('return value 1');
+ assert_iter_result(iterResult1, 'return value 1', true, '1st return()');
+
+ const iterResult2 = await it.return('return value 2');
+ assert_iter_result(iterResult2, 'return value 2', true, '1st return()');
+}, 'return(); return()');
+
+promise_test(async () => {
+ const rs = new ReadableStream();
+ const it = rs.values();
+
+ const iterResults = await Promise.allSettled([it.return('return value 1'), it.return('return value 2')]);
+
+ assert_equals(iterResults[0].status, 'fulfilled', '1st return() promise status');
+ assert_iter_result(iterResults[0].value, 'return value 1', true, '1st return()');
+
+ assert_equals(iterResults[1].status, 'fulfilled', '2nd return() promise status');
+ assert_iter_result(iterResults[1].value, 'return value 2', true, '1st return()');
+}, 'return(); return() [no awaiting]');
+
+test(() => {
+ const s = new ReadableStream({
+ start(c) {
+ c.enqueue(0);
+ c.close();
+ },
+ });
+ s.values();
+ assert_throws_js(TypeError, () => s.values(), 'values() should throw');
+}, 'values() throws if there\'s already a lock');
+
+promise_test(async () => {
+ const s = new ReadableStream({
+ start(c) {
+ c.enqueue(1);
+ c.enqueue(2);
+ c.enqueue(3);
+ c.close();
+ }
+ });
+
+ const chunks = [];
+ for await (const chunk of s) {
+ chunks.push(chunk);
+ }
+ assert_array_equals(chunks, [1, 2, 3]);
+
+ const reader = s.getReader();
+ await reader.closed;
+}, 'Acquiring a reader after exhaustively async-iterating a stream');
+
+promise_test(async t => {
+ let timesPulled = 0;
+ const s = new ReadableStream({
+ pull(c) {
+ if (timesPulled === 0) {
+ c.enqueue(0);
+ ++timesPulled;
+ } else {
+ c.error(error1);
+ }
+ }
+ });
+
+ const it = s[Symbol.asyncIterator]({ preventCancel: true });
+
+ const iterResult1 = await it.next();
+ assert_iter_result(iterResult1, 0, false, '1st next()');
+
+ await promise_rejects_exactly(t, error1, it.next(), '2nd next()');
+
+ const iterResult2 = await it.return('return value');
+ assert_iter_result(iterResult2, 'return value', true, 'return()');
+
+ // i.e. it should not reject with a generic "this stream is locked" TypeError.
+ const reader = s.getReader();
+ await promise_rejects_exactly(t, error1, reader.closed, 'closed on the new reader should reject with the error');
+}, 'Acquiring a reader after return()ing from a stream that errors');
+
+promise_test(async () => {
+ const s = new ReadableStream({
+ start(c) {
+ c.enqueue(1);
+ c.enqueue(2);
+ c.enqueue(3);
+ c.close();
+ },
+ });
+
+ // read the first two chunks, then cancel
+ const chunks = [];
+ for await (const chunk of s) {
+ chunks.push(chunk);
+ if (chunk >= 2) {
+ break;
+ }
+ }
+ assert_array_equals(chunks, [1, 2]);
+
+ const reader = s.getReader();
+ await reader.closed;
+}, 'Acquiring a reader after partially async-iterating a stream');
+
+promise_test(async () => {
+ const s = new ReadableStream({
+ start(c) {
+ c.enqueue(1);
+ c.enqueue(2);
+ c.enqueue(3);
+ c.close();
+ },
+ });
+
+ // read the first two chunks, then release lock
+ const chunks = [];
+ for await (const chunk of s.values({preventCancel: true})) {
+ chunks.push(chunk);
+ if (chunk >= 2) {
+ break;
+ }
+ }
+ assert_array_equals(chunks, [1, 2]);
+
+ const reader = s.getReader();
+ const readResult = await reader.read();
+ assert_iter_result(readResult, 3, false);
+ await reader.closed;
+}, 'Acquiring a reader and reading the remaining chunks after partially async-iterating a stream with preventCancel = true');
+
+for (const preventCancel of [false, true]) {
+ test(() => {
+ const rs = new ReadableStream();
+ rs.values({ preventCancel }).return();
+ // The test passes if this line doesn't throw.
+ rs.getReader();
+ }, `return() should unlock the stream synchronously when preventCancel = ${preventCancel}`);
+}
+
+promise_test(async () => {
+ const rs = new ReadableStream({
+ async start(c) {
+ c.enqueue('a');
+ c.enqueue('b');
+ c.enqueue('c');
+ await flushAsyncEvents();
+ // At this point, the async iterator has a read request in the stream's queue for its pending next() promise.
+ // Closing the stream now causes two things to happen *synchronously*:
+ // 1. ReadableStreamClose resolves reader.[[closedPromise]] with undefined.
+ // 2. ReadableStreamClose calls the read request's close steps, which calls ReadableStreamReaderGenericRelease,
+ // which replaces reader.[[closedPromise]] with a rejected promise.
+ c.close();
+ }
+ });
+
+ const chunks = [];
+ for await (const chunk of rs) {
+ chunks.push(chunk);
+ }
+ assert_array_equals(chunks, ['a', 'b', 'c']);
+}, 'close() while next() is pending');
diff --git a/testing/web-platform/tests/streams/readable-streams/bad-strategies.any.js b/testing/web-platform/tests/streams/readable-streams/bad-strategies.any.js
new file mode 100644
index 0000000000..521fbffe3a
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/bad-strategies.any.js
@@ -0,0 +1,159 @@
+// META: global=window,worker
+'use strict';
+
+test(() => {
+
+ const theError = new Error('a unique string');
+
+ assert_throws_exactly(theError, () => {
+ new ReadableStream({}, {
+ get size() {
+ throw theError;
+ },
+ highWaterMark: 5
+ });
+ }, 'construction should re-throw the error');
+
+}, 'Readable stream: throwing strategy.size getter');
+
+promise_test(t => {
+
+ const controllerError = { name: 'controller error' };
+ const thrownError = { name: 'thrown error' };
+
+ let controller;
+ const rs = new ReadableStream(
+ {
+ start(c) {
+ controller = c;
+ }
+ },
+ {
+ size() {
+ controller.error(controllerError);
+ throw thrownError;
+ },
+ highWaterMark: 5
+ }
+ );
+
+ assert_throws_exactly(thrownError, () => controller.enqueue('a'), 'enqueue should re-throw the error');
+
+ return promise_rejects_exactly(t, controllerError, rs.getReader().closed);
+
+}, 'Readable stream: strategy.size errors the stream and then throws');
+
+promise_test(t => {
+
+ const theError = { name: 'my error' };
+
+ let controller;
+ const rs = new ReadableStream(
+ {
+ start(c) {
+ controller = c;
+ }
+ },
+ {
+ size() {
+ controller.error(theError);
+ return Infinity;
+ },
+ highWaterMark: 5
+ }
+ );
+
+ assert_throws_js(RangeError, () => controller.enqueue('a'), 'enqueue should throw a RangeError');
+
+ return promise_rejects_exactly(t, theError, rs.getReader().closed, 'closed should reject with the error');
+
+}, 'Readable stream: strategy.size errors the stream and then returns Infinity');
+
+promise_test(() => {
+
+ const theError = new Error('a unique string');
+ const rs = new ReadableStream(
+ {
+ start(c) {
+ assert_throws_exactly(theError, () => c.enqueue('a'), 'enqueue should throw the error');
+ }
+ },
+ {
+ size() {
+ throw theError;
+ },
+ highWaterMark: 5
+ }
+ );
+
+ return rs.getReader().closed.catch(e => {
+ assert_equals(e, theError, 'closed should reject with the error');
+ });
+
+}, 'Readable stream: throwing strategy.size method');
+
+test(() => {
+
+ const theError = new Error('a unique string');
+
+ assert_throws_exactly(theError, () => {
+ new ReadableStream({}, {
+ size() {
+ return 1;
+ },
+ get highWaterMark() {
+ throw theError;
+ }
+ });
+ }, 'construction should re-throw the error');
+
+}, 'Readable stream: throwing strategy.highWaterMark getter');
+
+test(() => {
+
+ for (const highWaterMark of [-1, -Infinity, NaN, 'foo', {}]) {
+ assert_throws_js(RangeError, () => {
+ new ReadableStream({}, {
+ size() {
+ return 1;
+ },
+ highWaterMark
+ });
+ }, 'construction should throw a RangeError for ' + highWaterMark);
+ }
+
+}, 'Readable stream: invalid strategy.highWaterMark');
+
+promise_test(() => {
+
+ const promises = [];
+ for (const size of [NaN, -Infinity, Infinity, -1]) {
+ let theError;
+ const rs = new ReadableStream(
+ {
+ start(c) {
+ try {
+ c.enqueue('hi');
+ assert_unreached('enqueue didn\'t throw');
+ } catch (error) {
+ assert_equals(error.name, 'RangeError', 'enqueue should throw a RangeError for ' + size);
+ theError = error;
+ }
+ }
+ },
+ {
+ size() {
+ return size;
+ },
+ highWaterMark: 5
+ }
+ );
+
+ promises.push(rs.getReader().closed.catch(e => {
+ assert_equals(e, theError, 'closed should reject with the error for ' + size);
+ }));
+ }
+
+ return Promise.all(promises);
+
+}, 'Readable stream: invalid strategy.size return value');
diff --git a/testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.any.js b/testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.any.js
new file mode 100644
index 0000000000..e9cf4c9249
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.any.js
@@ -0,0 +1,400 @@
+// META: global=window,worker
+'use strict';
+
+
+test(() => {
+
+ const theError = new Error('a unique string');
+
+ assert_throws_exactly(theError, () => {
+ new ReadableStream({
+ get start() {
+ throw theError;
+ }
+ });
+ }, 'constructing the stream should re-throw the error');
+
+}, 'Underlying source start: throwing getter');
+
+
+test(() => {
+
+ const theError = new Error('a unique string');
+
+ assert_throws_exactly(theError, () => {
+ new ReadableStream({
+ start() {
+ throw theError;
+ }
+ });
+ }, 'constructing the stream should re-throw the error');
+
+}, 'Underlying source start: throwing method');
+
+
+test(() => {
+
+ const theError = new Error('a unique string');
+ assert_throws_exactly(theError, () => new ReadableStream({
+ get pull() {
+ throw theError;
+ }
+ }), 'constructor should throw');
+
+}, 'Underlying source: throwing pull getter (initial pull)');
+
+
+promise_test(t => {
+
+ const theError = new Error('a unique string');
+ const rs = new ReadableStream({
+ pull() {
+ throw theError;
+ }
+ });
+
+ return promise_rejects_exactly(t, theError, rs.getReader().closed);
+
+}, 'Underlying source: throwing pull method (initial pull)');
+
+
+promise_test(t => {
+
+ const theError = new Error('a unique string');
+
+ let counter = 0;
+ const rs = new ReadableStream({
+ get pull() {
+ ++counter;
+ if (counter === 1) {
+ return c => c.enqueue('a');
+ }
+
+ throw theError;
+ }
+ });
+ const reader = rs.getReader();
+
+ return Promise.all([
+ reader.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'the first chunk read should be correct');
+ }),
+ reader.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'the second chunk read should be correct');
+ assert_equals(counter, 1, 'counter should be 1');
+ })
+ ]);
+
+}, 'Underlying source pull: throwing getter (second pull does not result in a second get)');
+
+promise_test(t => {
+
+ const theError = new Error('a unique string');
+
+ let counter = 0;
+ const rs = new ReadableStream({
+ pull(c) {
+ ++counter;
+ if (counter === 1) {
+ c.enqueue('a');
+ return;
+ }
+
+ throw theError;
+ }
+ });
+ const reader = rs.getReader();
+
+ return Promise.all([
+ reader.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'the chunk read should be correct');
+ }),
+ promise_rejects_exactly(t, theError, reader.closed)
+ ]);
+
+}, 'Underlying source pull: throwing method (second pull)');
+
+test(() => {
+
+ const theError = new Error('a unique string');
+ assert_throws_exactly(theError, () => new ReadableStream({
+ get cancel() {
+ throw theError;
+ }
+ }), 'constructor should throw');
+
+}, 'Underlying source cancel: throwing getter');
+
+promise_test(t => {
+
+ const theError = new Error('a unique string');
+ const rs = new ReadableStream({
+ cancel() {
+ throw theError;
+ }
+ });
+
+ return promise_rejects_exactly(t, theError, rs.cancel());
+
+}, 'Underlying source cancel: throwing method');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ rs.cancel();
+ assert_throws_js(TypeError, () => controller.enqueue('a'), 'Calling enqueue after canceling should throw');
+
+ return rs.getReader().closed;
+
+}, 'Underlying source: calling enqueue on an empty canceled stream should throw');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ c.enqueue('b');
+ controller = c;
+ }
+ });
+
+ rs.cancel();
+ assert_throws_js(TypeError, () => controller.enqueue('c'), 'Calling enqueue after canceling should throw');
+
+ return rs.getReader().closed;
+
+}, 'Underlying source: calling enqueue on a non-empty canceled stream should throw');
+
+promise_test(() => {
+
+ return new ReadableStream({
+ start(c) {
+ c.close();
+ assert_throws_js(TypeError, () => c.enqueue('a'), 'call to enqueue should throw a TypeError');
+ }
+ }).getReader().closed;
+
+}, 'Underlying source: calling enqueue on a closed stream should throw');
+
+promise_test(t => {
+
+ const theError = new Error('boo');
+ const closed = new ReadableStream({
+ start(c) {
+ c.error(theError);
+ assert_throws_js(TypeError, () => c.enqueue('a'), 'call to enqueue should throw the error');
+ }
+ }).getReader().closed;
+
+ return promise_rejects_exactly(t, theError, closed);
+
+}, 'Underlying source: calling enqueue on an errored stream should throw');
+
+promise_test(() => {
+
+ return new ReadableStream({
+ start(c) {
+ c.close();
+ assert_throws_js(TypeError, () => c.close(), 'second call to close should throw a TypeError');
+ }
+ }).getReader().closed;
+
+}, 'Underlying source: calling close twice on an empty stream should throw the second time');
+
+promise_test(() => {
+
+ let startCalled = false;
+ let readCalled = false;
+ const reader = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ c.close();
+ assert_throws_js(TypeError, () => c.close(), 'second call to close should throw a TypeError');
+ startCalled = true;
+ }
+ }).getReader();
+
+ return Promise.all([
+ reader.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'read() should read the enqueued chunk');
+ readCalled = true;
+ }),
+ reader.closed.then(() => {
+ assert_true(startCalled);
+ assert_true(readCalled);
+ })
+ ]);
+
+}, 'Underlying source: calling close twice on a non-empty stream should throw the second time');
+
+promise_test(() => {
+
+ let controller;
+ let startCalled = false;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ startCalled = true;
+ }
+ });
+
+ rs.cancel();
+ assert_throws_js(TypeError, () => controller.close(), 'Calling close after canceling should throw');
+
+ return rs.getReader().closed.then(() => {
+ assert_true(startCalled);
+ });
+
+}, 'Underlying source: calling close on an empty canceled stream should throw');
+
+promise_test(() => {
+
+ let controller;
+ let startCalled = false;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ c.enqueue('a');
+ startCalled = true;
+ }
+ });
+
+ rs.cancel();
+ assert_throws_js(TypeError, () => controller.close(), 'Calling close after canceling should throw');
+
+ return rs.getReader().closed.then(() => {
+ assert_true(startCalled);
+ });
+
+}, 'Underlying source: calling close on a non-empty canceled stream should throw');
+
+promise_test(() => {
+
+ const theError = new Error('boo');
+ let startCalled = false;
+
+ const closed = new ReadableStream({
+ start(c) {
+ c.error(theError);
+ assert_throws_js(TypeError, () => c.close(), 'call to close should throw a TypeError');
+ startCalled = true;
+ }
+ }).getReader().closed;
+
+ return closed.catch(e => {
+ assert_true(startCalled);
+ assert_equals(e, theError, 'closed should reject with the error');
+ });
+
+}, 'Underlying source: calling close after error should throw');
+
+promise_test(() => {
+
+ const theError = new Error('boo');
+ let startCalled = false;
+
+ const closed = new ReadableStream({
+ start(c) {
+ c.error(theError);
+ c.error();
+ startCalled = true;
+ }
+ }).getReader().closed;
+
+ return closed.catch(e => {
+ assert_true(startCalled);
+ assert_equals(e, theError, 'closed should reject with the error');
+ });
+
+}, 'Underlying source: calling error twice should not throw');
+
+promise_test(() => {
+
+ let startCalled = false;
+
+ const closed = new ReadableStream({
+ start(c) {
+ c.close();
+ c.error();
+ startCalled = true;
+ }
+ }).getReader().closed;
+
+ return closed.then(() => assert_true(startCalled));
+
+}, 'Underlying source: calling error after close should not throw');
+
+promise_test(() => {
+
+ let startCalled = false;
+ const firstError = new Error('1');
+ const secondError = new Error('2');
+
+ const closed = new ReadableStream({
+ start(c) {
+ c.error(firstError);
+ startCalled = true;
+ return Promise.reject(secondError);
+ }
+ }).getReader().closed;
+
+ return closed.catch(e => {
+ assert_true(startCalled);
+ assert_equals(e, firstError, 'closed should reject with the first error');
+ });
+
+}, 'Underlying source: calling error and returning a rejected promise from start should cause the stream to error ' +
+ 'with the first error');
+
+promise_test(() => {
+
+ let startCalled = false;
+ const firstError = new Error('1');
+ const secondError = new Error('2');
+
+ const closed = new ReadableStream({
+ pull(c) {
+ c.error(firstError);
+ startCalled = true;
+ return Promise.reject(secondError);
+ }
+ }).getReader().closed;
+
+ return closed.catch(e => {
+ assert_true(startCalled);
+ assert_equals(e, firstError, 'closed should reject with the first error');
+ });
+
+}, 'Underlying source: calling error and returning a rejected promise from pull should cause the stream to error ' +
+ 'with the first error');
+
+const error1 = { name: 'error1' };
+
+promise_test(t => {
+
+ let pullShouldThrow = false;
+ const rs = new ReadableStream({
+ pull(controller) {
+ if (pullShouldThrow) {
+ throw error1;
+ }
+ controller.enqueue(0);
+ }
+ }, new CountQueuingStrategy({highWaterMark: 1}));
+ const reader = rs.getReader();
+ return Promise.resolve().then(() => {
+ pullShouldThrow = true;
+ return Promise.all([
+ reader.read(),
+ promise_rejects_exactly(t, error1, reader.closed, '.closed promise should reject')
+ ]);
+ });
+
+}, 'read should not error if it dequeues and pull() throws');
diff --git a/testing/web-platform/tests/streams/readable-streams/cancel.any.js b/testing/web-platform/tests/streams/readable-streams/cancel.any.js
new file mode 100644
index 0000000000..800bd99441
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/cancel.any.js
@@ -0,0 +1,236 @@
+// META: global=window,worker
+// META: script=../resources/test-utils.js
+// META: script=../resources/rs-utils.js
+'use strict';
+
+promise_test(t => {
+
+ const randomSource = new RandomPushSource();
+
+ let cancellationFinished = false;
+ const rs = new ReadableStream({
+ start(c) {
+ randomSource.ondata = c.enqueue.bind(c);
+ randomSource.onend = c.close.bind(c);
+ randomSource.onerror = c.error.bind(c);
+ },
+
+ pull() {
+ randomSource.readStart();
+ },
+
+ cancel() {
+ randomSource.readStop();
+
+ return new Promise(resolve => {
+ t.step_timeout(() => {
+ cancellationFinished = true;
+ resolve();
+ }, 1);
+ });
+ }
+ });
+
+ const reader = rs.getReader();
+
+ // We call delay multiple times to avoid cancelling too early for the
+ // source to enqueue at least one chunk.
+ const cancel = delay(5).then(() => delay(5)).then(() => delay(5)).then(() => {
+ const cancelPromise = reader.cancel();
+ assert_false(cancellationFinished, 'cancellation in source should happen later');
+ return cancelPromise;
+ });
+
+ return readableStreamToArray(rs, reader).then(chunks => {
+ assert_greater_than(chunks.length, 0, 'at least one chunk should be read');
+ for (let i = 0; i < chunks.length; i++) {
+ assert_equals(chunks[i].length, 128, 'chunk ' + i + ' should have 128 bytes');
+ }
+ return cancel;
+ }).then(() => {
+ assert_true(cancellationFinished, 'it returns a promise that is fulfilled when the cancellation finishes');
+ });
+
+}, 'ReadableStream cancellation: integration test on an infinite stream derived from a random push source');
+
+test(() => {
+
+ let recordedReason;
+ const rs = new ReadableStream({
+ cancel(reason) {
+ recordedReason = reason;
+ }
+ });
+
+ const passedReason = new Error('Sorry, it just wasn\'t meant to be.');
+ rs.cancel(passedReason);
+
+ assert_equals(recordedReason, passedReason,
+ 'the error passed to the underlying source\'s cancel method should equal the one passed to the stream\'s cancel');
+
+}, 'ReadableStream cancellation: cancel(reason) should pass through the given reason to the underlying source');
+
+promise_test(() => {
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ c.close();
+ },
+ cancel() {
+ assert_unreached('underlying source cancel() should not have been called');
+ }
+ });
+
+ const reader = rs.getReader();
+
+ return rs.cancel().then(() => {
+ assert_unreached('cancel() should be rejected');
+ }, e => {
+ assert_equals(e.name, 'TypeError', 'cancel() should be rejected with a TypeError');
+ }).then(() => {
+ return reader.read();
+ }).then(result => {
+ assert_object_equals(result, { value: 'a', done: false }, 'read() should still work after the attempted cancel');
+ return reader.closed;
+ });
+
+}, 'ReadableStream cancellation: cancel() on a locked stream should fail and not call the underlying source cancel');
+
+promise_test(() => {
+
+ let cancelReceived = false;
+ const cancelReason = new Error('I am tired of this stream, I prefer to cancel it');
+ const rs = new ReadableStream({
+ cancel(reason) {
+ cancelReceived = true;
+ assert_equals(reason, cancelReason, 'cancellation reason given to the underlying source should be equal to the one passed');
+ }
+ });
+
+ return rs.cancel(cancelReason).then(() => {
+ assert_true(cancelReceived);
+ });
+
+}, 'ReadableStream cancellation: should fulfill promise when cancel callback went fine');
+
+promise_test(() => {
+
+ const rs = new ReadableStream({
+ cancel() {
+ return 'Hello';
+ }
+ });
+
+ return rs.cancel().then(v => {
+ assert_equals(v, undefined, 'cancel() return value should be fulfilled with undefined');
+ });
+
+}, 'ReadableStream cancellation: returning a value from the underlying source\'s cancel should not affect the fulfillment value of the promise returned by the stream\'s cancel');
+
+promise_test(() => {
+
+ const thrownError = new Error('test');
+ let cancelCalled = false;
+
+ const rs = new ReadableStream({
+ cancel() {
+ cancelCalled = true;
+ throw thrownError;
+ }
+ });
+
+ return rs.cancel('test').then(() => {
+ assert_unreached('cancel should reject');
+ }, e => {
+ assert_true(cancelCalled);
+ assert_equals(e, thrownError);
+ });
+
+}, 'ReadableStream cancellation: should reject promise when cancel callback raises an exception');
+
+promise_test(() => {
+
+ const cancelReason = new Error('test');
+
+ const rs = new ReadableStream({
+ cancel(error) {
+ assert_equals(error, cancelReason);
+ return delay(1);
+ }
+ });
+
+ return rs.cancel(cancelReason);
+
+}, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should fulfill when that one does (1)');
+
+promise_test(t => {
+
+ let resolveSourceCancelPromise;
+ let sourceCancelPromiseHasFulfilled = false;
+
+ const rs = new ReadableStream({
+ cancel() {
+ const sourceCancelPromise = new Promise(resolve => resolveSourceCancelPromise = resolve);
+
+ sourceCancelPromise.then(() => {
+ sourceCancelPromiseHasFulfilled = true;
+ });
+
+ return sourceCancelPromise;
+ }
+ });
+
+ t.step_timeout(() => resolveSourceCancelPromise('Hello'), 1);
+
+ return rs.cancel().then(value => {
+ assert_true(sourceCancelPromiseHasFulfilled, 'cancel() return value should be fulfilled only after the promise returned by the underlying source\'s cancel');
+ assert_equals(value, undefined, 'cancel() return value should be fulfilled with undefined');
+ });
+
+}, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should fulfill when that one does (2)');
+
+promise_test(t => {
+
+ let rejectSourceCancelPromise;
+ let sourceCancelPromiseHasRejected = false;
+
+ const rs = new ReadableStream({
+ cancel() {
+ const sourceCancelPromise = new Promise((resolve, reject) => rejectSourceCancelPromise = reject);
+
+ sourceCancelPromise.catch(() => {
+ sourceCancelPromiseHasRejected = true;
+ });
+
+ return sourceCancelPromise;
+ }
+ });
+
+ const errorInCancel = new Error('Sorry, it just wasn\'t meant to be.');
+
+ t.step_timeout(() => rejectSourceCancelPromise(errorInCancel), 1);
+
+ return rs.cancel().then(() => {
+ assert_unreached('cancel() return value should be rejected');
+ }, r => {
+ assert_true(sourceCancelPromiseHasRejected, 'cancel() return value should be rejected only after the promise returned by the underlying source\'s cancel');
+ assert_equals(r, errorInCancel, 'cancel() return value should be rejected with the underlying source\'s rejection reason');
+ });
+
+}, 'ReadableStream cancellation: if the underlying source\'s cancel method returns a promise, the promise returned by the stream\'s cancel should reject when that one does');
+
+promise_test(() => {
+
+ const rs = new ReadableStream({
+ start() {
+ return new Promise(() => {});
+ },
+ pull() {
+ assert_unreached('pull should not have been called');
+ }
+ });
+
+ return Promise.all([rs.cancel(), rs.getReader().closed]);
+
+}, 'ReadableStream cancellation: cancelling before start finishes should prevent pull() from being called');
diff --git a/testing/web-platform/tests/streams/readable-streams/constructor.any.js b/testing/web-platform/tests/streams/readable-streams/constructor.any.js
new file mode 100644
index 0000000000..608dc48cfa
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/constructor.any.js
@@ -0,0 +1,17 @@
+// META: global=window,worker
+'use strict';
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+const error2 = new Error('error2');
+error2.name = 'error2';
+
+test(() => {
+ const underlyingSource = { get start() { throw error1; } };
+ const queuingStrategy = { highWaterMark: 0, get size() { throw error2; } };
+
+ // underlyingSource is converted in prose in the method body, whereas queuingStrategy is done at the IDL layer.
+ // So the queuingStrategy exception should be encountered first.
+ assert_throws_exactly(error2, () => new ReadableStream(underlyingSource, queuingStrategy));
+}, 'underlyingSource argument should be converted after queuingStrategy argument');
diff --git a/testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.any.js b/testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.any.js
new file mode 100644
index 0000000000..02ac5bae5c
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.any.js
@@ -0,0 +1,208 @@
+// META: global=window,worker
+'use strict';
+
+test(() => {
+
+ new ReadableStream({}, new CountQueuingStrategy({ highWaterMark: 4 }));
+
+}, 'Can construct a readable stream with a valid CountQueuingStrategy');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream(
+ {
+ start(c) {
+ controller = c;
+ }
+ },
+ new CountQueuingStrategy({ highWaterMark: 0 })
+ );
+ const reader = rs.getReader();
+
+ assert_equals(controller.desiredSize, 0, '0 reads, 0 enqueues: desiredSize should be 0');
+ controller.enqueue('a');
+ assert_equals(controller.desiredSize, -1, '0 reads, 1 enqueue: desiredSize should be -1');
+ controller.enqueue('b');
+ assert_equals(controller.desiredSize, -2, '0 reads, 2 enqueues: desiredSize should be -2');
+ controller.enqueue('c');
+ assert_equals(controller.desiredSize, -3, '0 reads, 3 enqueues: desiredSize should be -3');
+ controller.enqueue('d');
+ assert_equals(controller.desiredSize, -4, '0 reads, 4 enqueues: desiredSize should be -4');
+
+ return reader.read()
+ .then(result => {
+ assert_object_equals(result, { value: 'a', done: false },
+ '1st read gives back the 1st chunk enqueued (queue now contains 3 chunks)');
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'b', done: false },
+ '2nd read gives back the 2nd chunk enqueued (queue now contains 2 chunks)');
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'c', done: false },
+ '3rd read gives back the 3rd chunk enqueued (queue now contains 1 chunk)');
+
+ assert_equals(controller.desiredSize, -1, '3 reads, 4 enqueues: desiredSize should be -1');
+ controller.enqueue('e');
+ assert_equals(controller.desiredSize, -2, '3 reads, 5 enqueues: desiredSize should be -2');
+
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'd', done: false },
+ '4th read gives back the 4th chunk enqueued (queue now contains 1 chunks)');
+ return reader.read();
+
+ }).then(result => {
+ assert_object_equals(result, { value: 'e', done: false },
+ '5th read gives back the 5th chunk enqueued (queue now contains 0 chunks)');
+
+ assert_equals(controller.desiredSize, 0, '5 reads, 5 enqueues: desiredSize should be 0');
+ controller.enqueue('f');
+ assert_equals(controller.desiredSize, -1, '5 reads, 6 enqueues: desiredSize should be -1');
+ controller.enqueue('g');
+ assert_equals(controller.desiredSize, -2, '5 reads, 7 enqueues: desiredSize should be -2');
+ });
+
+}, 'Correctly governs a ReadableStreamController\'s desiredSize property (HWM = 0)');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream(
+ {
+ start(c) {
+ controller = c;
+ }
+ },
+ new CountQueuingStrategy({ highWaterMark: 1 })
+ );
+ const reader = rs.getReader();
+
+ assert_equals(controller.desiredSize, 1, '0 reads, 0 enqueues: desiredSize should be 1');
+ controller.enqueue('a');
+ assert_equals(controller.desiredSize, 0, '0 reads, 1 enqueue: desiredSize should be 0');
+ controller.enqueue('b');
+ assert_equals(controller.desiredSize, -1, '0 reads, 2 enqueues: desiredSize should be -1');
+ controller.enqueue('c');
+ assert_equals(controller.desiredSize, -2, '0 reads, 3 enqueues: desiredSize should be -2');
+ controller.enqueue('d');
+ assert_equals(controller.desiredSize, -3, '0 reads, 4 enqueues: desiredSize should be -3');
+
+ return reader.read()
+ .then(result => {
+ assert_object_equals(result, { value: 'a', done: false },
+ '1st read gives back the 1st chunk enqueued (queue now contains 3 chunks)');
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'b', done: false },
+ '2nd read gives back the 2nd chunk enqueued (queue now contains 2 chunks)');
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'c', done: false },
+ '3rd read gives back the 3rd chunk enqueued (queue now contains 1 chunk)');
+
+ assert_equals(controller.desiredSize, 0, '3 reads, 4 enqueues: desiredSize should be 0');
+ controller.enqueue('e');
+ assert_equals(controller.desiredSize, -1, '3 reads, 5 enqueues: desiredSize should be -1');
+
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'd', done: false },
+ '4th read gives back the 4th chunk enqueued (queue now contains 1 chunks)');
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'e', done: false },
+ '5th read gives back the 5th chunk enqueued (queue now contains 0 chunks)');
+
+ assert_equals(controller.desiredSize, 1, '5 reads, 5 enqueues: desiredSize should be 1');
+ controller.enqueue('f');
+ assert_equals(controller.desiredSize, 0, '5 reads, 6 enqueues: desiredSize should be 0');
+ controller.enqueue('g');
+ assert_equals(controller.desiredSize, -1, '5 reads, 7 enqueues: desiredSize should be -1');
+ });
+
+}, 'Correctly governs a ReadableStreamController\'s desiredSize property (HWM = 1)');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream(
+ {
+ start(c) {
+ controller = c;
+ }
+ },
+ new CountQueuingStrategy({ highWaterMark: 4 })
+ );
+ const reader = rs.getReader();
+
+ assert_equals(controller.desiredSize, 4, '0 reads, 0 enqueues: desiredSize should be 4');
+ controller.enqueue('a');
+ assert_equals(controller.desiredSize, 3, '0 reads, 1 enqueue: desiredSize should be 3');
+ controller.enqueue('b');
+ assert_equals(controller.desiredSize, 2, '0 reads, 2 enqueues: desiredSize should be 2');
+ controller.enqueue('c');
+ assert_equals(controller.desiredSize, 1, '0 reads, 3 enqueues: desiredSize should be 1');
+ controller.enqueue('d');
+ assert_equals(controller.desiredSize, 0, '0 reads, 4 enqueues: desiredSize should be 0');
+ controller.enqueue('e');
+ assert_equals(controller.desiredSize, -1, '0 reads, 5 enqueues: desiredSize should be -1');
+ controller.enqueue('f');
+ assert_equals(controller.desiredSize, -2, '0 reads, 6 enqueues: desiredSize should be -2');
+
+
+ return reader.read()
+ .then(result => {
+ assert_object_equals(result, { value: 'a', done: false },
+ '1st read gives back the 1st chunk enqueued (queue now contains 5 chunks)');
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'b', done: false },
+ '2nd read gives back the 2nd chunk enqueued (queue now contains 4 chunks)');
+
+ assert_equals(controller.desiredSize, 0, '2 reads, 6 enqueues: desiredSize should be 0');
+ controller.enqueue('g');
+ assert_equals(controller.desiredSize, -1, '2 reads, 7 enqueues: desiredSize should be -1');
+
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'c', done: false },
+ '3rd read gives back the 3rd chunk enqueued (queue now contains 4 chunks)');
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'd', done: false },
+ '4th read gives back the 4th chunk enqueued (queue now contains 3 chunks)');
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'e', done: false },
+ '5th read gives back the 5th chunk enqueued (queue now contains 2 chunks)');
+ return reader.read();
+ })
+ .then(result => {
+ assert_object_equals(result, { value: 'f', done: false },
+ '6th read gives back the 6th chunk enqueued (queue now contains 0 chunks)');
+
+ assert_equals(controller.desiredSize, 3, '6 reads, 7 enqueues: desiredSize should be 3');
+ controller.enqueue('h');
+ assert_equals(controller.desiredSize, 2, '6 reads, 8 enqueues: desiredSize should be 2');
+ controller.enqueue('i');
+ assert_equals(controller.desiredSize, 1, '6 reads, 9 enqueues: desiredSize should be 1');
+ controller.enqueue('j');
+ assert_equals(controller.desiredSize, 0, '6 reads, 10 enqueues: desiredSize should be 0');
+ controller.enqueue('k');
+ assert_equals(controller.desiredSize, -1, '6 reads, 11 enqueues: desiredSize should be -1');
+ });
+
+}, 'Correctly governs a ReadableStreamController\'s desiredSize property (HWM = 4)');
diff --git a/testing/web-platform/tests/streams/readable-streams/crashtests/empty.js b/testing/web-platform/tests/streams/readable-streams/crashtests/empty.js
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/crashtests/empty.js
diff --git a/testing/web-platform/tests/streams/readable-streams/crashtests/strategy-worker-terminate.html b/testing/web-platform/tests/streams/readable-streams/crashtests/strategy-worker-terminate.html
new file mode 100644
index 0000000000..a75c3c66b6
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/crashtests/strategy-worker-terminate.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html class="test-wait">
+<meta charset="utf-8">
+<script>
+ var c = new Worker("/streams/readable-streams/crashtests/strategy-worker.js");
+ c.onmessage = () => {
+ c.terminate();
+ document.documentElement.classList.remove("test-wait");
+ }
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/crashtests/strategy-worker.js b/testing/web-platform/tests/streams/readable-streams/crashtests/strategy-worker.js
new file mode 100644
index 0000000000..dd0ab03b55
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/crashtests/strategy-worker.js
@@ -0,0 +1,4 @@
+var b = new CountQueuingStrategy({ highWaterMark: 3 });
+
+importScripts("empty.js");
+postMessage("done");
diff --git a/testing/web-platform/tests/streams/readable-streams/cross-realm-crash.window.js b/testing/web-platform/tests/streams/readable-streams/cross-realm-crash.window.js
new file mode 100644
index 0000000000..5fc7ce37a5
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/cross-realm-crash.window.js
@@ -0,0 +1,13 @@
+// This is a repro for a crash bug that existed in Blink. See
+// https://crbug.com/1290014. If there's no crash then the test passed.
+
+test(t => {
+ const iframeTag = document.createElement('iframe');
+ document.body.appendChild(iframeTag);
+
+ const readableStream = new ReadableStream();
+ const reader = new iframeTag.contentWindow.ReadableStreamDefaultReader(readableStream);
+ iframeTag.remove();
+ reader.cancel();
+ reader.read();
+}, 'should not crash on reading from stream cancelled in destroyed realm');
diff --git a/testing/web-platform/tests/streams/readable-streams/default-reader.any.js b/testing/web-platform/tests/streams/readable-streams/default-reader.any.js
new file mode 100644
index 0000000000..59d7ab2f74
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/default-reader.any.js
@@ -0,0 +1,539 @@
+// META: global=window,worker
+// META: script=../resources/rs-utils.js
+'use strict';
+
+test(() => {
+
+ assert_throws_js(TypeError, () => new ReadableStreamDefaultReader('potato'));
+ assert_throws_js(TypeError, () => new ReadableStreamDefaultReader({}));
+ assert_throws_js(TypeError, () => new ReadableStreamDefaultReader());
+
+}, 'ReadableStreamDefaultReader constructor should get a ReadableStream object as argument');
+
+test(() => {
+
+ const rsReader = new ReadableStreamDefaultReader(new ReadableStream());
+ assert_equals(rsReader.closed, rsReader.closed, 'closed should return the same promise');
+
+}, 'ReadableStreamDefaultReader closed should always return the same promise object');
+
+test(() => {
+
+ const rs = new ReadableStream();
+ new ReadableStreamDefaultReader(rs); // Constructing directly the first time should be fine.
+ assert_throws_js(TypeError, () => new ReadableStreamDefaultReader(rs),
+ 'constructing directly the second time should fail');
+
+}, 'Constructing a ReadableStreamDefaultReader directly should fail if the stream is already locked (via direct ' +
+ 'construction)');
+
+test(() => {
+
+ const rs = new ReadableStream();
+ new ReadableStreamDefaultReader(rs); // Constructing directly should be fine.
+ assert_throws_js(TypeError, () => rs.getReader(), 'getReader() should fail');
+
+}, 'Getting a ReadableStreamDefaultReader via getReader should fail if the stream is already locked (via direct ' +
+ 'construction)');
+
+test(() => {
+
+ const rs = new ReadableStream();
+ rs.getReader(); // getReader() should be fine.
+ assert_throws_js(TypeError, () => new ReadableStreamDefaultReader(rs), 'constructing directly should fail');
+
+}, 'Constructing a ReadableStreamDefaultReader directly should fail if the stream is already locked (via getReader)');
+
+test(() => {
+
+ const rs = new ReadableStream();
+ rs.getReader(); // getReader() should be fine.
+ assert_throws_js(TypeError, () => rs.getReader(), 'getReader() should fail');
+
+}, 'Getting a ReadableStreamDefaultReader via getReader should fail if the stream is already locked (via getReader)');
+
+test(() => {
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.close();
+ }
+ });
+
+ new ReadableStreamDefaultReader(rs); // Constructing directly should not throw.
+
+}, 'Constructing a ReadableStreamDefaultReader directly should be OK if the stream is closed');
+
+test(() => {
+
+ const theError = new Error('don\'t say i didn\'t warn ya');
+ const rs = new ReadableStream({
+ start(c) {
+ c.error(theError);
+ }
+ });
+
+ new ReadableStreamDefaultReader(rs); // Constructing directly should not throw.
+
+}, 'Constructing a ReadableStreamDefaultReader directly should be OK if the stream is errored');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ const reader = rs.getReader();
+
+ const promise = reader.read().then(result => {
+ assert_object_equals(result, { value: 'a', done: false }, 'read() should fulfill with the enqueued chunk');
+ });
+
+ controller.enqueue('a');
+ return promise;
+
+}, 'Reading from a reader for an empty stream will wait until a chunk is available');
+
+promise_test(() => {
+
+ let cancelCalled = false;
+ const passedReason = new Error('it wasn\'t the right time, sorry');
+ const rs = new ReadableStream({
+ cancel(reason) {
+ assert_true(rs.locked, 'the stream should still be locked');
+ assert_throws_js(TypeError, () => rs.getReader(), 'should not be able to get another reader');
+ assert_equals(reason, passedReason, 'the cancellation reason is passed through to the underlying source');
+ cancelCalled = true;
+ }
+ });
+
+ const reader = rs.getReader();
+ return reader.cancel(passedReason).then(() => assert_true(cancelCalled));
+
+}, 'cancel() on a reader does not release the reader');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const reader = rs.getReader();
+ const promise = reader.closed;
+
+ controller.close();
+ return promise;
+
+}, 'closed should be fulfilled after stream is closed (.closed access before acquiring)');
+
+promise_test(t => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const reader1 = rs.getReader();
+
+ reader1.releaseLock();
+
+ const reader2 = rs.getReader();
+ controller.close();
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, reader1.closed),
+ reader2.closed
+ ]);
+
+}, 'closed should be rejected after reader releases its lock (multiple stream locks)');
+
+promise_test(t => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const reader = rs.getReader();
+ const promise1 = reader.closed;
+
+ controller.close();
+
+ reader.releaseLock();
+ const promise2 = reader.closed;
+
+ assert_not_equals(promise1, promise2, '.closed should be replaced');
+ return Promise.all([
+ promise1,
+ promise_rejects_js(t, TypeError, promise2, '.closed after releasing lock'),
+ ]);
+
+}, 'closed is replaced when stream closes and reader releases its lock');
+
+promise_test(t => {
+
+ const theError = { name: 'unique error' };
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const reader = rs.getReader();
+ const promise1 = reader.closed;
+
+ controller.error(theError);
+
+ reader.releaseLock();
+ const promise2 = reader.closed;
+
+ assert_not_equals(promise1, promise2, '.closed should be replaced');
+ return Promise.all([
+ promise_rejects_exactly(t, theError, promise1, '.closed before releasing lock'),
+ promise_rejects_js(t, TypeError, promise2, '.closed after releasing lock')
+ ]);
+
+}, 'closed is replaced when stream errors and reader releases its lock');
+
+promise_test(() => {
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ c.enqueue('b');
+ c.close();
+ }
+ });
+
+ const reader1 = rs.getReader();
+ const promise1 = reader1.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'reading the first chunk from reader1 works');
+ });
+ reader1.releaseLock();
+
+ const reader2 = rs.getReader();
+ const promise2 = reader2.read().then(r => {
+ assert_object_equals(r, { value: 'b', done: false }, 'reading the second chunk from reader2 works');
+ });
+ reader2.releaseLock();
+
+ return Promise.all([promise1, promise2]);
+
+}, 'Multiple readers can access the stream in sequence');
+
+promise_test(() => {
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ }
+ });
+
+ const reader1 = rs.getReader();
+ reader1.releaseLock();
+
+ const reader2 = rs.getReader();
+
+ // Should be a no-op
+ reader1.releaseLock();
+
+ return reader2.read().then(result => {
+ assert_object_equals(result, { value: 'a', done: false },
+ 'read() should still work on reader2 even after reader1 is released');
+ });
+
+}, 'Cannot use an already-released reader to unlock a stream again');
+
+promise_test(t => {
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ },
+ cancel() {
+ assert_unreached('underlying source cancel should not be called');
+ }
+ });
+
+ const reader = rs.getReader();
+ reader.releaseLock();
+ const cancelPromise = reader.cancel();
+
+ const reader2 = rs.getReader();
+ const readPromise = reader2.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'a new reader should be able to read a chunk');
+ });
+
+ return Promise.all([
+ promise_rejects_js(t, TypeError, cancelPromise),
+ readPromise
+ ]);
+
+}, 'cancel() on a released reader is a no-op and does not pass through');
+
+promise_test(t => {
+
+ const promiseAsserts = [];
+
+ let controller;
+ const theError = { name: 'unique error' };
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const reader1 = rs.getReader();
+
+ promiseAsserts.push(
+ promise_rejects_exactly(t, theError, reader1.closed),
+ promise_rejects_exactly(t, theError, reader1.read())
+ );
+
+ assert_throws_js(TypeError, () => rs.getReader(), 'trying to get another reader before erroring should throw');
+
+ controller.error(theError);
+
+ reader1.releaseLock();
+
+ const reader2 = rs.getReader();
+
+ promiseAsserts.push(
+ promise_rejects_exactly(t, theError, reader2.closed),
+ promise_rejects_exactly(t, theError, reader2.read())
+ );
+
+ return Promise.all(promiseAsserts);
+
+}, 'Getting a second reader after erroring the stream and releasing the reader should succeed');
+
+promise_test(t => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const promise = rs.getReader().closed.then(
+ t.unreached_func('closed promise should not be fulfilled when stream is errored'),
+ err => {
+ assert_equals(err, undefined, 'passed error should be undefined as it was');
+ }
+ );
+
+ controller.error();
+ return promise;
+
+}, 'ReadableStreamDefaultReader closed promise should be rejected with undefined if that is the error');
+
+
+promise_test(t => {
+
+ const rs = new ReadableStream({
+ start() {
+ return Promise.reject();
+ }
+ });
+
+ return rs.getReader().read().then(
+ t.unreached_func('read promise should not be fulfilled when stream is errored'),
+ err => {
+ assert_equals(err, undefined, 'passed error should be undefined as it was');
+ }
+ );
+
+}, 'ReadableStreamDefaultReader: if start rejects with no parameter, it should error the stream with an undefined ' +
+ 'error');
+
+promise_test(t => {
+
+ const theError = { name: 'unique string' };
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const promise = promise_rejects_exactly(t, theError, rs.getReader().closed);
+
+ controller.error(theError);
+ return promise;
+
+}, 'Erroring a ReadableStream after checking closed should reject ReadableStreamDefaultReader closed promise');
+
+promise_test(t => {
+
+ const theError = { name: 'unique string' };
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ controller.error(theError);
+
+ // Let's call getReader twice for extra test coverage of this code path.
+ rs.getReader().releaseLock();
+
+ return promise_rejects_exactly(t, theError, rs.getReader().closed);
+
+}, 'Erroring a ReadableStream before checking closed should reject ReadableStreamDefaultReader closed promise');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ const reader = rs.getReader();
+
+ const promise = Promise.all([
+ reader.read().then(result => {
+ assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (1)');
+ }),
+ reader.read().then(result => {
+ assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (2)');
+ }),
+ reader.closed
+ ]);
+
+ controller.close();
+ return promise;
+
+}, 'Reading twice on a stream that gets closed');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ controller.close();
+ const reader = rs.getReader();
+
+ return Promise.all([
+ reader.read().then(result => {
+ assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (1)');
+ }),
+ reader.read().then(result => {
+ assert_object_equals(result, { value: undefined, done: true }, 'read() should fulfill with close (2)');
+ }),
+ reader.closed
+ ]);
+
+}, 'Reading twice on a closed stream');
+
+promise_test(t => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const myError = { name: 'mashed potatoes' };
+ controller.error(myError);
+
+ const reader = rs.getReader();
+
+ return Promise.all([
+ promise_rejects_exactly(t, myError, reader.read()),
+ promise_rejects_exactly(t, myError, reader.read()),
+ promise_rejects_exactly(t, myError, reader.closed)
+ ]);
+
+}, 'Reading twice on an errored stream');
+
+promise_test(t => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const myError = { name: 'mashed potatoes' };
+ const reader = rs.getReader();
+
+ const promise = Promise.all([
+ promise_rejects_exactly(t, myError, reader.read()),
+ promise_rejects_exactly(t, myError, reader.read()),
+ promise_rejects_exactly(t, myError, reader.closed)
+ ]);
+
+ controller.error(myError);
+ return promise;
+
+}, 'Reading twice on a stream that gets errored');
+
+test(() => {
+ const rs = new ReadableStream();
+ let toStringCalled = false;
+ const mode = {
+ toString() {
+ toStringCalled = true;
+ return '';
+ }
+ };
+ assert_throws_js(TypeError, () => rs.getReader({ mode }), 'getReader() should throw');
+ assert_true(toStringCalled, 'toString() should be called');
+}, 'getReader() should call ToString() on mode');
+
+promise_test(() => {
+ const rs = new ReadableStream({
+ pull(controller) {
+ controller.close();
+ }
+ });
+
+ const reader = rs.getReader();
+ return reader.read().then(() => {
+ // The test passes if releaseLock() does not throw.
+ reader.releaseLock();
+ });
+}, 'controller.close() should clear the list of pending read requests');
+
+promise_test(t => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const reader1 = rs.getReader();
+ const promise1 = promise_rejects_js(t, TypeError, reader1.read(), 'read() from reader1 should reject when reader1 is released');
+ reader1.releaseLock();
+
+ controller.enqueue('a');
+
+ const reader2 = rs.getReader();
+ const promise2 = reader2.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'read() from reader2 should resolve with enqueued chunk');
+ })
+ reader2.releaseLock();
+
+ return Promise.all([promise1, promise2]);
+
+}, 'Second reader can read chunks after first reader was released with pending read requests');
diff --git a/testing/web-platform/tests/streams/readable-streams/floating-point-total-queue-size.any.js b/testing/web-platform/tests/streams/readable-streams/floating-point-total-queue-size.any.js
new file mode 100644
index 0000000000..50cca3d951
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/floating-point-total-queue-size.any.js
@@ -0,0 +1,116 @@
+// META: global=window,worker
+'use strict';
+
+// Due to the limitations of floating-point precision, the calculation of desiredSize sometimes gives different answers
+// than adding up the items in the queue would. It is important that implementations give the same result in these edge
+// cases so that developers do not come to depend on non-standard behaviour. See
+// https://github.com/whatwg/streams/issues/582 and linked issues for further discussion.
+
+promise_test(() => {
+ const { reader, controller } = setupTestStream();
+
+ controller.enqueue(2);
+ assert_equals(controller.desiredSize, 0 - 2, 'desiredSize must be -2 after enqueueing such a chunk');
+
+ controller.enqueue(Number.MAX_SAFE_INTEGER);
+ assert_equals(controller.desiredSize, 0 - Number.MAX_SAFE_INTEGER - 2,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)');
+
+ return reader.read().then(() => {
+ assert_equals(controller.desiredSize, 0 - Number.MAX_SAFE_INTEGER - 2 + 2,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)');
+
+ return reader.read();
+ }).then(() => {
+ assert_equals(controller.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative');
+ });
+}, 'Floating point arithmetic must manifest near NUMBER.MAX_SAFE_INTEGER (total ends up positive)');
+
+promise_test(() => {
+ const { reader, controller } = setupTestStream();
+
+ controller.enqueue(1e-16);
+ assert_equals(controller.desiredSize, 0 - 1e-16, 'desiredSize must be -1e16 after enqueueing such a chunk');
+
+ controller.enqueue(1);
+ assert_equals(controller.desiredSize, 0 - 1e-16 - 1,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)');
+
+ return reader.read().then(() => {
+ assert_equals(controller.desiredSize, 0 - 1e-16 - 1 + 1e-16,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)');
+
+ return reader.read();
+ }).then(() => {
+ assert_equals(controller.desiredSize, 0, '[[queueTotalSize]] must clamp to 0 if it becomes negative');
+ });
+}, 'Floating point arithmetic must manifest near 0 (total ends up positive, but clamped)');
+
+promise_test(() => {
+ const { reader, controller } = setupTestStream();
+
+ controller.enqueue(1e-16);
+ assert_equals(controller.desiredSize, 0 - 1e-16, 'desiredSize must be -2e16 after enqueueing such a chunk');
+
+ controller.enqueue(1);
+ assert_equals(controller.desiredSize, 0 - 1e-16 - 1,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)');
+
+ controller.enqueue(2e-16);
+ assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a third chunk)');
+
+ return reader.read().then(() => {
+ assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)');
+
+ return reader.read();
+ }).then(() => {
+ assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16 + 1,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a second chunk)');
+
+ return reader.read();
+ }).then(() => {
+ assert_equals(controller.desiredSize, 0 - 1e-16 - 1 - 2e-16 + 1e-16 + 1 + 2e-16,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a third chunk)');
+ });
+}, 'Floating point arithmetic must manifest near 0 (total ends up positive, and not clamped)');
+
+promise_test(() => {
+ const { reader, controller } = setupTestStream();
+
+ controller.enqueue(2e-16);
+ assert_equals(controller.desiredSize, 0 - 2e-16, 'desiredSize must be -2e16 after enqueueing such a chunk');
+
+ controller.enqueue(1);
+ assert_equals(controller.desiredSize, 0 - 2e-16 - 1,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (adding a second chunk)');
+
+ return reader.read().then(() => {
+ assert_equals(controller.desiredSize, 0 - 2e-16 - 1 + 2e-16,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a chunk)');
+
+ return reader.read();
+ }).then(() => {
+ assert_equals(controller.desiredSize, 0,
+ 'desiredSize must be calculated using double-precision floating-point arithmetic (subtracting a second chunk)');
+ });
+}, 'Floating point arithmetic must manifest near 0 (total ends up zero)');
+
+function setupTestStream() {
+ const strategy = {
+ size(x) {
+ return x;
+ },
+ highWaterMark: 0
+ };
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, strategy);
+
+ return { reader: rs.getReader(), controller };
+}
diff --git a/testing/web-platform/tests/streams/readable-streams/garbage-collection.any.js b/testing/web-platform/tests/streams/readable-streams/garbage-collection.any.js
new file mode 100644
index 0000000000..e578176777
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/garbage-collection.any.js
@@ -0,0 +1,71 @@
+// META: global=window,worker
+// META: script=../resources/test-utils.js
+// META: script=/common/gc.js
+'use strict';
+
+promise_test(async () => {
+
+ let controller;
+ new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ await garbageCollect();
+
+ return delay(50).then(() => {
+ controller.close();
+ assert_throws_js(TypeError, () => controller.close(), 'close should throw a TypeError the second time');
+ controller.error();
+ });
+
+}, 'ReadableStreamController methods should continue working properly when scripts lose their reference to the ' +
+ 'readable stream');
+
+promise_test(async () => {
+
+ let controller;
+
+ const closedPromise = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }).getReader().closed;
+
+ await garbageCollect();
+
+ return delay(50).then(() => controller.close()).then(() => closedPromise);
+
+}, 'ReadableStream closed promise should fulfill even if the stream and reader JS references are lost');
+
+promise_test(async t => {
+
+ const theError = new Error('boo');
+ let controller;
+
+ const closedPromise = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }).getReader().closed;
+
+ await garbageCollect();
+
+ return delay(50).then(() => controller.error(theError))
+ .then(() => promise_rejects_exactly(t, theError, closedPromise));
+
+}, 'ReadableStream closed promise should reject even if stream and reader JS references are lost');
+
+promise_test(async () => {
+
+ const rs = new ReadableStream({});
+
+ rs.getReader();
+
+ await garbageCollect();
+
+ return delay(50).then(() => assert_throws_js(TypeError, () => rs.getReader(),
+ 'old reader should still be locking the stream even after garbage collection'));
+
+}, 'Garbage-collecting a ReadableStreamDefaultReader should not unlock its stream');
diff --git a/testing/web-platform/tests/streams/readable-streams/general.any.js b/testing/web-platform/tests/streams/readable-streams/general.any.js
new file mode 100644
index 0000000000..2a32b27943
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/general.any.js
@@ -0,0 +1,840 @@
+// META: global=window,worker
+// META: script=../resources/test-utils.js
+// META: script=../resources/rs-utils.js
+'use strict';
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+test(() => {
+
+ new ReadableStream(); // ReadableStream constructed with no parameters
+ new ReadableStream({ }); // ReadableStream constructed with an empty object as parameter
+ new ReadableStream({ type: undefined }); // ReadableStream constructed with undefined type
+ new ReadableStream(undefined); // ReadableStream constructed with undefined as parameter
+
+ let x;
+ new ReadableStream(x); // ReadableStream constructed with an undefined variable as parameter
+
+}, 'ReadableStream can be constructed with no errors');
+
+test(() => {
+
+ assert_throws_js(TypeError, () => new ReadableStream(null), 'constructor should throw when the source is null');
+
+}, 'ReadableStream can\'t be constructed with garbage');
+
+test(() => {
+
+ assert_throws_js(TypeError, () => new ReadableStream({ type: null }),
+ 'constructor should throw when the type is null');
+ assert_throws_js(TypeError, () => new ReadableStream({ type: '' }),
+ 'constructor should throw when the type is empty string');
+ assert_throws_js(TypeError, () => new ReadableStream({ type: 'asdf' }),
+ 'constructor should throw when the type is asdf');
+ assert_throws_exactly(
+ error1,
+ () => new ReadableStream({ type: { get toString() { throw error1; } } }),
+ 'constructor should throw when ToString() throws'
+ );
+ assert_throws_exactly(
+ error1,
+ () => new ReadableStream({ type: { toString() { throw error1; } } }),
+ 'constructor should throw when ToString() throws'
+ );
+
+}, 'ReadableStream can\'t be constructed with an invalid type');
+
+test(() => {
+
+ assert_throws_js(TypeError, () => {
+ new ReadableStream({ start: 'potato' });
+ }, 'constructor should throw when start is not a function');
+
+}, 'ReadableStream constructor should throw for non-function start arguments');
+
+test(() => {
+
+ assert_throws_js(TypeError, () => new ReadableStream({ cancel: '2' }), 'constructor should throw');
+
+}, 'ReadableStream constructor will not tolerate initial garbage as cancel argument');
+
+test(() => {
+
+ assert_throws_js(TypeError, () => new ReadableStream({ pull: { } }), 'constructor should throw');
+
+}, 'ReadableStream constructor will not tolerate initial garbage as pull argument');
+
+test(() => {
+
+ let startCalled = false;
+
+ const source = {
+ start() {
+ assert_equals(this, source, 'source is this during start');
+ startCalled = true;
+ }
+ };
+
+ new ReadableStream(source);
+ assert_true(startCalled);
+
+}, 'ReadableStream start should be called with the proper thisArg');
+
+test(() => {
+
+ let startCalled = false;
+ const source = {
+ start(controller) {
+ const properties = ['close', 'constructor', 'desiredSize', 'enqueue', 'error'];
+ assert_array_equals(Object.getOwnPropertyNames(Object.getPrototypeOf(controller)).sort(), properties,
+ 'prototype should have the right properties');
+
+ controller.test = '';
+ assert_array_equals(Object.getOwnPropertyNames(Object.getPrototypeOf(controller)).sort(), properties,
+ 'prototype should still have the right properties');
+ assert_not_equals(Object.getOwnPropertyNames(controller).indexOf('test'), -1,
+ '"test" should be a property of the controller');
+
+ startCalled = true;
+ }
+ };
+
+ new ReadableStream(source);
+ assert_true(startCalled);
+
+}, 'ReadableStream start controller parameter should be extensible');
+
+test(() => {
+ (new ReadableStream()).getReader(undefined);
+ (new ReadableStream()).getReader({});
+ (new ReadableStream()).getReader({ mode: undefined, notmode: 'ignored' });
+ assert_throws_js(TypeError, () => (new ReadableStream()).getReader({ mode: 'potato' }));
+}, 'default ReadableStream getReader() should only accept mode:undefined');
+
+promise_test(() => {
+
+ function SimpleStreamSource() {}
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+ SimpleStreamSource.prototype = {
+ start: resolve
+ };
+
+ new ReadableStream(new SimpleStreamSource());
+ return promise;
+
+}, 'ReadableStream should be able to call start method within prototype chain of its source');
+
+promise_test(() => {
+
+ const rs = new ReadableStream({
+ start(c) {
+ return delay(5).then(() => {
+ c.enqueue('a');
+ c.close();
+ });
+ }
+ });
+
+ const reader = rs.getReader();
+ return reader.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'value read should be the one enqueued');
+ return reader.closed;
+ });
+
+}, 'ReadableStream start should be able to return a promise');
+
+promise_test(() => {
+
+ const theError = new Error('rejected!');
+ const rs = new ReadableStream({
+ start() {
+ return delay(1).then(() => {
+ throw theError;
+ });
+ }
+ });
+
+ return rs.getReader().closed.then(() => {
+ assert_unreached('closed promise should be rejected');
+ }, e => {
+ assert_equals(e, theError, 'promise should be rejected with the same error');
+ });
+
+}, 'ReadableStream start should be able to return a promise and reject it');
+
+promise_test(() => {
+
+ const objects = [
+ { potato: 'Give me more!' },
+ 'test',
+ 1
+ ];
+
+ const rs = new ReadableStream({
+ start(c) {
+ for (const o of objects) {
+ c.enqueue(o);
+ }
+ c.close();
+ }
+ });
+
+ const reader = rs.getReader();
+
+ return Promise.all([reader.read(), reader.read(), reader.read(), reader.closed]).then(r => {
+ assert_object_equals(r[0], { value: objects[0], done: false }, 'value read should be the one enqueued');
+ assert_object_equals(r[1], { value: objects[1], done: false }, 'value read should be the one enqueued');
+ assert_object_equals(r[2], { value: objects[2], done: false }, 'value read should be the one enqueued');
+ });
+
+}, 'ReadableStream should be able to enqueue different objects.');
+
+promise_test(() => {
+
+ const error = new Error('pull failure');
+ const rs = new ReadableStream({
+ pull() {
+ return Promise.reject(error);
+ }
+ });
+
+ const reader = rs.getReader();
+
+ let closed = false;
+ let read = false;
+
+ return Promise.all([
+ reader.closed.then(() => {
+ assert_unreached('closed should be rejected');
+ }, e => {
+ closed = true;
+ assert_false(read);
+ assert_equals(e, error, 'closed should be rejected with the thrown error');
+ }),
+ reader.read().then(() => {
+ assert_unreached('read() should be rejected');
+ }, e => {
+ read = true;
+ assert_true(closed);
+ assert_equals(e, error, 'read() should be rejected with the thrown error');
+ })
+ ]);
+
+}, 'ReadableStream: if pull rejects, it should error the stream');
+
+promise_test(() => {
+
+ let pullCount = 0;
+
+ new ReadableStream({
+ pull() {
+ pullCount++;
+ }
+ });
+
+ return flushAsyncEvents().then(() => {
+ assert_equals(pullCount, 1, 'pull should be called once start finishes');
+ return delay(10);
+ }).then(() => {
+ assert_equals(pullCount, 1, 'pull should be called exactly once');
+ });
+
+}, 'ReadableStream: should only call pull once upon starting the stream');
+
+promise_test(() => {
+
+ let pullCount = 0;
+
+ const rs = new ReadableStream({
+ pull(c) {
+ // Don't enqueue immediately after start. We want the stream to be empty when we call .read() on it.
+ if (pullCount > 0) {
+ c.enqueue(pullCount);
+ }
+ ++pullCount;
+ }
+ });
+
+ return flushAsyncEvents().then(() => {
+ assert_equals(pullCount, 1, 'pull should be called once start finishes');
+ }).then(() => {
+ const reader = rs.getReader();
+ const read = reader.read();
+ assert_equals(pullCount, 2, 'pull should be called when read is called');
+ return read;
+ }).then(result => {
+ assert_equals(pullCount, 3, 'pull should be called again in reaction to calling read');
+ assert_object_equals(result, { value: 1, done: false }, 'the result read should be the one enqueued');
+ });
+
+}, 'ReadableStream: should call pull when trying to read from a started, empty stream');
+
+promise_test(() => {
+
+ let pullCount = 0;
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ },
+ pull() {
+ pullCount++;
+ }
+ });
+
+ const read = rs.getReader().read();
+ assert_equals(pullCount, 0, 'calling read() should not cause pull to be called yet');
+
+ return flushAsyncEvents().then(() => {
+ assert_equals(pullCount, 1, 'pull should be called once start finishes');
+ return read;
+ }).then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'first read() should return first chunk');
+ assert_equals(pullCount, 1, 'pull should not have been called again');
+ return delay(10);
+ }).then(() => {
+ assert_equals(pullCount, 1, 'pull should be called exactly once');
+ });
+
+}, 'ReadableStream: should only call pull once on a non-empty stream read from before start fulfills');
+
+promise_test(() => {
+
+ let pullCount = 0;
+ const startPromise = Promise.resolve();
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ },
+ pull() {
+ pullCount++;
+ }
+ });
+
+ return flushAsyncEvents().then(() => {
+ assert_equals(pullCount, 0, 'pull should not be called once start finishes, since the queue is full');
+
+ const read = rs.getReader().read();
+ assert_equals(pullCount, 1, 'calling read() should cause pull to be called immediately');
+ return read;
+ }).then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'first read() should return first chunk');
+ return delay(10);
+ }).then(() => {
+ assert_equals(pullCount, 1, 'pull should be called exactly once');
+ });
+
+}, 'ReadableStream: should only call pull once on a non-empty stream read from after start fulfills');
+
+promise_test(() => {
+
+ let pullCount = 0;
+ let controller;
+
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ },
+ pull() {
+ ++pullCount;
+ }
+ });
+
+ const reader = rs.getReader();
+ return flushAsyncEvents().then(() => {
+ assert_equals(pullCount, 1, 'pull should have been called once by the time the stream starts');
+
+ controller.enqueue('a');
+ assert_equals(pullCount, 1, 'pull should not have been called again after enqueue');
+
+ return reader.read();
+ }).then(() => {
+ assert_equals(pullCount, 2, 'pull should have been called again after read');
+
+ return delay(10);
+ }).then(() => {
+ assert_equals(pullCount, 2, 'pull should be called exactly twice');
+ });
+}, 'ReadableStream: should call pull in reaction to read()ing the last chunk, if not draining');
+
+promise_test(() => {
+
+ let pullCount = 0;
+ let controller;
+
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ },
+ pull() {
+ ++pullCount;
+ }
+ });
+
+ const reader = rs.getReader();
+
+ return flushAsyncEvents().then(() => {
+ assert_equals(pullCount, 1, 'pull should have been called once by the time the stream starts');
+
+ controller.enqueue('a');
+ assert_equals(pullCount, 1, 'pull should not have been called again after enqueue');
+
+ controller.close();
+
+ return reader.read();
+ }).then(() => {
+ assert_equals(pullCount, 1, 'pull should not have been called a second time after read');
+
+ return delay(10);
+ }).then(() => {
+ assert_equals(pullCount, 1, 'pull should be called exactly once');
+ });
+
+}, 'ReadableStream: should not call pull() in reaction to read()ing the last chunk, if draining');
+
+promise_test(() => {
+
+ let resolve;
+ let returnedPromise;
+ let timesCalled = 0;
+
+ const rs = new ReadableStream({
+ pull(c) {
+ c.enqueue(++timesCalled);
+ returnedPromise = new Promise(r => resolve = r);
+ return returnedPromise;
+ }
+ });
+ const reader = rs.getReader();
+
+ return reader.read()
+ .then(result1 => {
+ assert_equals(timesCalled, 1,
+ 'pull should have been called once after start, but not yet have been called a second time');
+ assert_object_equals(result1, { value: 1, done: false }, 'read() should fulfill with the enqueued value');
+
+ return delay(10);
+ }).then(() => {
+ assert_equals(timesCalled, 1, 'after 10 ms, pull should still only have been called once');
+
+ resolve();
+ return returnedPromise;
+ }).then(() => {
+ assert_equals(timesCalled, 2,
+ 'after the promise returned by pull is fulfilled, pull should be called a second time');
+ });
+
+}, 'ReadableStream: should not call pull until the previous pull call\'s promise fulfills');
+
+promise_test(() => {
+
+ let timesCalled = 0;
+
+ const rs = new ReadableStream(
+ {
+ start(c) {
+ c.enqueue('a');
+ c.enqueue('b');
+ c.enqueue('c');
+ },
+ pull() {
+ ++timesCalled;
+ }
+ },
+ {
+ size() {
+ return 1;
+ },
+ highWaterMark: Infinity
+ }
+ );
+ const reader = rs.getReader();
+
+ return flushAsyncEvents().then(() => {
+ return reader.read();
+ }).then(result1 => {
+ assert_object_equals(result1, { value: 'a', done: false }, 'first chunk should be as expected');
+
+ return reader.read();
+ }).then(result2 => {
+ assert_object_equals(result2, { value: 'b', done: false }, 'second chunk should be as expected');
+
+ return reader.read();
+ }).then(result3 => {
+ assert_object_equals(result3, { value: 'c', done: false }, 'third chunk should be as expected');
+
+ return delay(10);
+ }).then(() => {
+ // Once for after start, and once for every read.
+ assert_equals(timesCalled, 4, 'pull() should be called exactly four times');
+ });
+
+}, 'ReadableStream: should pull after start, and after every read');
+
+promise_test(() => {
+
+ let timesCalled = 0;
+ const startPromise = Promise.resolve();
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ c.close();
+ return startPromise;
+ },
+ pull() {
+ ++timesCalled;
+ }
+ });
+
+ const reader = rs.getReader();
+ return startPromise.then(() => {
+ assert_equals(timesCalled, 0, 'after start finishes, pull should not have been called');
+
+ return reader.read();
+ }).then(() => {
+ assert_equals(timesCalled, 0, 'reading should not have triggered a pull call');
+
+ return reader.closed;
+ }).then(() => {
+ assert_equals(timesCalled, 0, 'stream should have closed with still no calls to pull');
+ });
+
+}, 'ReadableStream: should not call pull after start if the stream is now closed');
+
+promise_test(() => {
+
+ let timesCalled = 0;
+ let resolve;
+ const ready = new Promise(r => resolve = r);
+
+ new ReadableStream(
+ {
+ start() {},
+ pull(c) {
+ c.enqueue(++timesCalled);
+
+ if (timesCalled === 4) {
+ resolve();
+ }
+ }
+ },
+ {
+ size() {
+ return 1;
+ },
+ highWaterMark: 4
+ }
+ );
+
+ return ready.then(() => {
+ // after start: size = 0, pull()
+ // after enqueue(1): size = 1, pull()
+ // after enqueue(2): size = 2, pull()
+ // after enqueue(3): size = 3, pull()
+ // after enqueue(4): size = 4, do not pull
+ assert_equals(timesCalled, 4, 'pull() should have been called four times');
+ });
+
+}, 'ReadableStream: should call pull after enqueueing from inside pull (with no read requests), if strategy allows');
+
+promise_test(() => {
+
+ let pullCalled = false;
+
+ const rs = new ReadableStream({
+ pull(c) {
+ pullCalled = true;
+ c.close();
+ }
+ });
+
+ const reader = rs.getReader();
+ return reader.closed.then(() => {
+ assert_true(pullCalled);
+ });
+
+}, 'ReadableStream pull should be able to close a stream.');
+
+promise_test(t => {
+
+ const controllerError = { name: 'controller error' };
+
+ const rs = new ReadableStream({
+ pull(c) {
+ c.error(controllerError);
+ }
+ });
+
+ return promise_rejects_exactly(t, controllerError, rs.getReader().closed);
+
+}, 'ReadableStream pull should be able to error a stream.');
+
+promise_test(t => {
+
+ const controllerError = { name: 'controller error' };
+ const thrownError = { name: 'thrown error' };
+
+ const rs = new ReadableStream({
+ pull(c) {
+ c.error(controllerError);
+ throw thrownError;
+ }
+ });
+
+ return promise_rejects_exactly(t, controllerError, rs.getReader().closed);
+
+}, 'ReadableStream pull should be able to error a stream and throw.');
+
+test(() => {
+
+ let startCalled = false;
+
+ new ReadableStream({
+ start(c) {
+ assert_equals(c.enqueue('a'), undefined, 'the first enqueue should return undefined');
+ c.close();
+
+ assert_throws_js(TypeError, () => c.enqueue('b'), 'enqueue after close should throw a TypeError');
+ startCalled = true;
+ }
+ });
+
+ assert_true(startCalled);
+
+}, 'ReadableStream: enqueue should throw when the stream is readable but draining');
+
+test(() => {
+
+ let startCalled = false;
+
+ new ReadableStream({
+ start(c) {
+ c.close();
+
+ assert_throws_js(TypeError, () => c.enqueue('a'), 'enqueue after close should throw a TypeError');
+ startCalled = true;
+ }
+ });
+
+ assert_true(startCalled);
+
+}, 'ReadableStream: enqueue should throw when the stream is closed');
+
+promise_test(() => {
+
+ let startCalled = 0;
+ let pullCalled = 0;
+ let cancelCalled = 0;
+
+ /* eslint-disable no-use-before-define */
+ class Source {
+ start(c) {
+ startCalled++;
+ assert_equals(this, theSource, 'start() should be called with the correct this');
+ c.enqueue('a');
+ }
+
+ pull() {
+ pullCalled++;
+ assert_equals(this, theSource, 'pull() should be called with the correct this');
+ }
+
+ cancel() {
+ cancelCalled++;
+ assert_equals(this, theSource, 'cancel() should be called with the correct this');
+ }
+ }
+ /* eslint-enable no-use-before-define */
+
+ const theSource = new Source();
+ theSource.debugName = 'the source object passed to the constructor'; // makes test failures easier to diagnose
+
+ const rs = new ReadableStream(theSource);
+ const reader = rs.getReader();
+
+ return reader.read().then(() => {
+ reader.releaseLock();
+ rs.cancel();
+ assert_equals(startCalled, 1);
+ assert_equals(pullCalled, 1);
+ assert_equals(cancelCalled, 1);
+ return rs.getReader().closed;
+ });
+
+}, 'ReadableStream: should call underlying source methods as methods');
+
+test(() => {
+ new ReadableStream({
+ start(c) {
+ assert_equals(c.desiredSize, 10, 'desiredSize must start at highWaterMark');
+ c.close();
+ assert_equals(c.desiredSize, 0, 'after closing, desiredSize must be 0');
+ }
+ }, {
+ highWaterMark: 10
+ });
+}, 'ReadableStream: desiredSize when closed');
+
+test(() => {
+ new ReadableStream({
+ start(c) {
+ assert_equals(c.desiredSize, 10, 'desiredSize must start at highWaterMark');
+ c.error();
+ assert_equals(c.desiredSize, null, 'after erroring, desiredSize must be null');
+ }
+ }, {
+ highWaterMark: 10
+ });
+}, 'ReadableStream: desiredSize when errored');
+
+test(() => {
+ class Subclass extends ReadableStream {
+ extraFunction() {
+ return true;
+ }
+ }
+ assert_equals(
+ Object.getPrototypeOf(Subclass.prototype), ReadableStream.prototype,
+ 'Subclass.prototype\'s prototype should be ReadableStream.prototype');
+ assert_equals(Object.getPrototypeOf(Subclass), ReadableStream,
+ 'Subclass\'s prototype should be ReadableStream');
+ const sub = new Subclass();
+ assert_true(sub instanceof ReadableStream,
+ 'Subclass object should be an instance of ReadableStream');
+ assert_true(sub instanceof Subclass,
+ 'Subclass object should be an instance of Subclass');
+ const lockedGetter = Object.getOwnPropertyDescriptor(
+ ReadableStream.prototype, 'locked').get;
+ assert_equals(lockedGetter.call(sub), sub.locked,
+ 'Subclass object should pass brand check');
+ assert_true(sub.extraFunction(),
+ 'extraFunction() should be present on Subclass object');
+}, 'Subclassing ReadableStream should work');
+
+test(() => {
+
+ let startCalled = false;
+ new ReadableStream({
+ start(c) {
+ assert_equals(c.desiredSize, 1);
+ c.enqueue('a');
+ assert_equals(c.desiredSize, 0);
+ c.enqueue('b');
+ assert_equals(c.desiredSize, -1);
+ c.enqueue('c');
+ assert_equals(c.desiredSize, -2);
+ c.enqueue('d');
+ assert_equals(c.desiredSize, -3);
+ c.enqueue('e');
+ startCalled = true;
+ }
+ });
+
+ assert_true(startCalled);
+
+}, 'ReadableStream strategies: the default strategy should give desiredSize of 1 to start, decreasing by 1 per enqueue');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ const reader = rs.getReader();
+
+ assert_equals(controller.desiredSize, 1, 'desiredSize should start at 1');
+ controller.enqueue('a');
+ assert_equals(controller.desiredSize, 0, 'desiredSize should decrease to 0 after first enqueue');
+
+ return reader.read().then(result1 => {
+ assert_object_equals(result1, { value: 'a', done: false }, 'first chunk read should be correct');
+
+ assert_equals(controller.desiredSize, 1, 'desiredSize should go up to 1 after the first read');
+ controller.enqueue('b');
+ assert_equals(controller.desiredSize, 0, 'desiredSize should go down to 0 after the second enqueue');
+
+ return reader.read();
+ }).then(result2 => {
+ assert_object_equals(result2, { value: 'b', done: false }, 'second chunk read should be correct');
+
+ assert_equals(controller.desiredSize, 1, 'desiredSize should go up to 1 after the second read');
+ controller.enqueue('c');
+ assert_equals(controller.desiredSize, 0, 'desiredSize should go down to 0 after the third enqueue');
+
+ return reader.read();
+ }).then(result3 => {
+ assert_object_equals(result3, { value: 'c', done: false }, 'third chunk read should be correct');
+
+ assert_equals(controller.desiredSize, 1, 'desiredSize should go up to 1 after the third read');
+ controller.enqueue('d');
+ assert_equals(controller.desiredSize, 0, 'desiredSize should go down to 0 after the fourth enqueue');
+ });
+
+}, 'ReadableStream strategies: the default strategy should continue giving desiredSize of 1 if the chunks are read immediately');
+
+promise_test(t => {
+
+ const randomSource = new RandomPushSource(8);
+
+ const rs = new ReadableStream({
+ start(c) {
+ assert_equals(typeof c, 'object', 'c should be an object in start');
+ assert_equals(typeof c.enqueue, 'function', 'enqueue should be a function in start');
+ assert_equals(typeof c.close, 'function', 'close should be a function in start');
+ assert_equals(typeof c.error, 'function', 'error should be a function in start');
+
+ randomSource.ondata = t.step_func(chunk => {
+ if (!c.enqueue(chunk) <= 0) {
+ randomSource.readStop();
+ }
+ });
+
+ randomSource.onend = c.close.bind(c);
+ randomSource.onerror = c.error.bind(c);
+ },
+
+ pull(c) {
+ assert_equals(typeof c, 'object', 'c should be an object in pull');
+ assert_equals(typeof c.enqueue, 'function', 'enqueue should be a function in pull');
+ assert_equals(typeof c.close, 'function', 'close should be a function in pull');
+
+ randomSource.readStart();
+ }
+ });
+
+ return readableStreamToArray(rs).then(chunks => {
+ assert_equals(chunks.length, 8, '8 chunks should be read');
+ for (const chunk of chunks) {
+ assert_equals(chunk.length, 128, 'chunk should have 128 bytes');
+ }
+ });
+
+}, 'ReadableStream integration test: adapting a random push source');
+
+promise_test(() => {
+
+ const rs = sequentialReadableStream(10);
+
+ return readableStreamToArray(rs).then(chunks => {
+ assert_true(rs.source.closed, 'source should be closed after all chunks are read');
+ assert_array_equals(chunks, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'the expected 10 chunks should be read');
+ });
+
+}, 'ReadableStream integration test: adapting a sync pull source');
+
+promise_test(() => {
+
+ const rs = sequentialReadableStream(10, { async: true });
+
+ return readableStreamToArray(rs).then(chunks => {
+ assert_true(rs.source.closed, 'source should be closed after all chunks are read');
+ assert_array_equals(chunks, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 'the expected 10 chunks should be read');
+ });
+
+}, 'ReadableStream integration test: adapting an async pull source');
diff --git a/testing/web-platform/tests/streams/readable-streams/global.html b/testing/web-platform/tests/streams/readable-streams/global.html
new file mode 100644
index 0000000000..08665d318e
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/global.html
@@ -0,0 +1,162 @@
+<!doctype html>
+<meta charset="utf-8">
+<title>Ensure Stream objects are created in expected globals. </title>
+
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<body></body>
+<script>
+// These tests are loosely derived from Gecko's readable-stream-globals.js,
+// which is a test case designed around the JS Streams implementation.
+//
+// Unlike in JS Streams, where function calls switch realms and change
+// the resulting global of the resulting objects, in WebIDL streams,
+// the global of an object is (currently underspecified, but) intended
+// to be the "Relevant Global" of the 'this' object.
+//
+// See:
+// https://html.spec.whatwg.org/multipage/webappapis.html#relevant
+// https://github.com/whatwg/streams/issues/1213
+"use strict"
+
+const iframe = document.createElement("iframe")
+document.body.append(iframe)
+
+const otherGlobal = iframe.contentWindow;
+const OtherReadableStream = otherGlobal.ReadableStream
+const OtherReadableStreamDefaultReader = otherGlobal.ReadableStreamDefaultReader;
+const OtherReadableStreamDefaultController = otherGlobal.ReadableStreamDefaultController;
+
+promise_test(async () => {
+
+ // Controllers
+ let controller;
+ let otherController;
+
+ // Get Stream Prototypes and controllers.
+ let streamController;
+ let stream = new ReadableStream({start(c) { streamController = c; }});
+
+ const callReaderThisGlobal = OtherReadableStream.prototype.getReader.call(stream);
+ const newReaderOtherGlobal = new OtherReadableStreamDefaultReader(new ReadableStream());
+
+ // Relevant Global Checking.
+ assert_equals(callReaderThisGlobal instanceof ReadableStreamDefaultReader, true, "reader was created in this global (.call)");
+ assert_equals(newReaderOtherGlobal instanceof ReadableStreamDefaultReader, false, "reader was created in other global (new)");
+
+ assert_equals(callReaderThisGlobal instanceof OtherReadableStreamDefaultReader, false, "reader isn't coming from other global (.call)" );
+ assert_equals(newReaderOtherGlobal instanceof OtherReadableStreamDefaultReader, true, "reader isn't coming from other global (new)");
+
+ assert_equals(otherController instanceof ReadableStreamDefaultController, false, "otherController should come from other gloal")
+
+
+ const request = callReaderThisGlobal.read();
+ assert_equals(request instanceof Promise, true, "Promise comes from this global");
+
+ streamController.close();
+ const requestResult = await request;
+ assert_equals(requestResult instanceof Object, true, "returned object comes from this global");
+}, "Stream objects created in expected globals")
+
+promise_test(async () => {
+ const stream = new ReadableStream();
+ const otherReader = new OtherReadableStreamDefaultReader(stream);
+ const cancelPromise = ReadableStreamDefaultReader.prototype.cancel.call(otherReader);
+ assert_equals(cancelPromise instanceof Promise, true, "Cancel promise comes from the same global as the stream");
+ assert_equals(await cancelPromise, undefined, "Cancel promise resolves to undefined");
+}, "Cancel promise is created in same global as stream")
+
+// Refresh the streams and controllers.
+function getFreshInstances() {
+ let controller;
+ let otherController;
+ let stream = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ new OtherReadableStream({
+ start(c) {
+ otherController = c;
+ }
+ });
+
+ return {stream, controller, otherController}
+}
+
+
+promise_test(async () => {
+ // Test closed promise on reader from another global (connected to a this-global stream)
+ const {stream, controller, otherController} = getFreshInstances();
+
+ const otherReader = new OtherReadableStreamDefaultReader(stream);
+ const closedPromise = otherReader.closed;
+ assert_equals(closedPromise instanceof otherGlobal.Promise, true, "Closed promise in other global.");
+}, "Closed Promise in correct global");
+
+promise_test(async () => {
+ const {stream, controller, otherController} = getFreshInstances();
+
+ const otherReader = OtherReadableStream.prototype.getReader.call(stream);
+ assert_equals(otherReader instanceof ReadableStreamDefaultReader, true, "Reader comes from this global")
+ const request = otherReader.read();
+ assert_equals(request instanceof Promise, true, "Promise still comes from stream's realm (this realm)");
+ otherController.close.call(controller);
+ assert_equals((await request) instanceof otherGlobal.Object, true, "Object comes from other realm");
+}, "Reader objects in correct global");
+
+
+promise_test(async () => {
+ const {stream, controller, otherController} = getFreshInstances();
+ assert_equals(controller.desiredSize, 1, "Desired size is expected");
+ Object.defineProperty(controller, "desiredSize",
+ Object.getOwnPropertyDescriptor(OtherReadableStreamDefaultController.prototype, "desiredSize"));
+ assert_equals(controller.desiredSize, 1, "Grafting getter from other prototype still returns desired size");
+}, "Desired size can be grafted from one prototype to another");
+
+promise_test(async () => {
+ const {stream, controller, otherController} = getFreshInstances();
+
+ // Make sure the controller close method returns the correct TypeError
+ const enqueuedError = { name: "enqueuedError" };
+ controller.error(enqueuedError);
+
+ assert_throws_js(TypeError, () => controller.close(), "Current Global controller");
+ assert_throws_js(otherGlobal.TypeError, () => otherController.close.call(controller), "Other global controller");
+}, "Closing errored stream throws object in appropriate global")
+
+promise_test(async () => {
+ const {otherController} = getFreshInstances();
+ // We can enqueue chunks from multiple globals
+ const chunk = { name: "chunk" };
+
+ let controller;
+ const stream = new ReadableStream({ start(c) { controller = c; } }, { size() {return 1} });
+ otherController.enqueue.call(controller, chunk);
+ otherController.enqueue.call(controller, new otherGlobal.Uint8Array(10));
+ controller.enqueue(new otherGlobal.Uint8Array(10));
+}, "Can enqueue chunks from multiple globals")
+
+promise_test(async () => {
+ const {stream, controller, otherController} = getFreshInstances();
+ const chunk = { name: "chunk" };
+
+ // We get the correct type errors out of a closed stream.
+ controller.close();
+ assert_throws_js(TypeError, () => controller.enqueue(new otherGlobal.Uint8Array(10)));
+ assert_throws_js(otherGlobal.TypeError, () => otherController.enqueue.call(controller, chunk));
+ assert_throws_js(otherGlobal.TypeError, () => otherController.enqueue.call(controller, new otherGlobal.Uint8Array(10)));
+}, "Correct errors and globals for closed streams");
+
+
+promise_test(async () => {
+ const {stream, controller, otherController} = getFreshInstances();
+ // Branches out of tee are in the correct global
+
+ const [branch1, branch2] = otherGlobal.ReadableStream.prototype.tee.call(stream);
+ assert_equals(branch1 instanceof ReadableStream, true, "Branch created in this global (as stream is in this global)");
+ assert_equals(branch2 instanceof ReadableStream, true, "Branch created in this global (as stream is in this global)");
+}, "Tee Branches in correct global");
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/owning-type-message-port.any.js b/testing/web-platform/tests/streams/readable-streams/owning-type-message-port.any.js
new file mode 100644
index 0000000000..e9961ce042
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/owning-type-message-port.any.js
@@ -0,0 +1,49 @@
+// META: global=window,worker
+// META: script=../resources/test-utils.js
+// META: script=../resources/rs-utils.js
+'use strict';
+
+promise_test(async () => {
+ const channel = new MessageChannel;
+ const port1 = channel.port1;
+ const port2 = channel.port2;
+
+ const source = {
+ start(controller) {
+ controller.enqueue(port1, { transfer : [ port1 ] });
+ },
+ type: 'owning'
+ };
+
+ const stream = new ReadableStream(source);
+
+ const chunk = await stream.getReader().read();
+
+ assert_not_equals(chunk.value, port1);
+
+ let promise = new Promise(resolve => port2.onmessage = e => resolve(e.data));
+ chunk.value.postMessage("toPort2");
+ assert_equals(await promise, "toPort2");
+
+ promise = new Promise(resolve => chunk.value.onmessage = e => resolve(e.data));
+ port2.postMessage("toPort1");
+ assert_equals(await promise, "toPort1");
+}, 'Transferred MessageChannel works as expected');
+
+promise_test(async t => {
+ const channel = new MessageChannel;
+ const port1 = channel.port1;
+ const port2 = channel.port2;
+
+ const source = {
+ start(controller) {
+ controller.enqueue({ port1 }, { transfer : [ port1 ] });
+ },
+ type: 'owning'
+ };
+
+ const stream = new ReadableStream(source);
+ const [clone1, clone2] = stream.tee();
+
+ await promise_rejects_dom(t, "DataCloneError", clone2.getReader().read());
+}, 'Second branch of owning ReadableStream tee should end up into errors with transfer only values');
diff --git a/testing/web-platform/tests/streams/readable-streams/owning-type-video-frame.any.js b/testing/web-platform/tests/streams/readable-streams/owning-type-video-frame.any.js
new file mode 100644
index 0000000000..ec01fda0b3
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/owning-type-video-frame.any.js
@@ -0,0 +1,128 @@
+// META: global=window,worker
+// META: script=../resources/test-utils.js
+// META: script=../resources/rs-utils.js
+'use strict';
+
+function createVideoFrame()
+{
+ let init = {
+ format: 'I420',
+ timestamp: 1234,
+ codedWidth: 4,
+ codedHeight: 2
+ };
+ let data = new Uint8Array([
+ 1, 2, 3, 4, 5, 6, 7, 8, // y
+ 1, 2, // u
+ 1, 2, // v
+ ]);
+
+ return new VideoFrame(data, init);
+}
+
+promise_test(async () => {
+ const videoFrame = createVideoFrame();
+ videoFrame.test = 1;
+ const source = {
+ start(controller) {
+ assert_equals(videoFrame.format, 'I420');
+ controller.enqueue(videoFrame, { transfer : [ videoFrame ] });
+ assert_equals(videoFrame.format, null);
+ assert_equals(videoFrame.test, 1);
+ },
+ type: 'owning'
+ };
+
+ const stream = new ReadableStream(source);
+ // Cancelling the stream should close all video frames, thus no console messages of GCing VideoFrames should happen.
+ stream.cancel();
+}, 'ReadableStream of type owning should close serialized chunks');
+
+promise_test(async () => {
+ const videoFrame = createVideoFrame();
+ videoFrame.test = 1;
+ const source = {
+ start(controller) {
+ assert_equals(videoFrame.format, 'I420');
+ controller.enqueue({ videoFrame }, { transfer : [ videoFrame ] });
+ assert_equals(videoFrame.format, null);
+ assert_equals(videoFrame.test, 1);
+ },
+ type: 'owning'
+ };
+
+ const stream = new ReadableStream(source);
+ const reader = stream.getReader();
+
+ const chunk = await reader.read();
+ assert_equals(chunk.value.videoFrame.format, 'I420');
+ assert_equals(chunk.value.videoFrame.test, undefined);
+
+ chunk.value.videoFrame.close();
+}, 'ReadableStream of type owning should transfer JS chunks with transferred values');
+
+promise_test(async t => {
+ const videoFrame = createVideoFrame();
+ videoFrame.close();
+ const source = {
+ start(controller) {
+ assert_throws_dom("DataCloneError", () => controller.enqueue(videoFrame, { transfer : [ videoFrame ] }));
+ },
+ type: 'owning'
+ };
+
+ const stream = new ReadableStream(source);
+ const reader = stream.getReader();
+
+ await promise_rejects_dom(t, "DataCloneError", reader.read());
+}, 'ReadableStream of type owning should error when trying to enqueue not serializable values');
+
+promise_test(async () => {
+ const videoFrame = createVideoFrame();
+ const source = {
+ start(controller) {
+ controller.enqueue(videoFrame, { transfer : [ videoFrame ] });
+ },
+ type: 'owning'
+ };
+
+ const stream = new ReadableStream(source);
+ const [clone1, clone2] = stream.tee();
+
+ const chunk1 = await clone1.getReader().read();
+ const chunk2 = await clone2.getReader().read();
+
+ assert_equals(videoFrame.format, null);
+ assert_equals(chunk1.value.format, 'I420');
+ assert_equals(chunk2.value.format, 'I420');
+
+ chunk1.value.close();
+ chunk2.value.close();
+}, 'ReadableStream of type owning should clone serializable objects when teeing');
+
+promise_test(async () => {
+ const videoFrame = createVideoFrame();
+ videoFrame.test = 1;
+ const source = {
+ start(controller) {
+ assert_equals(videoFrame.format, 'I420');
+ controller.enqueue({ videoFrame }, { transfer : [ videoFrame ] });
+ assert_equals(videoFrame.format, null);
+ assert_equals(videoFrame.test, 1);
+ },
+ type: 'owning'
+ };
+
+ const stream = new ReadableStream(source);
+ const [clone1, clone2] = stream.tee();
+
+ const chunk1 = await clone1.getReader().read();
+ const chunk2 = await clone2.getReader().read();
+
+ assert_equals(videoFrame.format, null);
+ assert_equals(chunk1.value.videoFrame.format, 'I420');
+ assert_equals(chunk2.value.videoFrame.format, 'I420');
+
+ chunk1.value.videoFrame.close();
+ chunk2.value.videoFrame.close();
+}, 'ReadableStream of type owning should clone JS Objects with serializables when teeing');
diff --git a/testing/web-platform/tests/streams/readable-streams/owning-type.any.js b/testing/web-platform/tests/streams/readable-streams/owning-type.any.js
new file mode 100644
index 0000000000..27a3dda894
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/owning-type.any.js
@@ -0,0 +1,91 @@
+// META: global=window,worker
+// META: script=../resources/test-utils.js
+// META: script=../resources/rs-utils.js
+'use strict';
+
+test(() => {
+ new ReadableStream({ type: 'owning' }); // ReadableStream constructed with 'owning' type
+}, 'ReadableStream can be constructed with owning type');
+
+test(() => {
+ let startCalled = false;
+
+ const source = {
+ start(controller) {
+ assert_equals(this, source, 'source is this during start');
+ assert_true(controller instanceof ReadableStreamDefaultController, 'default controller');
+ startCalled = true;
+ },
+ type: 'owning'
+ };
+
+ new ReadableStream(source);
+ assert_true(startCalled);
+}, 'ReadableStream of type owning should call start with a ReadableStreamDefaultController');
+
+test(() => {
+ let startCalled = false;
+
+ const source = {
+ start(controller) {
+ controller.enqueue("a", { transfer: [] });
+ controller.enqueue("a", { transfer: undefined });
+ startCalled = true;
+ },
+ type: 'owning'
+ };
+
+ new ReadableStream(source);
+ assert_true(startCalled);
+}, 'ReadableStream should be able to call enqueue with an empty transfer list');
+
+test(() => {
+ let startCalled = false;
+
+ const uint8Array = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ const buffer = uint8Array.buffer;
+ let source = {
+ start(controller) {
+ startCalled = true;
+ assert_throws_js(TypeError, () => { controller.enqueue(buffer, { transfer : [ buffer ] }); }, "transfer list is not empty");
+ }
+ };
+
+ new ReadableStream(source);
+ assert_true(startCalled);
+
+ startCalled = false;
+ source = {
+ start(controller) {
+ startCalled = true;
+ assert_throws_js(TypeError, () => { controller.enqueue(buffer, { get transfer() { throw new TypeError(); } }) }, "getter throws");
+ }
+ };
+
+ new ReadableStream(source);
+ assert_true(startCalled);
+}, 'ReadableStream should check transfer parameter');
+
+promise_test(async () => {
+ const uint8Array = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
+ const buffer = uint8Array.buffer;
+ buffer.test = 1;
+ const source = {
+ start(controller) {
+ assert_equals(buffer.byteLength, 8);
+ controller.enqueue(buffer, { transfer : [ buffer ] });
+ assert_equals(buffer.byteLength, 0);
+ assert_equals(buffer.test, 1);
+ },
+ type: 'owning'
+ };
+
+ const stream = new ReadableStream(source);
+ const reader = stream.getReader();
+
+ const chunk = await reader.read();
+
+ assert_not_equals(chunk.value, buffer);
+ assert_equals(chunk.value.byteLength, 8);
+ assert_equals(chunk.value.test, undefined);
+}, 'ReadableStream of type owning should transfer enqueued chunks');
diff --git a/testing/web-platform/tests/streams/readable-streams/patched-global.any.js b/testing/web-platform/tests/streams/readable-streams/patched-global.any.js
new file mode 100644
index 0000000000..a64a054a97
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/patched-global.any.js
@@ -0,0 +1,142 @@
+// META: global=window,worker
+'use strict';
+
+// Tests which patch the global environment are kept separate to avoid
+// interfering with other tests.
+
+const ReadableStream_prototype_locked_get =
+ Object.getOwnPropertyDescriptor(ReadableStream.prototype, 'locked').get;
+
+// Verify that |rs| passes the brand check as a readable stream.
+function isReadableStream(rs) {
+ try {
+ ReadableStream_prototype_locked_get.call(rs);
+ return true;
+ } catch (e) {
+ return false;
+ }
+}
+
+test(t => {
+ const rs = new ReadableStream();
+
+ const trappedProperties = ['highWaterMark', 'size', 'start', 'type', 'mode'];
+ for (const property of trappedProperties) {
+ // eslint-disable-next-line no-extend-native, accessor-pairs
+ Object.defineProperty(Object.prototype, property, {
+ get() { throw new Error(`${property} getter called`); },
+ configurable: true
+ });
+ }
+ t.add_cleanup(() => {
+ for (const property of trappedProperties) {
+ delete Object.prototype[property];
+ }
+ });
+
+ const [branch1, branch2] = rs.tee();
+ assert_true(isReadableStream(branch1), 'branch1 should be a ReadableStream');
+ assert_true(isReadableStream(branch2), 'branch2 should be a ReadableStream');
+}, 'ReadableStream tee() should not touch Object.prototype properties');
+
+test(t => {
+ const rs = new ReadableStream();
+
+ const oldReadableStream = self.ReadableStream;
+
+ self.ReadableStream = function() {
+ throw new Error('ReadableStream called on global object');
+ };
+
+ t.add_cleanup(() => {
+ self.ReadableStream = oldReadableStream;
+ });
+
+ const [branch1, branch2] = rs.tee();
+
+ assert_true(isReadableStream(branch1), 'branch1 should be a ReadableStream');
+ assert_true(isReadableStream(branch2), 'branch2 should be a ReadableStream');
+}, 'ReadableStream tee() should not call the global ReadableStream');
+
+promise_test(async t => {
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue(1);
+ c.enqueue(2);
+ c.enqueue(3);
+ c.close();
+ }
+ });
+
+ const oldReadableStreamGetReader = ReadableStream.prototype.getReader;
+
+ const ReadableStreamDefaultReader = (new ReadableStream()).getReader().constructor;
+ const oldDefaultReaderRead = ReadableStreamDefaultReader.prototype.read;
+ const oldDefaultReaderCancel = ReadableStreamDefaultReader.prototype.cancel;
+ const oldDefaultReaderReleaseLock = ReadableStreamDefaultReader.prototype.releaseLock;
+
+ self.ReadableStream.prototype.getReader = function() {
+ throw new Error('patched getReader() called');
+ };
+
+ ReadableStreamDefaultReader.prototype.read = function() {
+ throw new Error('patched read() called');
+ };
+ ReadableStreamDefaultReader.prototype.cancel = function() {
+ throw new Error('patched cancel() called');
+ };
+ ReadableStreamDefaultReader.prototype.releaseLock = function() {
+ throw new Error('patched releaseLock() called');
+ };
+
+ t.add_cleanup(() => {
+ self.ReadableStream.prototype.getReader = oldReadableStreamGetReader;
+
+ ReadableStreamDefaultReader.prototype.read = oldDefaultReaderRead;
+ ReadableStreamDefaultReader.prototype.cancel = oldDefaultReaderCancel;
+ ReadableStreamDefaultReader.prototype.releaseLock = oldDefaultReaderReleaseLock;
+ });
+
+ // read the first chunk, then cancel
+ for await (const chunk of rs) {
+ break;
+ }
+
+ // should be able to acquire a new reader
+ const reader = oldReadableStreamGetReader.call(rs);
+ // stream should be cancelled
+ await reader.closed;
+}, 'ReadableStream async iterator should use the original values of getReader() and ReadableStreamDefaultReader ' +
+ 'methods');
+
+test(t => {
+ const oldPromiseThen = Promise.prototype.then;
+ Promise.prototype.then = () => {
+ throw new Error('patched then() called');
+ };
+ t.add_cleanup(() => {
+ Promise.prototype.then = oldPromiseThen;
+ });
+ const [branch1, branch2] = new ReadableStream().tee();
+ assert_true(isReadableStream(branch1), 'branch1 should be a ReadableStream');
+ assert_true(isReadableStream(branch2), 'branch2 should be a ReadableStream');
+}, 'tee() should not call Promise.prototype.then()');
+
+test(t => {
+ const oldPromiseThen = Promise.prototype.then;
+ Promise.prototype.then = () => {
+ throw new Error('patched then() called');
+ };
+ t.add_cleanup(() => {
+ Promise.prototype.then = oldPromiseThen;
+ });
+ let readableController;
+ const rs = new ReadableStream({
+ start(c) {
+ readableController = c;
+ }
+ });
+ const ws = new WritableStream();
+ rs.pipeTo(ws);
+ readableController.close();
+}, 'pipeTo() should not call Promise.prototype.then()');
diff --git a/testing/web-platform/tests/streams/readable-streams/read-task-handling.window.js b/testing/web-platform/tests/streams/readable-streams/read-task-handling.window.js
new file mode 100644
index 0000000000..2edc0ddddf
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/read-task-handling.window.js
@@ -0,0 +1,46 @@
+// META: global=window,worker
+'use strict';
+
+function performMicrotaskCheckpoint() {
+ document.createNodeIterator(document, -1, {
+ acceptNode() {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ }).nextNode();
+}
+
+test(() => {
+ // Add a getter for "then" that will incidentally be invoked
+ // during promise resolution.
+ Object.prototype.__defineGetter__('then', () => {
+ // Clean up behind ourselves.
+ delete Object.prototype.then;
+
+ // This promise should (like all promises) be resolved
+ // asynchronously.
+ var executed = false;
+ Promise.resolve().then(_ => { executed = true; });
+
+ // This shouldn't run microtasks! They should only run
+ // after the fetch is resolved.
+ performMicrotaskCheckpoint();
+
+ // The fulfill handler above shouldn't have run yet. If it has run,
+ // throw to reject this promise and fail the test.
+ assert_false(executed, "shouldn't have run microtasks yet");
+
+ // Otherwise act as if there's no "then" property so the promise
+ // fulfills and the test passes.
+ return undefined;
+ });
+
+ const readable = new ReadableStream({
+ pull(c) {
+ c.enqueue({});
+ }
+ }, { highWaterMark: 0 });
+
+ // Create a read request, incidentally resolving a promise with an
+ // object value, thereby invoking the getter installed above.
+ readable.getReader().read();
+}, "reading from a stream should occur in a microtask scope");
diff --git a/testing/web-platform/tests/streams/readable-streams/reentrant-strategies.any.js b/testing/web-platform/tests/streams/readable-streams/reentrant-strategies.any.js
new file mode 100644
index 0000000000..b4988bc243
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/reentrant-strategies.any.js
@@ -0,0 +1,264 @@
+// META: global=window,worker
+// META: script=../resources/recording-streams.js
+// META: script=../resources/rs-utils.js
+// META: script=../resources/test-utils.js
+'use strict';
+
+// The size() function of the readable strategy can re-entrantly call back into the ReadableStream implementation. This
+// makes it risky to cache state across the call to ReadableStreamDefaultControllerEnqueue. These tests attempt to catch
+// such errors. They are separated from the other strategy tests because no real user code should ever do anything like
+// this.
+
+const error1 = new Error('error1');
+error1.name = 'error1';
+
+promise_test(() => {
+ let controller;
+ let calls = 0;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, {
+ size() {
+ ++calls;
+ if (calls < 2) {
+ controller.enqueue('b');
+ }
+ return 1;
+ }
+ });
+ controller.enqueue('a');
+ controller.close();
+ return readableStreamToArray(rs)
+ .then(array => assert_array_equals(array, ['b', 'a'], 'array should contain two chunks'));
+}, 'enqueue() inside size() should work');
+
+promise_test(() => {
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, {
+ size() {
+ // The queue is empty.
+ controller.close();
+ // The state has gone from "readable" to "closed".
+ return 1;
+ // This chunk will be enqueued, but will be impossible to read because the state is already "closed".
+ }
+ });
+ controller.enqueue('a');
+ return readableStreamToArray(rs)
+ .then(array => assert_array_equals(array, [], 'array should contain no chunks'));
+ // The chunk 'a' is still in rs's queue. It is closed so 'a' cannot be read.
+}, 'close() inside size() should not crash');
+
+promise_test(() => {
+ let controller;
+ let calls = 0;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, {
+ size() {
+ ++calls;
+ if (calls === 2) {
+ // The queue contains one chunk.
+ controller.close();
+ // The state is still "readable", but closeRequest is now true.
+ }
+ return 1;
+ }
+ });
+ controller.enqueue('a');
+ controller.enqueue('b');
+ return readableStreamToArray(rs)
+ .then(array => assert_array_equals(array, ['a', 'b'], 'array should contain two chunks'));
+}, 'close request inside size() should work');
+
+promise_test(t => {
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, {
+ size() {
+ controller.error(error1);
+ return 1;
+ }
+ });
+ controller.enqueue('a');
+ return promise_rejects_exactly(t, error1, rs.getReader().read(), 'read() should reject');
+}, 'error() inside size() should work');
+
+promise_test(() => {
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, {
+ size() {
+ assert_equals(controller.desiredSize, 1, 'desiredSize should be 1');
+ return 1;
+ },
+ highWaterMark: 1
+ });
+ controller.enqueue('a');
+ controller.close();
+ return readableStreamToArray(rs)
+ .then(array => assert_array_equals(array, ['a'], 'array should contain one chunk'));
+}, 'desiredSize inside size() should work');
+
+promise_test(t => {
+ let cancelPromise;
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ },
+ cancel: t.step_func(reason => {
+ assert_equals(reason, error1, 'reason should be error1');
+ assert_throws_js(TypeError, () => controller.enqueue(), 'enqueue() should throw');
+ })
+ }, {
+ size() {
+ cancelPromise = rs.cancel(error1);
+ return 1;
+ },
+ highWaterMark: Infinity
+ });
+ controller.enqueue('a');
+ const reader = rs.getReader();
+ return Promise.all([
+ reader.closed,
+ cancelPromise
+ ]);
+}, 'cancel() inside size() should work');
+
+promise_test(() => {
+ let controller;
+ let pipeToPromise;
+ const ws = recordingWritableStream();
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, {
+ size() {
+ if (!pipeToPromise) {
+ pipeToPromise = rs.pipeTo(ws);
+ }
+ return 1;
+ },
+ highWaterMark: 1
+ });
+ controller.enqueue('a');
+ assert_not_equals(pipeToPromise, undefined);
+
+ // Some pipeTo() implementations need an additional chunk enqueued in order for the first one to be processed. See
+ // https://github.com/whatwg/streams/issues/794 for background.
+ controller.enqueue('a');
+
+ // Give pipeTo() a chance to process the queued chunks.
+ return delay(0).then(() => {
+ assert_array_equals(ws.events, ['write', 'a', 'write', 'a'], 'ws should contain two chunks');
+ controller.close();
+ return pipeToPromise;
+ }).then(() => {
+ assert_array_equals(ws.events, ['write', 'a', 'write', 'a', 'close'], 'target should have been closed');
+ });
+}, 'pipeTo() inside size() should behave as expected');
+
+promise_test(() => {
+ let controller;
+ let readPromise;
+ let calls = 0;
+ let readResolved = false;
+ let reader;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, {
+ size() {
+ // This is triggered by controller.enqueue(). The queue is empty and there are no pending reads. This read is
+ // added to the list of pending reads.
+ readPromise = reader.read();
+ ++calls;
+ return 1;
+ },
+ highWaterMark: 0
+ });
+ reader = rs.getReader();
+ controller.enqueue('a');
+ readPromise.then(() => {
+ readResolved = true;
+ });
+ return flushAsyncEvents().then(() => {
+ assert_false(readResolved);
+ controller.enqueue('b');
+ assert_equals(calls, 1, 'size() should have been called once');
+ return delay(0);
+ }).then(() => {
+ assert_true(readResolved);
+ assert_equals(calls, 1, 'size() should only be called once');
+ return readPromise;
+ }).then(({ value, done }) => {
+ assert_false(done, 'done should be false');
+ // See https://github.com/whatwg/streams/issues/794 for why this chunk is not 'a'.
+ assert_equals(value, 'b', 'chunk should have been read');
+ assert_equals(calls, 1, 'calls should still be 1');
+ return reader.read();
+ }).then(({ value, done }) => {
+ assert_false(done, 'done should be false again');
+ assert_equals(value, 'a', 'chunk a should come after b');
+ });
+}, 'read() inside of size() should behave as expected');
+
+promise_test(() => {
+ let controller;
+ let reader;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, {
+ size() {
+ reader = rs.getReader();
+ return 1;
+ }
+ });
+ controller.enqueue('a');
+ return reader.read().then(({ value, done }) => {
+ assert_false(done, 'done should be false');
+ assert_equals(value, 'a', 'value should be a');
+ });
+}, 'getReader() inside size() should work');
+
+promise_test(() => {
+ let controller;
+ let branch1;
+ let branch2;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }, {
+ size() {
+ [branch1, branch2] = rs.tee();
+ return 1;
+ }
+ });
+ controller.enqueue('a');
+ assert_true(rs.locked, 'rs should be locked');
+ controller.close();
+ return Promise.all([
+ readableStreamToArray(branch1).then(array => assert_array_equals(array, ['a'], 'branch1 should have one chunk')),
+ readableStreamToArray(branch2).then(array => assert_array_equals(array, ['a'], 'branch2 should have one chunk'))
+ ]);
+}, 'tee() inside size() should work');
diff --git a/testing/web-platform/tests/streams/readable-streams/tee.any.js b/testing/web-platform/tests/streams/readable-streams/tee.any.js
new file mode 100644
index 0000000000..00397932f4
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/tee.any.js
@@ -0,0 +1,479 @@
+// META: global=window,worker
+// META: script=../resources/rs-utils.js
+// META: script=../resources/test-utils.js
+// META: script=../resources/recording-streams.js
+// META: script=../resources/rs-test-templates.js
+'use strict';
+
+test(() => {
+
+ const rs = new ReadableStream();
+ const result = rs.tee();
+
+ assert_true(Array.isArray(result), 'return value should be an array');
+ assert_equals(result.length, 2, 'array should have length 2');
+ assert_equals(result[0].constructor, ReadableStream, '0th element should be a ReadableStream');
+ assert_equals(result[1].constructor, ReadableStream, '1st element should be a ReadableStream');
+
+}, 'ReadableStream teeing: rs.tee() returns an array of two ReadableStreams');
+
+promise_test(t => {
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ c.enqueue('b');
+ c.close();
+ }
+ });
+
+ const branch = rs.tee();
+ const branch1 = branch[0];
+ const branch2 = branch[1];
+ const reader1 = branch1.getReader();
+ const reader2 = branch2.getReader();
+
+ reader2.closed.then(t.unreached_func('branch2 should not be closed'));
+
+ return Promise.all([
+ reader1.closed,
+ reader1.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'first chunk from branch1 should be correct');
+ }),
+ reader1.read().then(r => {
+ assert_object_equals(r, { value: 'b', done: false }, 'second chunk from branch1 should be correct');
+ }),
+ reader1.read().then(r => {
+ assert_object_equals(r, { value: undefined, done: true }, 'third read() from branch1 should be done');
+ }),
+ reader2.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'first chunk from branch2 should be correct');
+ })
+ ]);
+
+}, 'ReadableStream teeing: should be able to read one branch to the end without affecting the other');
+
+promise_test(() => {
+
+ const theObject = { the: 'test object' };
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue(theObject);
+ }
+ });
+
+ const branch = rs.tee();
+ const branch1 = branch[0];
+ const branch2 = branch[1];
+ const reader1 = branch1.getReader();
+ const reader2 = branch2.getReader();
+
+ return Promise.all([reader1.read(), reader2.read()]).then(values => {
+ assert_object_equals(values[0], values[1], 'the values should be equal');
+ });
+
+}, 'ReadableStream teeing: values should be equal across each branch');
+
+promise_test(t => {
+
+ const theError = { name: 'boo!' };
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ c.enqueue('b');
+ },
+ pull() {
+ throw theError;
+ }
+ });
+
+ const branches = rs.tee();
+ const reader1 = branches[0].getReader();
+ const reader2 = branches[1].getReader();
+
+ reader1.label = 'reader1';
+ reader2.label = 'reader2';
+
+ return Promise.all([
+ promise_rejects_exactly(t, theError, reader1.closed),
+ promise_rejects_exactly(t, theError, reader2.closed),
+ reader1.read().then(r => {
+ assert_object_equals(r, { value: 'a', done: false }, 'should be able to read the first chunk in branch1');
+ }),
+ reader1.read().then(r => {
+ assert_object_equals(r, { value: 'b', done: false }, 'should be able to read the second chunk in branch1');
+
+ return promise_rejects_exactly(t, theError, reader2.read());
+ })
+ .then(() => promise_rejects_exactly(t, theError, reader1.read()))
+ ]);
+
+}, 'ReadableStream teeing: errors in the source should propagate to both branches');
+
+promise_test(() => {
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ c.enqueue('b');
+ c.close();
+ }
+ });
+
+ const branches = rs.tee();
+ const branch1 = branches[0];
+ const branch2 = branches[1];
+ branch1.cancel();
+
+ return Promise.all([
+ readableStreamToArray(branch1).then(chunks => {
+ assert_array_equals(chunks, [], 'branch1 should have no chunks');
+ }),
+ readableStreamToArray(branch2).then(chunks => {
+ assert_array_equals(chunks, ['a', 'b'], 'branch2 should have two chunks');
+ })
+ ]);
+
+}, 'ReadableStream teeing: canceling branch1 should not impact branch2');
+
+promise_test(() => {
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ c.enqueue('b');
+ c.close();
+ }
+ });
+
+ const branches = rs.tee();
+ const branch1 = branches[0];
+ const branch2 = branches[1];
+ branch2.cancel();
+
+ return Promise.all([
+ readableStreamToArray(branch1).then(chunks => {
+ assert_array_equals(chunks, ['a', 'b'], 'branch1 should have two chunks');
+ }),
+ readableStreamToArray(branch2).then(chunks => {
+ assert_array_equals(chunks, [], 'branch2 should have no chunks');
+ })
+ ]);
+
+}, 'ReadableStream teeing: canceling branch2 should not impact branch1');
+
+templatedRSTeeCancel('ReadableStream teeing', (extras) => {
+ return new ReadableStream({ ...extras });
+});
+
+promise_test(t => {
+
+ let controller;
+ const stream = new ReadableStream({ start(c) { controller = c; } });
+ const [branch1, branch2] = stream.tee();
+
+ const error = new Error();
+ error.name = 'distinctive';
+
+ // Ensure neither branch is waiting in ReadableStreamDefaultReaderRead().
+ controller.enqueue();
+ controller.enqueue();
+
+ return delay(0).then(() => {
+ // This error will have to be detected via [[closedPromise]].
+ controller.error(error);
+
+ const reader1 = branch1.getReader();
+ const reader2 = branch2.getReader();
+
+ return Promise.all([
+ promise_rejects_exactly(t, error, reader1.closed, 'reader1.closed should reject'),
+ promise_rejects_exactly(t, error, reader2.closed, 'reader2.closed should reject')
+ ]);
+ });
+
+}, 'ReadableStream teeing: erroring a teed stream should error both branches');
+
+promise_test(() => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const branches = rs.tee();
+ const reader1 = branches[0].getReader();
+ const reader2 = branches[1].getReader();
+
+ const promise = Promise.all([reader1.closed, reader2.closed]);
+
+ controller.close();
+ return promise;
+
+}, 'ReadableStream teeing: closing the original should immediately close the branches');
+
+promise_test(t => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const branches = rs.tee();
+ const reader1 = branches[0].getReader();
+ const reader2 = branches[1].getReader();
+
+ const theError = { name: 'boo!' };
+ const promise = Promise.all([
+ promise_rejects_exactly(t, theError, reader1.closed),
+ promise_rejects_exactly(t, theError, reader2.closed)
+ ]);
+
+ controller.error(theError);
+ return promise;
+
+}, 'ReadableStream teeing: erroring the original should immediately error the branches');
+
+promise_test(async t => {
+
+ let controller;
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
+ const cancelPromise = reader2.cancel();
+
+ controller.enqueue('a');
+
+ const read1 = await reader1.read();
+ assert_object_equals(read1, { value: 'a', done: false }, 'first read() from branch1 should fulfill with the chunk');
+
+ controller.close();
+
+ const read2 = await reader1.read();
+ assert_object_equals(read2, { value: undefined, done: true }, 'second read() from branch1 should be done');
+
+ await Promise.all([
+ reader1.closed,
+ cancelPromise
+ ]);
+
+}, 'ReadableStream teeing: canceling branch1 should finish when branch2 reads until end of stream');
+
+promise_test(async t => {
+
+ let controller;
+ const theError = { name: 'boo!' };
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
+ const cancelPromise = reader2.cancel();
+
+ controller.error(theError);
+
+ await Promise.all([
+ promise_rejects_exactly(t, theError, reader1.read()),
+ cancelPromise
+ ]);
+
+}, 'ReadableStream teeing: canceling branch1 should finish when original stream errors');
+
+promise_test(async () => {
+
+ const rs = new ReadableStream({});
+
+ const [branch1, branch2] = rs.tee();
+
+ const cancel1 = branch1.cancel();
+ await flushAsyncEvents();
+ const cancel2 = branch2.cancel();
+
+ await Promise.all([cancel1, cancel2]);
+
+}, 'ReadableStream teeing: canceling both branches in sequence with delay');
+
+promise_test(async t => {
+
+ const theError = { name: 'boo!' };
+ const rs = new ReadableStream({
+ cancel() {
+ throw theError;
+ }
+ });
+
+ const [branch1, branch2] = rs.tee();
+
+ const cancel1 = branch1.cancel();
+ await flushAsyncEvents();
+ const cancel2 = branch2.cancel();
+
+ await Promise.all([
+ promise_rejects_exactly(t, theError, cancel1),
+ promise_rejects_exactly(t, theError, cancel2)
+ ]);
+
+}, 'ReadableStream teeing: failing to cancel when canceling both branches in sequence with delay');
+
+test(t => {
+
+ // Copy original global.
+ const oldReadableStream = ReadableStream;
+ const getReader = ReadableStream.prototype.getReader;
+
+ const origRS = new ReadableStream();
+
+ // Replace the global ReadableStream constructor with one that doesn't work.
+ ReadableStream = function() {
+ throw new Error('global ReadableStream constructor called');
+ };
+ t.add_cleanup(() => {
+ ReadableStream = oldReadableStream;
+ });
+
+ // This will probably fail if the global ReadableStream constructor was used.
+ const [rs1, rs2] = origRS.tee();
+
+ // These will definitely fail if the global ReadableStream constructor was used.
+ assert_not_equals(getReader.call(rs1), undefined, 'getReader should work on rs1');
+ assert_not_equals(getReader.call(rs2), undefined, 'getReader should work on rs2');
+
+}, 'ReadableStreamTee should not use a modified ReadableStream constructor from the global object');
+
+promise_test(t => {
+
+ const rs = recordingReadableStream({}, { highWaterMark: 0 });
+
+ // Create two branches, each with a HWM of 1. This should result in one
+ // chunk being pulled, not two.
+ rs.tee();
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(rs.events, ['pull'], 'pull should only be called once');
+ });
+
+}, 'ReadableStreamTee should not pull more chunks than can fit in the branch queue');
+
+promise_test(t => {
+
+ const rs = recordingReadableStream({
+ pull(controller) {
+ controller.enqueue('a');
+ }
+ }, { highWaterMark: 0 });
+
+ const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
+ return Promise.all([reader1.read(), reader2.read()])
+ .then(() => {
+ assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
+ });
+
+}, 'ReadableStreamTee should only pull enough to fill the emptiest queue');
+
+promise_test(t => {
+
+ const rs = recordingReadableStream({}, { highWaterMark: 0 });
+ const theError = { name: 'boo!' };
+
+ rs.controller.error(theError);
+
+ const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
+
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(rs.events, [], 'pull should not be called');
+
+ return Promise.all([
+ promise_rejects_exactly(t, theError, reader1.closed),
+ promise_rejects_exactly(t, theError, reader2.closed)
+ ]);
+ });
+
+}, 'ReadableStreamTee should not pull when original is already errored');
+
+for (const branch of [1, 2]) {
+ promise_test(t => {
+
+ const rs = recordingReadableStream({}, { highWaterMark: 0 });
+ const theError = { name: 'boo!' };
+
+ const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
+
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(rs.events, ['pull'], 'pull should be called once');
+
+ rs.controller.enqueue('a');
+
+ const reader = (branch === 1) ? reader1 : reader2;
+ return reader.read();
+ }).then(() => flushAsyncEvents()).then(() => {
+ assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
+
+ rs.controller.error(theError);
+
+ return Promise.all([
+ promise_rejects_exactly(t, theError, reader1.closed),
+ promise_rejects_exactly(t, theError, reader2.closed)
+ ]);
+ }).then(() => flushAsyncEvents()).then(() => {
+ assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
+ });
+
+ }, `ReadableStreamTee stops pulling when original stream errors while branch ${branch} is reading`);
+}
+
+promise_test(t => {
+
+ const rs = recordingReadableStream({}, { highWaterMark: 0 });
+ const theError = { name: 'boo!' };
+
+ const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
+
+ return flushAsyncEvents().then(() => {
+ assert_array_equals(rs.events, ['pull'], 'pull should be called once');
+
+ rs.controller.enqueue('a');
+
+ return Promise.all([reader1.read(), reader2.read()]);
+ }).then(() => flushAsyncEvents()).then(() => {
+ assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
+
+ rs.controller.error(theError);
+
+ return Promise.all([
+ promise_rejects_exactly(t, theError, reader1.closed),
+ promise_rejects_exactly(t, theError, reader2.closed)
+ ]);
+ }).then(() => flushAsyncEvents()).then(() => {
+ assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
+ });
+
+}, 'ReadableStreamTee stops pulling when original stream errors while both branches are reading');
+
+promise_test(async () => {
+
+ const rs = recordingReadableStream();
+
+ const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
+ const branch1Reads = [reader1.read(), reader1.read()];
+ const branch2Reads = [reader2.read(), reader2.read()];
+
+ await flushAsyncEvents();
+ rs.controller.enqueue('a');
+ rs.controller.close();
+
+ assert_object_equals(await branch1Reads[0], { value: 'a', done: false }, 'first chunk from branch1 should be correct');
+ assert_object_equals(await branch2Reads[0], { value: 'a', done: false }, 'first chunk from branch2 should be correct');
+
+ assert_object_equals(await branch1Reads[1], { value: undefined, done: true }, 'second read() from branch1 should be done');
+ assert_object_equals(await branch2Reads[1], { value: undefined, done: true }, 'second read() from branch2 should be done');
+
+}, 'ReadableStream teeing: enqueue() and close() while both branches are pulling');
diff --git a/testing/web-platform/tests/streams/readable-streams/templated.any.js b/testing/web-platform/tests/streams/readable-streams/templated.any.js
new file mode 100644
index 0000000000..ecae3f4d8b
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/templated.any.js
@@ -0,0 +1,143 @@
+// META: global=window,worker
+// META: script=../resources/test-utils.js
+// META: script=../resources/rs-test-templates.js
+'use strict';
+
+// Run the readable stream test templates against readable streams created directly using the constructor
+
+const theError = { name: 'boo!' };
+const chunks = ['a', 'b'];
+
+templatedRSEmpty('ReadableStream (empty)', () => {
+ return new ReadableStream();
+});
+
+templatedRSEmptyReader('ReadableStream (empty) reader', () => {
+ return streamAndDefaultReader(new ReadableStream());
+});
+
+templatedRSClosed('ReadableStream (closed via call in start)', () => {
+ return new ReadableStream({
+ start(c) {
+ c.close();
+ }
+ });
+});
+
+templatedRSClosedReader('ReadableStream reader (closed before getting reader)', () => {
+ let controller;
+ const stream = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ controller.close();
+ const result = streamAndDefaultReader(stream);
+ return result;
+});
+
+templatedRSClosedReader('ReadableStream reader (closed after getting reader)', () => {
+ let controller;
+ const stream = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ const result = streamAndDefaultReader(stream);
+ controller.close();
+ return result;
+});
+
+templatedRSClosed('ReadableStream (closed via cancel)', () => {
+ const stream = new ReadableStream();
+ stream.cancel();
+ return stream;
+});
+
+templatedRSClosedReader('ReadableStream reader (closed via cancel after getting reader)', () => {
+ const stream = new ReadableStream();
+ const result = streamAndDefaultReader(stream);
+ result.reader.cancel();
+ return result;
+});
+
+templatedRSErrored('ReadableStream (errored via call in start)', () => {
+ return new ReadableStream({
+ start(c) {
+ c.error(theError);
+ }
+ });
+}, theError);
+
+templatedRSErroredSyncOnly('ReadableStream (errored via call in start)', () => {
+ return new ReadableStream({
+ start(c) {
+ c.error(theError);
+ }
+ });
+}, theError);
+
+templatedRSErrored('ReadableStream (errored via returning a rejected promise in start)', () => {
+ return new ReadableStream({
+ start() {
+ return Promise.reject(theError);
+ }
+ });
+}, theError);
+
+templatedRSErroredReader('ReadableStream (errored via returning a rejected promise in start) reader', () => {
+ return streamAndDefaultReader(new ReadableStream({
+ start() {
+ return Promise.reject(theError);
+ }
+ }));
+}, theError);
+
+templatedRSErroredReader('ReadableStream reader (errored before getting reader)', () => {
+ let controller;
+ const stream = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+ controller.error(theError);
+ return streamAndDefaultReader(stream);
+}, theError);
+
+templatedRSErroredReader('ReadableStream reader (errored after getting reader)', () => {
+ let controller;
+ const result = streamAndDefaultReader(new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }));
+ controller.error(theError);
+ return result;
+}, theError);
+
+templatedRSTwoChunksOpenReader('ReadableStream (two chunks enqueued, still open) reader', () => {
+ return streamAndDefaultReader(new ReadableStream({
+ start(c) {
+ c.enqueue(chunks[0]);
+ c.enqueue(chunks[1]);
+ }
+ }));
+}, chunks);
+
+templatedRSTwoChunksClosedReader('ReadableStream (two chunks enqueued, then closed) reader', () => {
+ let doClose;
+ const stream = new ReadableStream({
+ start(c) {
+ c.enqueue(chunks[0]);
+ c.enqueue(chunks[1]);
+ doClose = c.close.bind(c);
+ }
+ });
+ const result = streamAndDefaultReader(stream);
+ doClose();
+ return result;
+}, chunks);
+
+function streamAndDefaultReader(stream) {
+ return { stream, reader: stream.getReader() };
+}