summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/streams/readable-streams
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /testing/web-platform/tests/streams/readable-streams
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/web-platform/tests/streams/readable-streams')
-rw-r--r--testing/web-platform/tests/streams/readable-streams/bad-strategies.https.html12
-rw-r--r--testing/web-platform/tests/streams/readable-streams/bad-strategies.js164
-rw-r--r--testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.https.html12
-rw-r--r--testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.js383
-rw-r--r--testing/web-platform/tests/streams/readable-streams/brand-checks.https.html13
-rw-r--r--testing/web-platform/tests/streams/readable-streams/brand-checks.js151
-rw-r--r--testing/web-platform/tests/streams/readable-streams/cancel.https.html14
-rw-r--r--testing/web-platform/tests/streams/readable-streams/cancel.js241
-rw-r--r--testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.https.html12
-rw-r--r--testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.js213
-rw-r--r--testing/web-platform/tests/streams/readable-streams/garbage-collection.https.html13
-rw-r--r--testing/web-platform/tests/streams/readable-streams/garbage-collection.js75
-rw-r--r--testing/web-platform/tests/streams/readable-streams/general.https.html14
-rw-r--r--testing/web-platform/tests/streams/readable-streams/general.js859
-rw-r--r--testing/web-platform/tests/streams/readable-streams/pipe-through.https.html13
-rw-r--r--testing/web-platform/tests/streams/readable-streams/pipe-through.js108
-rw-r--r--testing/web-platform/tests/streams/readable-streams/readable-stream-reader.https.html12
-rw-r--r--testing/web-platform/tests/streams/readable-streams/readable-stream-reader.js485
-rw-r--r--testing/web-platform/tests/streams/readable-streams/tee.https.html13
-rw-r--r--testing/web-platform/tests/streams/readable-streams/tee.js254
-rw-r--r--testing/web-platform/tests/streams/readable-streams/templated.https.html14
-rw-r--r--testing/web-platform/tests/streams/readable-streams/templated.js148
22 files changed, 3223 insertions, 0 deletions
diff --git a/testing/web-platform/tests/streams/readable-streams/bad-strategies.https.html b/testing/web-platform/tests/streams/readable-streams/bad-strategies.https.html
new file mode 100644
index 000000000..6613ab7fb
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/bad-strategies.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="bad-strategies.js"></script>
+<script>
+'use strict';
+worker_test('bad-strategies.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/bad-strategies.js b/testing/web-platform/tests/streams/readable-streams/bad-strategies.js
new file mode 100644
index 000000000..5a52d60d8
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/bad-strategies.js
@@ -0,0 +1,164 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('/resources/testharness.js');
+}
+
+test(() => {
+
+ const theError = new Error('a unique string');
+
+ assert_throws(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(thrownError, () => controller.enqueue('a'), 'enqueue should re-throw the error');
+
+ return promise_rejects(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(new RangeError(), () => controller.enqueue('a'), 'enqueue should throw a RangeError');
+
+ return promise_rejects(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(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(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(new 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');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.https.html b/testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.https.html
new file mode 100644
index 000000000..2b779761a
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="bad-underlying-sources.js"></script>
+<script>
+'use strict';
+worker_test('bad-underlying-sources.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.js b/testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.js
new file mode 100644
index 000000000..b95b54bc7
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/bad-underlying-sources.js
@@ -0,0 +1,383 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('/resources/testharness.js');
+}
+
+
+test(() => {
+
+ const theError = new Error('a unique string');
+
+ assert_throws(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(theError, () => {
+ new ReadableStream({
+ start() {
+ throw theError;
+ }
+ });
+ }, 'constructing the stream should re-throw the error');
+
+}, 'Underlying source start: throwing method');
+
+
+promise_test(t => {
+
+ const theError = new Error('a unique string');
+ const rs = new ReadableStream({
+ get pull() {
+ throw theError;
+ }
+ });
+
+ return promise_rejects(t, theError, rs.getReader().closed);
+
+}, '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(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 chunk read should be correct');
+ }),
+ promise_rejects(t, theError, reader.closed)
+ ]);
+
+}, 'Underlying source pull: throwing getter (second pull)');
+
+
+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(t, theError, reader.closed)
+ ]);
+
+}, 'Underlying source pull: throwing method (second pull)');
+
+promise_test(t => {
+
+ const theError = new Error('a unique string');
+ const rs = new ReadableStream({
+ get cancel() {
+ throw theError;
+ }
+ });
+
+ return promise_rejects(t, theError, rs.cancel());
+
+}, '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(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(new 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(new 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(new 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(new TypeError(), () => c.enqueue('a'), 'call to enqueue should throw the error');
+ }
+ }).getReader().closed;
+
+ return promise_rejects(t, theError, closed);
+
+}, 'Underlying source: calling enqueue on an errored stream should throw');
+
+promise_test(() => {
+
+ return new ReadableStream({
+ start(c) {
+ c.close();
+ assert_throws(new 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(new 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(new 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(new 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(new 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);
+ assert_throws(new TypeError(), () => c.error(), 'second call to error 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 error twice should throw the second time');
+
+promise_test(() => {
+
+ let startCalled = false;
+
+ const closed = new ReadableStream({
+ start(c) {
+ c.close();
+ assert_throws(new TypeError(), () => c.error(), 'second call to error should throw a TypeError');
+ startCalled = true;
+ }
+ }).getReader().closed;
+
+ return closed.then(() => assert_true(startCalled));
+
+}, 'Underlying source: calling error after close should 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');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/brand-checks.https.html b/testing/web-platform/tests/streams/readable-streams/brand-checks.https.html
new file mode 100644
index 000000000..ef5f326e3
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/brand-checks.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="../resources/test-utils.js"></script>
+<script src="brand-checks.js"></script>
+<script>
+'use strict';
+worker_test('brand-checks.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/brand-checks.js b/testing/web-platform/tests/streams/readable-streams/brand-checks.js
new file mode 100644
index 000000000..32553cf92
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/brand-checks.js
@@ -0,0 +1,151 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('../resources/test-utils.js');
+ self.importScripts('/resources/testharness.js');
+}
+
+let ReadableStreamReader;
+let ReadableStreamController;
+
+test(() => {
+
+ // It's not exposed globally, but we test a few of its properties here.
+ ReadableStreamReader = (new ReadableStream()).getReader().constructor;
+
+}, 'Can get the ReadableStreamReader constructor indirectly');
+
+test(() => {
+
+ // It's not exposed globally, but we test a few of its properties here.
+ new ReadableStream({
+ start(c) {
+ ReadableStreamController = c.constructor;
+ }
+ });
+
+}, 'Can get the ReadableStreamController constructor indirectly');
+
+function fakeReadableStream() {
+ return {
+ cancel() { return Promise.resolve(); },
+ getReader() { return new ReadableStreamReader(new ReadableStream()); },
+ pipeThrough(obj) { return obj.readable; },
+ pipeTo() { return Promise.resolve(); },
+ tee() { return [realReadableStream(), realReadableStream()]; }
+ };
+}
+
+function realReadableStream() {
+ return new ReadableStream();
+}
+
+function fakeReadableStreamReader() {
+ return {
+ get closed() { return Promise.resolve(); },
+ cancel() { return Promise.resolve(); },
+ read() { return Promise.resolve({ value: undefined, done: true }); },
+ releaseLock() { return; }
+ };
+}
+
+function fakeReadableStreamController() {
+ return {
+ close() { },
+ enqueue() { },
+ error() { }
+ };
+}
+
+promise_test(t => {
+
+ return methodRejects(t, ReadableStream.prototype, 'cancel', fakeReadableStream());
+
+}, 'ReadableStream.prototype.cancel enforces a brand check');
+
+test(() => {
+
+ methodThrows(ReadableStream.prototype, 'getReader', fakeReadableStream());
+
+}, 'ReadableStream.prototype.getReader enforces a brand check');
+
+test(() => {
+
+ methodThrows(ReadableStream.prototype, 'tee', fakeReadableStream());
+
+}, 'ReadableStream.prototype.tee enforces a brand check');
+
+test(() => {
+
+ assert_throws(new TypeError(), () => new ReadableStreamReader(fakeReadableStream()),
+ 'Constructing a ReadableStreamReader should throw');
+
+}, 'ReadableStreamReader enforces a brand check on its argument');
+
+promise_test(t => {
+
+ return Promise.all([
+ getterRejects(t, ReadableStreamReader.prototype, 'closed', fakeReadableStreamReader()),
+ getterRejects(t, ReadableStreamReader.prototype, 'closed', realReadableStream())
+ ]);
+
+}, 'ReadableStreamReader.prototype.closed enforces a brand check');
+
+promise_test(t => {
+
+ return Promise.all([
+ methodRejects(t, ReadableStreamReader.prototype, 'cancel', fakeReadableStreamReader()),
+ methodRejects(t, ReadableStreamReader.prototype, 'cancel', realReadableStream())
+ ]);
+
+}, 'ReadableStreamReader.prototype.cancel enforces a brand check');
+
+promise_test(t => {
+
+ return Promise.all([
+ methodRejects(t, ReadableStreamReader.prototype, 'read', fakeReadableStreamReader()),
+ methodRejects(t, ReadableStreamReader.prototype, 'read', realReadableStream())
+ ]);
+
+}, 'ReadableStreamReader.prototype.read enforces a brand check');
+
+test(() => {
+
+ methodThrows(ReadableStreamReader.prototype, 'releaseLock', fakeReadableStreamReader());
+ methodThrows(ReadableStreamReader.prototype, 'releaseLock', realReadableStream());
+
+}, 'ReadableStreamReader.prototype.releaseLock enforces a brand check');
+
+test(() => {
+
+ assert_throws(new TypeError(), () => new ReadableStreamController(fakeReadableStream()),
+ 'Constructing a ReadableStreamController should throw');
+
+}, 'ReadableStreamController enforces a brand check on its argument');
+
+test(() => {
+
+ assert_throws(new TypeError(), () => new ReadableStreamController(realReadableStream()),
+ 'Constructing a ReadableStreamController should throw');
+
+}, 'ReadableStreamController can\'t be given a fully-constructed ReadableStream');
+
+test(() => {
+
+ methodThrows(ReadableStreamController.prototype, 'close', fakeReadableStreamController());
+
+}, 'ReadableStreamController.prototype.close enforces a brand check');
+
+test(() => {
+
+ methodThrows(ReadableStreamController.prototype, 'enqueue', fakeReadableStreamController());
+
+}, 'ReadableStreamController.prototype.enqueue enforces a brand check');
+
+test(() => {
+
+ methodThrows(ReadableStreamController.prototype, 'error', fakeReadableStreamController());
+
+}, 'ReadableStreamController.prototype.error enforces a brand check');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/cancel.https.html b/testing/web-platform/tests/streams/readable-streams/cancel.https.html
new file mode 100644
index 000000000..e43c6200a
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/cancel.https.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="../resources/test-utils.js"></script>
+<script src="../resources/rs-utils.js"></script>
+<script src="cancel.js"></script>
+<script>
+'use strict';
+worker_test('cancel.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/cancel.js b/testing/web-platform/tests/streams/readable-streams/cancel.js
new file mode 100644
index 000000000..3857c8e93
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/cancel.js
@@ -0,0 +1,241 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('../resources/test-utils.js');
+ self.importScripts('../resources/rs-utils.js');
+ self.importScripts('/resources/testharness.js');
+}
+
+promise_test(() => {
+
+ 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 => {
+ setTimeout(() => {
+ 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(() => {
+ let 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(() => {
+
+ let resolveSourceCancelPromise;
+ let sourceCancelPromiseHasFulfilled = false;
+
+ const rs = new ReadableStream({
+ cancel() {
+ const sourceCancelPromise = new Promise(resolve => resolveSourceCancelPromise = resolve);
+
+ sourceCancelPromise.then(() => {
+ sourceCancelPromiseHasFulfilled = true;
+ });
+
+ return sourceCancelPromise;
+ }
+ });
+
+ setTimeout(() => 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(() => {
+
+ 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.');
+
+ setTimeout(() => 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');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.https.html b/testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.https.html
new file mode 100644
index 000000000..a281f0473
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="count-queuing-strategy-integration.js"></script>
+<script>
+'use strict';
+worker_test('count-queuing-strategy-integration.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.js b/testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.js
new file mode 100644
index 000000000..fb9a1aa2b
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/count-queuing-strategy-integration.js
@@ -0,0 +1,213 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('/resources/testharness.js');
+}
+
+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');
+
+
+ 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)');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/garbage-collection.https.html b/testing/web-platform/tests/streams/readable-streams/garbage-collection.https.html
new file mode 100644
index 000000000..9215eb7da
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/garbage-collection.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="../resources/test-utils.js"></script>
+<script src="garbage-collection.js"></script>
+<script>
+'use strict';
+worker_test('garbage-collection.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/garbage-collection.js b/testing/web-platform/tests/streams/readable-streams/garbage-collection.js
new file mode 100644
index 000000000..fb00c946b
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/garbage-collection.js
@@ -0,0 +1,75 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('../resources/test-utils.js');
+ self.importScripts('/resources/testharness.js');
+}
+
+promise_test(() => {
+
+ let controller;
+ new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ });
+
+ garbageCollect();
+
+ return delay(50).then(() => {
+ controller.close();
+ assert_throws(new TypeError(), () => controller.close(), 'close should throw a TypeError the second time');
+ assert_throws(new TypeError(), () => controller.error(), 'error should throw a TypeError on a closed stream');
+ });
+
+}, 'ReadableStreamController methods should continue working properly when scripts lose their reference to the ' +
+ 'readable stream');
+
+promise_test(() => {
+
+ let controller;
+
+ const closedPromise = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }).getReader().closed;
+
+ 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(t => {
+
+ const theError = new Error('boo');
+ let controller;
+
+ const closedPromise = new ReadableStream({
+ start(c) {
+ controller = c;
+ }
+ }).getReader().closed;
+
+ garbageCollect();
+
+ return delay(50).then(() => controller.error(theError))
+ .then(() => promise_rejects(t, theError, closedPromise));
+
+}, 'ReadableStream closed promise should reject even if stream and reader JS references are lost');
+
+promise_test(() => {
+
+ const rs = new ReadableStream({});
+
+ rs.getReader();
+
+ garbageCollect();
+
+ return delay(50).then(() => assert_throws(new TypeError(), () => rs.getReader(),
+ 'old reader should still be locking the stream even after garbage collection'));
+
+}, 'Garbage-collecting a ReadableStreamReader should not unlock its stream');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/general.https.html b/testing/web-platform/tests/streams/readable-streams/general.https.html
new file mode 100644
index 000000000..465271a9b
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/general.https.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="../resources/test-utils.js"></script>
+<script src="../resources/rs-utils.js"></script>
+<script src="general.js"></script>
+<script>
+'use strict';
+worker_test('general.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/general.js b/testing/web-platform/tests/streams/readable-streams/general.js
new file mode 100644
index 000000000..a8924be05
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/general.js
@@ -0,0 +1,859 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('../resources/test-utils.js');
+ self.importScripts('../resources/rs-utils.js');
+ self.importScripts('/resources/testharness.js');
+}
+
+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(new TypeError(), () => new ReadableStream(null), 'constructor should throw when the source is null');
+
+}, 'ReadableStream can\'t be constructed with garbage');
+
+test(() => {
+
+ assert_throws(new RangeError(), () => new ReadableStream({ type: null }),
+ 'constructor should throw when the type is null');
+ assert_throws(new RangeError(), () => new ReadableStream({ type: '' }),
+ 'constructor should throw when the type is empty string');
+ assert_throws(new RangeError(), () => new ReadableStream({ type: 'asdf' }),
+ 'constructor should throw when the type is asdf');
+
+}, 'ReadableStream can\'t be constructed with an invalid type');
+
+test(() => {
+
+ const methods = ['cancel', 'constructor', 'getReader', 'pipeThrough', 'pipeTo', 'tee'];
+ const properties = methods.concat(['locked']).sort();
+
+ const rs = new ReadableStream();
+ const proto = Object.getPrototypeOf(rs);
+
+ assert_array_equals(Object.getOwnPropertyNames(proto).sort(), properties, 'should have all the correct methods');
+
+ for (const m of methods) {
+ const propDesc = Object.getOwnPropertyDescriptor(proto, m);
+ assert_false(propDesc.enumerable, 'method should be non-enumerable');
+ assert_true(propDesc.configurable, 'method should be configurable');
+ assert_true(propDesc.writable, 'method should be writable');
+ assert_equals(typeof rs[m], 'function', 'method should be a function');
+ }
+
+ const lockedPropDesc = Object.getOwnPropertyDescriptor(proto, 'locked');
+ assert_false(lockedPropDesc.enumerable, 'locked should be non-enumerable');
+ assert_equals(lockedPropDesc.writable, undefined, 'locked should not be a data property');
+ assert_equals(typeof lockedPropDesc.get, 'function', 'locked should have a getter');
+ assert_equals(lockedPropDesc.set, undefined, 'locked should not have a setter');
+ assert_true(lockedPropDesc.configurable, 'locked should be configurable');
+
+ assert_equals(rs.cancel.length, 1, 'cancel should have 1 parameter');
+ assert_equals(rs.constructor.length, 0, 'constructor should have no parameters');
+ assert_equals(rs.getReader.length, 0, 'getReader should have no parameters');
+ assert_equals(rs.pipeThrough.length, 2, 'pipeThrough should have 2 parameters');
+ assert_equals(rs.pipeTo.length, 1, 'pipeTo should have 1 parameter');
+ assert_equals(rs.tee.length, 0, 'tee should have no parameters');
+
+}, 'ReadableStream instances should have the correct list of properties');
+
+test(() => {
+
+ assert_throws(new TypeError(), () => {
+ new ReadableStream({ start: 'potato' });
+ }, 'constructor should throw when start is not a function');
+
+}, 'ReadableStream constructor should throw for non-function start arguments');
+
+test(() => {
+
+ new ReadableStream({ cancel: '2' });
+
+}, 'ReadableStream constructor can get initial garbage as cancel argument');
+
+test(() => {
+
+ new ReadableStream({ pull: { } });
+
+}, 'ReadableStream constructor can get initial garbage as pull argument');
+
+test(() => {
+
+ let startCalled = false;
+
+ const source = {
+ start(controller) {
+ assert_equals(this, source, 'source is this during start');
+
+ const methods = ['close', 'enqueue', 'error', 'constructor'];
+ const properties = ['desiredSize'].concat(methods).sort();
+ const proto = Object.getPrototypeOf(controller);
+
+ assert_array_equals(Object.getOwnPropertyNames(proto).sort(), properties,
+ 'the controller should have the right properties');
+
+ for (const m of methods) {
+ const propDesc = Object.getOwnPropertyDescriptor(proto, m);
+ assert_equals(typeof controller[m], 'function', `should have a ${m} method`);
+ assert_false(propDesc.enumerable, m + ' should be non-enumerable');
+ assert_true(propDesc.configurable, m + ' should be configurable');
+ assert_true(propDesc.writable, m + ' should be writable');
+ }
+
+ const desiredSizePropDesc = Object.getOwnPropertyDescriptor(proto, 'desiredSize');
+ assert_false(desiredSizePropDesc.enumerable, 'desiredSize should be non-enumerable');
+ assert_equals(desiredSizePropDesc.writable, undefined, 'desiredSize should not be a data property');
+ assert_equals(typeof desiredSizePropDesc.get, 'function', 'desiredSize should have a getter');
+ assert_equals(desiredSizePropDesc.set, undefined, 'desiredSize should not have a setter');
+ assert_true(desiredSizePropDesc.configurable, 'desiredSize should be configurable');
+
+ assert_equals(controller.close.length, 0, 'close should have no parameters');
+ assert_equals(controller.constructor.length, 4, 'constructor should have 4 parameter');
+ assert_equals(controller.enqueue.length, 1, 'enqueue should have 1 parameter');
+ assert_equals(controller.error.length, 1, 'error should have 1 parameter');
+
+ startCalled = true;
+ }
+ };
+
+ new ReadableStream(source);
+ assert_true(startCalled);
+
+}, 'ReadableStream start should be called with the proper parameters');
+
+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');
+
+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_true(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_false(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;
+ const startPromise = Promise.resolve();
+
+ new ReadableStream({
+ start() {
+ return startPromise;
+ },
+ pull() {
+ pullCount++;
+ }
+ });
+
+ return startPromise.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 startPromise = Promise.resolve();
+
+ const rs = new ReadableStream({
+ start() {
+ return startPromise;
+ },
+ 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 startPromise.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 startPromise = Promise.resolve();
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.enqueue('a');
+ return startPromise;
+ },
+ pull() {
+ pullCount++;
+ }
+ });
+
+ const read = rs.getReader().read();
+ assert_equals(pullCount, 0, 'calling read() should not cause pull to be called yet');
+
+ return startPromise.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');
+ return startPromise;
+ },
+ pull() {
+ pullCount++;
+ }
+ });
+
+ return startPromise.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 startPromise = Promise.resolve();
+
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ return startPromise;
+ },
+ pull() {
+ ++pullCount;
+ }
+ });
+
+ const reader = rs.getReader();
+ return startPromise.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 startPromise = Promise.resolve();
+
+ const rs = new ReadableStream({
+ start(c) {
+ controller = c;
+ return startPromise;
+ },
+ pull() {
+ ++pullCount;
+ }
+ });
+
+ const reader = rs.getReader();
+
+ return startPromise.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 startPromise = Promise.resolve();
+
+ const rs = new ReadableStream({
+ start() {
+ return startPromise;
+ },
+ pull(c) {
+ c.enqueue(++timesCalled);
+ returnedPromise = new Promise(r => resolve = r);
+ return returnedPromise;
+ }
+ });
+ const reader = rs.getReader();
+
+ return startPromise.then(() => {
+ 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 startPromise = Promise.resolve();
+
+ const rs = new ReadableStream(
+ {
+ start(c) {
+ c.enqueue('a');
+ c.enqueue('b');
+ c.enqueue('c');
+ return startPromise;
+ },
+ pull() {
+ ++timesCalled;
+ }
+ },
+ {
+ size() {
+ return 1;
+ },
+ highWaterMark: Infinity
+ }
+ );
+ const reader = rs.getReader();
+
+ return startPromise.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(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(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(new 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(new 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(() => {
+
+ 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');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/pipe-through.https.html b/testing/web-platform/tests/streams/readable-streams/pipe-through.https.html
new file mode 100644
index 000000000..fbac05a1b
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/pipe-through.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="../resources/test-utils.js"></script>
+<script src="pipe-through.js"></script>
+<script>
+'use strict';
+worker_test('pipe-through.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/pipe-through.js b/testing/web-platform/tests/streams/readable-streams/pipe-through.js
new file mode 100644
index 000000000..4988928e7
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/pipe-through.js
@@ -0,0 +1,108 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('../resources/test-utils.js');
+ self.importScripts('/resources/testharness.js');
+}
+
+test(() => {
+
+ let pipeToArguments;
+ const thisValue = {
+ pipeTo() {
+ pipeToArguments = arguments;
+ }
+ };
+
+ const input = { readable: {}, writable: {} };
+ const options = {};
+ const result = ReadableStream.prototype.pipeThrough.call(thisValue, input, options);
+
+ assert_array_equals(pipeToArguments, [input.writable, options],
+ 'correct arguments should be passed to thisValue.pipeTo');
+ assert_equals(result, input.readable, 'return value should be the passed readable property');
+
+}, 'ReadableStream.prototype.pipeThrough should work generically on its this and its arguments');
+
+test(() => {
+
+ const thisValue = {
+ pipeTo() {
+ assert_unreached('pipeTo should not be called');
+ }
+ };
+
+ methodThrows(ReadableStream.prototype, 'pipeThrough', thisValue, [undefined, {}]);
+ methodThrows(ReadableStream.prototype, 'pipeThrough', thisValue, [null, {}]);
+
+}, 'ReadableStream.prototype.pipeThrough should throw when its first argument is not convertible to an object');
+
+test(() => {
+
+ const args = [{ readable: {}, writable: {} }, {}];
+
+ methodThrows(ReadableStream.prototype, 'pipeThrough', undefined, args);
+ methodThrows(ReadableStream.prototype, 'pipeThrough', null, args);
+ methodThrows(ReadableStream.prototype, 'pipeThrough', 1, args);
+ methodThrows(ReadableStream.prototype, 'pipeThrough', { pipeTo: 'test' }, args);
+
+}, 'ReadableStream.prototype.pipeThrough should throw when "this" has no pipeTo method');
+
+test(() => {
+ const error = new Error('potato');
+
+ const throwingPipeTo = {
+ get pipeTo() {
+ throw error;
+ }
+ };
+ assert_throws(error,
+ () => ReadableStream.prototype.pipeThrough.call(throwingPipeTo, { readable: { }, writable: { } }, {}),
+ 'pipeThrough should rethrow the error thrown by pipeTo');
+
+ const thisValue = {
+ pipeTo() {
+ assert_unreached('pipeTo should not be called');
+ }
+ };
+
+ const throwingWritable = {
+ readable: {},
+ get writable() {
+ throw error;
+ }
+ };
+ assert_throws(error,
+ () => ReadableStream.prototype.pipeThrough.call(thisValue, throwingWritable, {}),
+ 'pipeThrough should rethrow the error thrown by the writable getter');
+
+ const throwingReadable = {
+ get readable() {
+ throw error;
+ },
+ writable: {}
+ };
+ assert_throws(error,
+ () => ReadableStream.prototype.pipeThrough.call(thisValue, throwingReadable, {}),
+ 'pipeThrough should rethrow the error thrown by the readable getter');
+
+}, 'ReadableStream.prototype.pipeThrough should rethrow errors from accessing pipeTo, readable, or writable');
+
+test(() => {
+
+ let count = 0;
+ const thisValue = {
+ pipeTo() {
+ ++count;
+ }
+ };
+
+ ReadableStream.prototype.pipeThrough.call(thisValue, { readable: {}, writable: {} });
+ ReadableStream.prototype.pipeThrough.call(thisValue, { readable: {} }, {});
+ ReadableStream.prototype.pipeThrough.call(thisValue, { writable: {} }, {});
+
+ assert_equals(count, 3, 'pipeTo was called 3 times');
+
+}, 'ReadableStream.prototype.pipeThrough should work with missing readable, writable, or options');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/readable-stream-reader.https.html b/testing/web-platform/tests/streams/readable-streams/readable-stream-reader.https.html
new file mode 100644
index 000000000..cc5613518
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/readable-stream-reader.https.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="readable-stream-reader.js"></script>
+<script>
+'use strict';
+worker_test('readable-stream-reader.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/readable-stream-reader.js b/testing/web-platform/tests/streams/readable-streams/readable-stream-reader.js
new file mode 100644
index 000000000..1d5bc1300
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/readable-stream-reader.js
@@ -0,0 +1,485 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('../resources/rs-utils.js');
+ self.importScripts('/resources/testharness.js');
+}
+
+let ReadableStreamReader;
+
+test(() => {
+
+ // It's not exposed globally, but we test a few of its properties here.
+ ReadableStreamReader = (new ReadableStream()).getReader().constructor;
+
+}, 'Can get the ReadableStreamReader constructor indirectly');
+
+test(() => {
+
+ assert_throws(new TypeError(), () => new ReadableStreamReader('potato'));
+ assert_throws(new TypeError(), () => new ReadableStreamReader({}));
+ assert_throws(new TypeError(), () => new ReadableStreamReader());
+
+}, 'ReadableStreamReader constructor should get a ReadableStream object as argument');
+
+test(() => {
+
+ const methods = ['cancel', 'constructor', 'read', 'releaseLock'];
+ const properties = methods.concat(['closed']).sort();
+
+ const rsReader = new ReadableStreamReader(new ReadableStream());
+ const proto = Object.getPrototypeOf(rsReader);
+
+ assert_array_equals(Object.getOwnPropertyNames(proto).sort(), properties);
+
+ for (const m of methods) {
+ const propDesc = Object.getOwnPropertyDescriptor(proto, m);
+ assert_equals(propDesc.enumerable, false, 'method should be non-enumerable');
+ assert_equals(propDesc.configurable, true, 'method should be configurable');
+ assert_equals(propDesc.writable, true, 'method should be writable');
+ assert_equals(typeof rsReader[m], 'function', 'should have be a method');
+ }
+
+ const closedPropDesc = Object.getOwnPropertyDescriptor(proto, 'closed');
+ assert_equals(closedPropDesc.enumerable, false, 'closed should be non-enumerable');
+ assert_equals(closedPropDesc.configurable, true, 'closed should be configurable');
+ assert_not_equals(closedPropDesc.get, undefined, 'closed should have a getter');
+ assert_equals(closedPropDesc.set, undefined, 'closed should not have a setter');
+
+ assert_equals(rsReader.cancel.length, 1, 'cancel has 1 parameter');
+ assert_not_equals(rsReader.closed, undefined, 'has a non-undefined closed property');
+ assert_equals(typeof rsReader.closed.then, 'function', 'closed property is thenable');
+ assert_equals(typeof rsReader.constructor, 'function', 'has a constructor method');
+ assert_equals(rsReader.constructor.length, 1, 'constructor has 1 parameter');
+ assert_equals(typeof rsReader.read, 'function', 'has a getReader method');
+ assert_equals(rsReader.read.length, 0, 'read has no parameters');
+ assert_equals(typeof rsReader.releaseLock, 'function', 'has a releaseLock method');
+ assert_equals(rsReader.releaseLock.length, 0, 'releaseLock has no parameters');
+
+}, 'ReadableStreamReader instances should have the correct list of properties');
+
+test(() => {
+
+ const rsReader = new ReadableStreamReader(new ReadableStream());
+ assert_equals(rsReader.closed, rsReader.closed, 'closed should return the same promise');
+
+}, 'ReadableStreamReader closed should always return the same promise object');
+
+test(() => {
+
+ const rs = new ReadableStream();
+ new ReadableStreamReader(rs); // Constructing directly the first time should be fine.
+ assert_throws(new TypeError(), () => new ReadableStreamReader(rs),
+ 'constructing directly the second time should fail');
+
+}, 'Constructing a ReadableStreamReader directly should fail if the stream is already locked (via direct ' +
+ 'construction)');
+
+test(() => {
+
+ const rs = new ReadableStream();
+ new ReadableStreamReader(rs); // Constructing directly should be fine.
+ assert_throws(new TypeError(), () => rs.getReader(), 'getReader() should fail');
+
+}, 'Getting a ReadableStreamReader 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(new TypeError(), () => new ReadableStreamReader(rs), 'constructing directly should fail');
+
+}, 'Constructing a ReadableStreamReader directly should fail if the stream is already locked (via getReader)');
+
+test(() => {
+
+ const rs = new ReadableStream();
+ rs.getReader(); // getReader() should be fine.
+ assert_throws(new TypeError(), () => rs.getReader(), 'getReader() should fail');
+
+}, 'Getting a ReadableStreamReader via getReader should fail if the stream is already locked (via getReader)');
+
+test(() => {
+
+ const rs = new ReadableStream({
+ start(c) {
+ c.close();
+ }
+ });
+
+ new ReadableStreamReader(rs); // Constructing directly should not throw.
+
+}, 'Constructing a ReadableStreamReader 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 ReadableStreamReader(rs); // Constructing directly should not throw.
+
+}, 'Constructing a ReadableStreamReader 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(new 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(t, new TypeError(), reader1.closed),
+ reader2.closed
+ ]);
+
+}, 'closed should be rejected after reader releases its lock (multiple stream locks)');
+
+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(t, new 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(t, theError, reader1.closed),
+ promise_rejects(t, theError, reader1.read())
+ );
+
+ assert_throws(new 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(t, theError, reader2.closed),
+ promise_rejects(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;
+
+}, 'ReadableStreamReader 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');
+ }
+ );
+
+}, 'ReadableStreamReader: 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(t, theError, rs.getReader().closed);
+
+ controller.error(theError);
+ return promise;
+
+}, 'Erroring a ReadableStream after checking closed should reject ReadableStreamReader 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(t, theError, rs.getReader().closed);
+
+}, 'Erroring a ReadableStream before checking closed should reject ReadableStreamReader 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(t, myError, reader.read()),
+ promise_rejects(t, myError, reader.read()),
+ promise_rejects(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(t, myError, reader.read()),
+ promise_rejects(t, myError, reader.read()),
+ promise_rejects(t, myError, reader.closed)
+ ]);
+
+ controller.error(myError);
+ return promise;
+
+}, 'Reading twice on a stream that gets errored');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/tee.https.html b/testing/web-platform/tests/streams/readable-streams/tee.https.html
new file mode 100644
index 000000000..37c3e8d82
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/tee.https.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="../resources/rs-utils.js"></script>
+<script src="tee.js"></script>
+<script>
+'use strict';
+worker_test('tee.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/tee.js b/testing/web-platform/tests/streams/readable-streams/tee.js
new file mode 100644
index 000000000..485f2af7a
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/tee.js
@@ -0,0 +1,254 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('../resources/rs-utils.js');
+ self.importScripts('/resources/testharness.js');
+}
+
+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(t, theError, reader1.closed),
+ promise_rejects(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(t, theError, reader2.read());
+ })
+ .then(() => promise_rejects(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 branch2');
+
+promise_test(() => {
+
+ const reason1 = new Error('We\'re wanted men.');
+ const reason2 = new Error('I have the death sentence on twelve systems.');
+
+ let resolve;
+ const promise = new Promise(r => resolve = r);
+ const rs = new ReadableStream({
+ cancel(reason) {
+ assert_array_equals(reason, [reason1, reason2],
+ 'the cancel reason should be an array containing those from the branches');
+ resolve();
+ }
+ });
+
+ const branch = rs.tee();
+ const branch1 = branch[0];
+ const branch2 = branch[1];
+ branch1.cancel(reason1);
+ branch2.cancel(reason2);
+
+ return promise;
+
+}, 'ReadableStream teeing: canceling both branches should aggregate the cancel reasons into an array');
+
+promise_test(t => {
+
+ const theError = { name: 'I\'ll be careful.' };
+ const rs = new ReadableStream({
+ cancel() {
+ throw theError;
+ }
+ });
+
+ const branch = rs.tee();
+ const branch1 = branch[0];
+ const branch2 = branch[1];
+
+ return Promise.all([
+ promise_rejects(t, theError, branch1.cancel()),
+ promise_rejects(t, theError, branch2.cancel())
+ ]);
+
+}, 'ReadableStream teeing: failing to cancel the original stream should cause cancel() to reject on 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(t, theError, reader1.closed),
+ promise_rejects(t, theError, reader2.closed)
+ ]);
+
+ controller.error(theError);
+ return promise;
+
+}, 'ReadableStream teeing: erroring the original should immediately error the branches');
+
+done();
diff --git a/testing/web-platform/tests/streams/readable-streams/templated.https.html b/testing/web-platform/tests/streams/readable-streams/templated.https.html
new file mode 100644
index 000000000..7719eee16
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/templated.https.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/service-workers/service-worker/resources/test-helpers.sub.js"></script>
+<script src="../resources/test-initializer.js"></script>
+
+<script src="../resources/test-utils.js"></script>
+<script src="../resources/rs-test-templates.js"></script>
+<script src="templated.js"></script>
+<script>
+'use strict';
+worker_test('templated.js');
+</script>
diff --git a/testing/web-platform/tests/streams/readable-streams/templated.js b/testing/web-platform/tests/streams/readable-streams/templated.js
new file mode 100644
index 000000000..6db042999
--- /dev/null
+++ b/testing/web-platform/tests/streams/readable-streams/templated.js
@@ -0,0 +1,148 @@
+'use strict';
+
+if (self.importScripts) {
+ self.importScripts('/resources/testharness.js');
+ self.importScripts('../resources/test-utils.js');
+ self.importScripts('../resources/rs-test-templates.js');
+}
+
+// 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() };
+}
+
+done();