diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md
index 337944bf9c1c72..2129ee15c5b8a8 100644
--- a/test/fixtures/wpt/README.md
+++ b/test/fixtures/wpt/README.md
@@ -27,7 +27,7 @@ Last update:
- performance-timeline: https://github.com/web-platform-tests/wpt/tree/94caab7038/performance-timeline
- resource-timing: https://github.com/web-platform-tests/wpt/tree/22d38586d0/resource-timing
- resources: https://github.com/web-platform-tests/wpt/tree/6a2f322376/resources
-- streams: https://github.com/web-platform-tests/wpt/tree/bc9dcbbf1a/streams
+- streams: https://github.com/web-platform-tests/wpt/tree/f8f26a372f/streams
- url: https://github.com/web-platform-tests/wpt/tree/7a3645b79a/url
- urlpattern: https://github.com/web-platform-tests/wpt/tree/f07c03cbed/urlpattern
- user-timing: https://github.com/web-platform-tests/wpt/tree/5ae85bf826/user-timing
diff --git a/test/fixtures/wpt/streams/META.yml b/test/fixtures/wpt/streams/META.yml
index 1259a55cb5a99e..a16a3c47a03559 100644
--- a/test/fixtures/wpt/streams/META.yml
+++ b/test/fixtures/wpt/streams/META.yml
@@ -1,7 +1,6 @@
spec: https://streams.spec.whatwg.org/
suggested_reviewers:
- - domenic
- - yutakahirano
- youennf
- wanderview
- ricea
+ - MattiasBuelens
diff --git a/test/fixtures/wpt/streams/WEB_FEATURES.yml b/test/fixtures/wpt/streams/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..d10bbe4a7d7012
--- /dev/null
+++ b/test/fixtures/wpt/streams/WEB_FEATURES.yml
@@ -0,0 +1,5 @@
+features:
+- name: streams
+ files:
+ # Top-level only. Subdirectories have their own mapping.
+ - "*"
diff --git a/test/fixtures/wpt/streams/piping/WEB_FEATURES.yml b/test/fixtures/wpt/streams/piping/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..8cf3517baf08a4
--- /dev/null
+++ b/test/fixtures/wpt/streams/piping/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: streams
+ files: "**"
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/WEB_FEATURES.yml b/test/fixtures/wpt/streams/readable-byte-streams/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..a35508fc0dc65d
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-byte-streams/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: readable-byte-streams
+ files: "**"
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/bad-buffers-and-views.any.js b/test/fixtures/wpt/streams/readable-byte-streams/bad-buffers-and-views.any.js
index afcc61e6800a28..eae8dde03f6b8d 100644
--- a/test/fixtures/wpt/streams/readable-byte-streams/bad-buffers-and-views.any.js
+++ b/test/fixtures/wpt/streams/readable-byte-streams/bad-buffers-and-views.any.js
@@ -123,8 +123,7 @@ promise_test(t => {
async_test(t => {
const stream = new ReadableStream({
pull: t.step_func_done(c => {
- // Detach it by reading into it
- reader.read(c.byobRequest.view);
+ c.byobRequest.view.buffer.transfer();
assert_throws_js(TypeError, () => c.byobRequest.respond(1),
'respond() must throw if the corresponding view has become detached');
@@ -141,9 +140,7 @@ async_test(t => {
const stream = new ReadableStream({
pull: t.step_func_done(c => {
c.close();
-
- // Detach it by reading into it
- reader.read(c.byobRequest.view);
+ c.byobRequest.view.buffer.transfer();
assert_throws_js(TypeError, () => c.byobRequest.respond(0),
'respond() must throw if the corresponding view has become detached');
@@ -159,9 +156,8 @@ async_test(t => {
async_test(t => {
const stream = new ReadableStream({
pull: t.step_func_done(c => {
- // Detach it by reading into it
const view = new Uint8Array([1, 2, 3]);
- reader.read(view);
+ view.buffer.transfer();
assert_throws_js(TypeError, () => c.byobRequest.respondWithNewView(view));
}),
@@ -364,8 +360,7 @@ async_test(t => {
async_test(t => {
const stream = new ReadableStream({
pull: t.step_func_done(c => {
- // Detach it by reading into it
- reader.read(c.byobRequest.view);
+ c.byobRequest.view.buffer.transfer();
assert_throws_js(TypeError, () => c.enqueue(new Uint8Array([1])),
'enqueue() must throw if the BYOB request\'s buffer has become detached');
@@ -382,9 +377,7 @@ async_test(t => {
const stream = new ReadableStream({
pull: t.step_func_done(c => {
c.close();
-
- // Detach it by reading into it
- reader.read(c.byobRequest.view);
+ c.byobRequest.view.buffer.transfer();
assert_throws_js(TypeError, () => c.enqueue(new Uint8Array([1])),
'enqueue() must throw if the BYOB request\'s buffer has become detached');
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/crashtests/tee-locked-stream.any.js b/test/fixtures/wpt/streams/readable-byte-streams/crashtests/tee-locked-stream.any.js
new file mode 100644
index 00000000000000..285b427e277862
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-byte-streams/crashtests/tee-locked-stream.any.js
@@ -0,0 +1,9 @@
+// META: global=window,worker
+'use strict';
+
+test(() => {
+ const byteReadable = new ReadableStream({type: 'bytes'});
+ byteReadable.getReader();
+ assert_throws_js(TypeError, () => byteReadable.tee(), 'byteReadable.tee() must throw');
+}, 'tee() on a locked byte stream does not crash');
+
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/general.any.js b/test/fixtures/wpt/streams/readable-byte-streams/general.any.js
index 4b0c73865f7cf9..50f4f019ff220f 100644
--- a/test/fixtures/wpt/streams/readable-byte-streams/general.any.js
+++ b/test/fixtures/wpt/streams/readable-byte-streams/general.any.js
@@ -236,6 +236,29 @@ promise_test(t => {
});
}, 'ReadableStream with byte source: Test that erroring a stream does not release a BYOB reader automatically');
+promise_test(async t => {
+ const rs = new ReadableStream({
+ type: 'bytes',
+ start(c) {
+ c.enqueue(new Uint8Array([1, 2, 3]));
+ }
+ });
+
+ const reader1 = rs.getReader({mode: 'byob'});
+ reader1.releaseLock();
+
+ const reader2 = rs.getReader({mode: 'byob'});
+
+ // Should be a no-op
+ reader1.releaseLock();
+
+ const result = await reader2.read(new Uint8Array([0, 0, 0]));
+ assert_typed_array_equals(result.value, new Uint8Array([1, 2, 3]),
+ 'read() should still work on reader2 even after reader1 is released');
+ assert_false(result.done, 'done');
+
+}, 'ReadableStream with byte source: cannot use an already-released BYOB reader to unlock a stream again');
+
promise_test(async t => {
const stream = new ReadableStream({
type: 'bytes'
@@ -992,6 +1015,69 @@ promise_test(() => {
}, 'ReadableStream with byte source: respond(3) to read(view) with 2 element Uint16Array enqueues the 1 byte ' +
'remainder');
+promise_test(() => {
+ let pullCount = 0;
+
+ let controller;
+ let byobRequest;
+ let viewInfo;
+
+ const stream = new ReadableStream({
+ start(c) {
+ controller = c;
+ },
+ pull() {
+ ++pullCount;
+
+ byobRequest = controller.byobRequest;
+ const view = byobRequest.view;
+ viewInfo = extractViewInfo(view);
+
+ view[0] = 0x01;
+ view[1] = 0x02;
+ view[2] = 0x03;
+
+ controller.byobRequest.respond(3);
+ },
+ type: 'bytes'
+ });
+
+ const reader = stream.getReader({ mode: 'byob' });
+ const read1 = reader.read(new Uint16Array(2));
+ const read2 = reader.read(new Uint8Array(1));
+
+ return read1.then(result => {
+ assert_equals(pullCount, 1);
+
+ assert_false(result.done, 'done');
+
+ const view = result.value;
+ assert_equals(view.byteOffset, 0, 'byteOffset');
+ assert_equals(view.byteLength, 2, 'byteLength');
+
+ const dataView = new DataView(view.buffer, view.byteOffset, view.byteLength);
+ assert_equals(dataView.getUint16(0), 0x0102);
+
+ return read2;
+ }).then(result => {
+ assert_equals(pullCount, 1);
+ assert_not_equals(byobRequest, null, 'byobRequest must not be null');
+ assert_equals(viewInfo.constructor, Uint8Array, 'view.constructor should be Uint8Array');
+ assert_equals(viewInfo.bufferByteLength, 4, 'view.buffer.byteLength should be 4');
+ assert_equals(viewInfo.byteOffset, 0, 'view.byteOffset should be 0');
+ assert_equals(viewInfo.byteLength, 4, 'view.byteLength should be 4');
+
+ assert_false(result.done, 'done');
+
+ const view = result.value;
+ assert_equals(view.byteOffset, 0, 'byteOffset');
+ assert_equals(view.byteLength, 1, 'byteLength');
+
+ assert_equals(view[0], 0x03);
+ });
+}, 'ReadableStream with byte source: respond(3) to read(view) with 2 element Uint16Array fulfills second read(view) ' +
+ 'with the 1 byte remainder');
+
promise_test(t => {
const stream = new ReadableStream({
start(controller) {
diff --git a/test/fixtures/wpt/streams/readable-byte-streams/templated.any.js b/test/fixtures/wpt/streams/readable-byte-streams/templated.any.js
new file mode 100644
index 00000000000000..e4c10f7685962e
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-byte-streams/templated.any.js
@@ -0,0 +1,24 @@
+// META: global=window,worker,shadowrealm
+// META: script=../resources/test-utils.js
+// META: script=../resources/rs-test-templates.js
+'use strict';
+
+templatedRSEmpty('ReadableStream with byte source (empty)', () => {
+ return new ReadableStream({ type: 'bytes' });
+});
+
+templatedRSEmptyReader('ReadableStream with byte source (empty) default reader', () => {
+ const stream = new ReadableStream({ type: 'bytes' });
+ const reader = stream.getReader();
+ return { stream, reader, read: () => reader.read() };
+});
+
+templatedRSEmptyReader('ReadableStream with byte source (empty) BYOB reader', () => {
+ const stream = new ReadableStream({ type: 'bytes' });
+ const reader = stream.getReader({ mode: 'byob' });
+ return { stream, reader, read: () => reader.read(new Uint8Array([0])) };
+});
+
+templatedRSThrowAfterCloseOrError('ReadableStream with byte source', (extras) => {
+ return new ReadableStream({ type: 'bytes', ...extras });
+});
diff --git a/test/fixtures/wpt/streams/readable-streams/WEB_FEATURES.yml b/test/fixtures/wpt/streams/readable-streams/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..ad2cb549631fd2
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-streams/WEB_FEATURES.yml
@@ -0,0 +1,14 @@
+features:
+- name: streams
+ files:
+ - "*"
+ - "!async-iterator.any.js"
+ - "!from.any.js"
+ # 'owning' type is not yet standardized
+ - "!owning-type*"
+- name: async-iterable-streams
+ files:
+ - async-iterator.any.js
+- name: readablestream-from
+ files:
+ - from.any.js
diff --git a/test/fixtures/wpt/streams/readable-streams/crashtests/WEB_FEATURES.yml b/test/fixtures/wpt/streams/readable-streams/crashtests/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..f4eaacc3edb435
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-streams/crashtests/WEB_FEATURES.yml
@@ -0,0 +1,7 @@
+features:
+- name: streams
+ files:
+ - strategy-worker-terminate.html
+- name: readablestream-from
+ files:
+ - from-cross-realm.https.html
diff --git a/test/fixtures/wpt/streams/readable-streams/crashtests/exception-during-size-while-pulling.html b/test/fixtures/wpt/streams/readable-streams/crashtests/exception-during-size-while-pulling.html
new file mode 100644
index 00000000000000..1229b690d3910b
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-streams/crashtests/exception-during-size-while-pulling.html
@@ -0,0 +1,36 @@
+
+
+
+
ReadableStream size algorithm throws during enqueue
+
diff --git a/test/fixtures/wpt/streams/readable-streams/crashtests/garbage-collection.any.js b/test/fixtures/wpt/streams/readable-streams/crashtests/garbage-collection.any.js
new file mode 100644
index 00000000000000..6e9d80c41425b1
--- /dev/null
+++ b/test/fixtures/wpt/streams/readable-streams/crashtests/garbage-collection.any.js
@@ -0,0 +1,38 @@
+// META: global=window,worker
+// META: script=/common/gc.js
+'use strict';
+
+// See https://crbug.com/335506658 for details.
+promise_test(async () => {
+ const closed = new ReadableStream({
+ pull(controller) {
+ controller.enqueue('is there anybody in there?');
+ }
+ }).getReader().closed;
+ // 3 GCs are actually required to trigger the bug at time of writing.
+ for (let i = 0; i < 5; ++i)
+ await garbageCollect();
+}, 'Garbage-collecting a stream along with its reader should not crash');
+
+promise_test(async () => {
+ let reader = new ReadableStream({
+ pull() { }
+ }).getReader();
+ const promise = reader.read();
+ reader = null;
+ for (let i = 0; i < 5; ++i)
+ await garbageCollect();
+}, 'Garbage-collecting a stream with a pending read should not crash');
+
+promise_test(async () => {
+ let reader = new ReadableStream({
+ type: "bytes",
+ pull() { return new Promise(resolve => {}); }
+ }).getReader({mode: "byob"});
+ const promise = reader.read(new Uint8Array(42));
+ reader = null;
+ for (let i = 0; i < 5; ++i)
+ await garbageCollect();
+}, 'Garbage-collecting a stream with a pending BYOB read should not crash');
+
+
diff --git a/test/fixtures/wpt/streams/readable-streams/from.any.js b/test/fixtures/wpt/streams/readable-streams/from.any.js
index 2a4212ab890606..2b28a05fee398c 100644
--- a/test/fixtures/wpt/streams/readable-streams/from.any.js
+++ b/test/fixtures/wpt/streams/readable-streams/from.any.js
@@ -150,6 +150,12 @@ const badIterables = [
['an object with a non-callable @@asyncIterator method', {
[Symbol.asyncIterator]: 42
}],
+ ['an object with an @@iterator method returning a non-object', {
+ [Symbol.iterator]: () => 42
+ }],
+ ['an object with an @@asyncIterator method returning a non-object', {
+ [Symbol.asyncIterator]: () => 42
+ }],
];
for (const [label, iterable] of badIterables) {
@@ -244,6 +250,64 @@ promise_test(async t => {
}, `ReadableStream.from: stream errors when next() rejects`);
+promise_test(async t => {
+ const theError = new Error('a unique string');
+
+ const iterable = {
+ next() {
+ throw theError;
+ },
+ [Symbol.asyncIterator]: () => iterable
+ };
+
+ const rs = ReadableStream.from(iterable);
+ const reader = rs.getReader();
+
+ await Promise.all([
+ promise_rejects_exactly(t, theError, reader.read()),
+ promise_rejects_exactly(t, theError, reader.closed)
+ ]);
+
+}, 'ReadableStream.from: stream errors when next() throws synchronously');
+
+promise_test(async t => {
+
+ const iterable = {
+ next() {
+ return 42; // not a promise or an iterator result
+ },
+ [Symbol.asyncIterator]: () => iterable
+ };
+
+ const rs = ReadableStream.from(iterable);
+ const reader = rs.getReader();
+
+ await Promise.all([
+ promise_rejects_js(t, TypeError, reader.read()),
+ promise_rejects_js(t, TypeError, reader.closed)
+ ]);
+
+}, 'ReadableStream.from: stream errors when next() returns a non-object');
+
+promise_test(async t => {
+
+ const iterable = {
+ next() {
+ return Promise.resolve(42); // not an iterator result
+ },
+ [Symbol.asyncIterator]: () => iterable
+ };
+
+ const rs = ReadableStream.from(iterable);
+ const reader = rs.getReader();
+
+ await Promise.all([
+ promise_rejects_js(t, TypeError, reader.read()),
+ promise_rejects_js(t, TypeError, reader.closed)
+ ]);
+
+}, 'ReadableStream.from: stream errors when next() fulfills with a non-object');
+
promise_test(async t => {
const iterable = {
@@ -360,6 +424,93 @@ promise_test(async t => {
}, `ReadableStream.from: return() is not called when iterator completes normally`);
+promise_test(async t => {
+
+ const theError = new Error('a unique string');
+
+ const iterable = {
+ next: t.unreached_func('next() should not be called'),
+ throw: t.unreached_func('throw() should not be called'),
+ // no return method
+ [Symbol.asyncIterator]: () => iterable
+ };
+
+ const rs = ReadableStream.from(iterable);
+ const reader = rs.getReader();
+
+ await Promise.all([
+ reader.cancel(theError),
+ reader.closed
+ ]);
+
+}, `ReadableStream.from: cancel() resolves when return() method is missing`);
+
+promise_test(async t => {
+
+ const theError = new Error('a unique string');
+
+ const iterable = {
+ next: t.unreached_func('next() should not be called'),
+ throw: t.unreached_func('throw() should not be called'),
+ return: 42,
+ [Symbol.asyncIterator]: () => iterable
+ };
+
+ const rs = ReadableStream.from(iterable);
+ const reader = rs.getReader();
+
+ await promise_rejects_js(t, TypeError, reader.cancel(theError), 'cancel() should reject with a TypeError');
+
+ await reader.closed;
+
+}, `ReadableStream.from: cancel() rejects when return() is not a method`);
+
+promise_test(async t => {
+
+ const cancelReason = new Error('cancel reason');
+ const rejectError = new Error('reject error');
+
+ const iterable = {
+ next: t.unreached_func('next() should not be called'),
+ throw: t.unreached_func('throw() should not be called'),
+ async return() {
+ throw rejectError;
+ },
+ [Symbol.asyncIterator]: () => iterable
+ };
+
+ const rs = ReadableStream.from(iterable);
+ const reader = rs.getReader();
+
+ await promise_rejects_exactly(t, rejectError, reader.cancel(cancelReason), 'cancel() should reject with error from return()');
+
+ await reader.closed;
+
+}, `ReadableStream.from: cancel() rejects when return() rejects`);
+
+promise_test(async t => {
+
+ const cancelReason = new Error('cancel reason');
+ const rejectError = new Error('reject error');
+
+ const iterable = {
+ next: t.unreached_func('next() should not be called'),
+ throw: t.unreached_func('throw() should not be called'),
+ return() {
+ throw rejectError;
+ },
+ [Symbol.asyncIterator]: () => iterable
+ };
+
+ const rs = ReadableStream.from(iterable);
+ const reader = rs.getReader();
+
+ await promise_rejects_exactly(t, rejectError, reader.cancel(cancelReason), 'cancel() should reject with error from return()');
+
+ await reader.closed;
+
+}, `ReadableStream.from: cancel() rejects when return() throws synchronously`);
+
promise_test(async t => {
const theError = new Error('a unique string');
diff --git a/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js b/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js
index 13bd1fb3437877..abcb4cf21b8955 100644
--- a/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js
+++ b/test/fixtures/wpt/streams/readable-streams/garbage-collection.any.js
@@ -69,3 +69,22 @@ promise_test(async () => {
'old reader should still be locking the stream even after garbage collection'));
}, 'Garbage-collecting a ReadableStreamDefaultReader should not unlock its stream');
+
+promise_test(async () => {
+
+ const promise = (() => {
+ const rs = new ReadableStream({
+ pull(controller) {
+ controller.enqueue('words');
+ }
+ });
+ const reader = rs.getReader();
+ return reader.read();
+ })();
+ await garbageCollect();
+ const {value, done} = await promise;
+ // If we get here, the test passed.
+ assert_equals(value, 'words', 'value should be words');
+ assert_false(done, 'we should not be done');
+
+}, 'A ReadableStream and its reader should not be garbage collected while there is a read promise pending');
diff --git a/test/fixtures/wpt/streams/readable-streams/templated.any.js b/test/fixtures/wpt/streams/readable-streams/templated.any.js
index dc75b805a10d63..4f2440c0880bb7 100644
--- a/test/fixtures/wpt/streams/readable-streams/templated.any.js
+++ b/test/fixtures/wpt/streams/readable-streams/templated.any.js
@@ -13,7 +13,9 @@ templatedRSEmpty('ReadableStream (empty)', () => {
});
templatedRSEmptyReader('ReadableStream (empty) reader', () => {
- return streamAndDefaultReader(new ReadableStream());
+ const stream = new ReadableStream();
+ const reader = stream.getReader();
+ return { stream, reader, read: () => reader.read() };
});
templatedRSClosed('ReadableStream (closed via call in start)', () => {
@@ -138,6 +140,10 @@ templatedRSTwoChunksClosedReader('ReadableStream (two chunks enqueued, then clos
return result;
}, chunks);
+templatedRSThrowAfterCloseOrError('ReadableStream', (extras) => {
+ return new ReadableStream({ ...extras });
+});
+
function streamAndDefaultReader(stream) {
return { stream, reader: stream.getReader() };
}
diff --git a/test/fixtures/wpt/streams/resources/rs-test-templates.js b/test/fixtures/wpt/streams/resources/rs-test-templates.js
index 25751c477f5dc8..73ef0463768d23 100644
--- a/test/fixtures/wpt/streams/resources/rs-test-templates.js
+++ b/test/fixtures/wpt/streams/resources/rs-test-templates.js
@@ -200,7 +200,7 @@ self.templatedRSEmptyReader = (label, factory) => {
test(() => {
- const stream = factory().stream;
+ const { stream } = factory();
assert_true(stream.locked, 'locked getter should return true');
@@ -208,9 +208,9 @@ self.templatedRSEmptyReader = (label, factory) => {
promise_test(t => {
- const reader = factory().reader;
+ const { read } = factory();
- reader.read().then(
+ read().then(
t.unreached_func('read() should not fulfill'),
t.unreached_func('read() should not reject')
);
@@ -221,14 +221,14 @@ self.templatedRSEmptyReader = (label, factory) => {
promise_test(t => {
- const reader = factory().reader;
+ const { read } = factory();
- reader.read().then(
+ read().then(
t.unreached_func('read() should not fulfill'),
t.unreached_func('read() should not reject')
);
- reader.read().then(
+ read().then(
t.unreached_func('read() should not fulfill'),
t.unreached_func('read() should not reject')
);
@@ -239,26 +239,24 @@ self.templatedRSEmptyReader = (label, factory) => {
test(() => {
- const reader = factory().reader;
- assert_not_equals(reader.read(), reader.read(), 'the promises returned should be distinct');
+ const { read } = factory();
+ assert_not_equals(read(), read(), 'the promises returned should be distinct');
}, label + ': read() should return distinct promises each time');
test(() => {
- const stream = factory().stream;
+ const { stream } = factory();
assert_throws_js(TypeError, () => stream.getReader(), 'stream.getReader() should throw a TypeError');
}, label + ': getReader() again on the stream should fail');
promise_test(async t => {
- const streamAndReader = factory();
- const stream = streamAndReader.stream;
- const reader = streamAndReader.reader;
+ const { stream, reader, read } = factory();
- const read1 = reader.read();
- const read2 = reader.read();
+ const read1 = read();
+ const read2 = read();
const closed = reader.closed;
reader.releaseLock();
@@ -275,19 +273,19 @@ self.templatedRSEmptyReader = (label, factory) => {
promise_test(t => {
- const reader = factory().reader;
+ const { reader, read } = factory();
reader.releaseLock();
return Promise.all([
- promise_rejects_js(t, TypeError, reader.read()),
- promise_rejects_js(t, TypeError, reader.read())
+ promise_rejects_js(t, TypeError, read()),
+ promise_rejects_js(t, TypeError, read())
]);
}, label + ': releasing the lock should cause further read() calls to reject with a TypeError');
promise_test(t => {
- const reader = factory().reader;
+ const { reader } = factory();
const closedBefore = reader.closed;
reader.releaseLock();
@@ -301,9 +299,7 @@ self.templatedRSEmptyReader = (label, factory) => {
test(() => {
- const streamAndReader = factory();
- const stream = streamAndReader.stream;
- const reader = streamAndReader.reader;
+ const { stream, reader } = factory();
reader.releaseLock();
assert_false(stream.locked, 'locked getter should return false');
@@ -312,10 +308,10 @@ self.templatedRSEmptyReader = (label, factory) => {
promise_test(() => {
- const reader = factory().reader;
+ const { reader, read } = factory();
reader.cancel();
- return reader.read().then(r => {
+ return read().then(r => {
assert_object_equals(r, { value: undefined, done: true }, 'read()ing from the reader should give a done result');
});
@@ -323,7 +319,7 @@ self.templatedRSEmptyReader = (label, factory) => {
promise_test(t => {
- const stream = factory().stream;
+ const { stream } = factory();
return promise_rejects_js(t, TypeError, stream.cancel());
}, label + ': canceling via the stream should fail');
@@ -719,3 +715,62 @@ self.templatedRSTeeCancel = (label, factory) => {
}, `${label}: erroring a teed stream should properly handle canceled branches`);
};
+
+self.templatedRSThrowAfterCloseOrError = (label, factory) => {
+ test(() => {}, 'Running templatedRSThrowAfterCloseOrError with ' + label);
+
+ const theError = new Error('a unique string');
+
+ promise_test(async t => {
+ let controller;
+ const stream = factory({
+ start: t.step_func((c) => {
+ controller = c;
+ })
+ });
+
+ controller.close();
+
+ assert_throws_js(TypeError, () => controller.enqueue(new Uint8Array([1])));
+ }, `${label}: enqueue() throws after close()`);
+
+ promise_test(async t => {
+ let controller;
+ const stream = factory({
+ start: t.step_func((c) => {
+ controller = c;
+ })
+ });
+
+ controller.enqueue(new Uint8Array([1]));
+ controller.close();
+
+ assert_throws_js(TypeError, () => controller.enqueue(new Uint8Array([2])));
+ }, `${label}: enqueue() throws after enqueue() and close()`);
+
+ promise_test(async t => {
+ let controller;
+ const stream = factory({
+ start: t.step_func((c) => {
+ controller = c;
+ })
+ });
+
+ controller.error(theError);
+
+ assert_throws_js(TypeError, () => controller.enqueue(new Uint8Array([1])));
+ }, `${label}: enqueue() throws after error()`);
+
+ promise_test(async t => {
+ let controller;
+ const stream = factory({
+ start: t.step_func((c) => {
+ controller = c;
+ })
+ });
+
+ controller.error(theError);
+
+ assert_throws_js(TypeError, () => controller.close());
+ }, `${label}: close() throws after error()`);
+};
diff --git a/test/fixtures/wpt/streams/transferable/WEB_FEATURES.yml b/test/fixtures/wpt/streams/transferable/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..4ecacf5edbc7da
--- /dev/null
+++ b/test/fixtures/wpt/streams/transferable/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: transferable-streams
+ files: "**"
diff --git a/test/fixtures/wpt/streams/transform-streams/WEB_FEATURES.yml b/test/fixtures/wpt/streams/transform-streams/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..f39dc16de4c731
--- /dev/null
+++ b/test/fixtures/wpt/streams/transform-streams/WEB_FEATURES.yml
@@ -0,0 +1,8 @@
+features:
+- name: streams
+ files:
+ - "*"
+ - "!cancel.any.js"
+- name: transformstream-transformer-cancel
+ files:
+ - cancel.any.js
diff --git a/test/fixtures/wpt/streams/transform-streams/cancel.any.js b/test/fixtures/wpt/streams/transform-streams/cancel.any.js
index 5c7fc4eae5d55b..9b369bfb7c68b4 100644
--- a/test/fixtures/wpt/streams/transform-streams/cancel.any.js
+++ b/test/fixtures/wpt/streams/transform-streams/cancel.any.js
@@ -113,3 +113,93 @@ promise_test(async t => {
const closePromise = ts.readable.cancel(1);
await promise_rejects_exactly(t, thrownError, closePromise, 'closePromise should reject with thrownError');
}, 'writable.abort() and readable.cancel() should reject if a transformer.cancel() calls controller.error()');
+
+promise_test(async t => {
+ const cancelReason = new Error('cancel reason');
+ let controller;
+ let cancelPromise;
+ let flushCalled = false;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ },
+ flush() {
+ flushCalled = true;
+ cancelPromise = ts.readable.cancel(cancelReason);
+ },
+ cancel: t.unreached_func('cancel should not be called')
+ });
+ await flushAsyncEvents(); // ensure stream is started
+ await ts.writable.close();
+ assert_true(flushCalled, 'flush() was called');
+ await cancelPromise;
+}, 'readable.cancel() should not call cancel() when flush() is already called from writable.close()');
+
+promise_test(async t => {
+ const cancelReason = new Error('cancel reason');
+ const abortReason = new Error('abort reason');
+ let cancelCalls = 0;
+ let controller;
+ let cancelPromise;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ },
+ cancel() {
+ if (++cancelCalls === 1) {
+ cancelPromise = ts.readable.cancel(cancelReason);
+ }
+ },
+ flush: t.unreached_func('flush should not be called')
+ });
+ await flushAsyncEvents(); // ensure stream is started
+ await ts.writable.abort(abortReason);
+ assert_equals(cancelCalls, 1);
+ await cancelPromise;
+ assert_equals(cancelCalls, 1);
+}, 'readable.cancel() should not call cancel() again when already called from writable.abort()');
+
+promise_test(async t => {
+ const cancelReason = new Error('cancel reason');
+ let controller;
+ let closePromise;
+ let cancelCalled = false;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ },
+ cancel() {
+ cancelCalled = true;
+ closePromise = ts.writable.close();
+ },
+ flush: t.unreached_func('flush should not be called')
+ });
+ await flushAsyncEvents(); // ensure stream is started
+ await ts.readable.cancel(cancelReason);
+ assert_true(cancelCalled, 'cancel() was called');
+ await closePromise;
+}, 'writable.close() should not call flush() when cancel() is already called from readable.cancel()');
+
+promise_test(async t => {
+ const cancelReason = new Error('cancel reason');
+ const abortReason = new Error('abort reason');
+ let cancelCalls = 0;
+ let controller;
+ let abortPromise;
+ const ts = new TransformStream({
+ start(c) {
+ controller = c;
+ },
+ cancel() {
+ if (++cancelCalls === 1) {
+ abortPromise = ts.writable.abort(abortReason);
+ }
+ },
+ flush: t.unreached_func('flush should not be called')
+ });
+ await flushAsyncEvents(); // ensure stream is started
+ await promise_rejects_exactly(t, abortReason, ts.readable.cancel(cancelReason));
+ assert_equals(cancelCalls, 1);
+ await promise_rejects_exactly(t, abortReason, abortPromise);
+ assert_equals(cancelCalls, 1);
+}, 'writable.abort() should not call cancel() again when already called from readable.cancel()');
diff --git a/test/fixtures/wpt/streams/writable-streams/WEB_FEATURES.yml b/test/fixtures/wpt/streams/writable-streams/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..8cf3517baf08a4
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: streams
+ files: "**"
diff --git a/test/fixtures/wpt/streams/writable-streams/aborting.any.js b/test/fixtures/wpt/streams/writable-streams/aborting.any.js
index 9171dbe158f00c..9d8e80b1de63c5 100644
--- a/test/fixtures/wpt/streams/writable-streams/aborting.any.js
+++ b/test/fixtures/wpt/streams/writable-streams/aborting.any.js
@@ -1472,16 +1472,96 @@ promise_test(async t => {
promise_test(async t => {
let ctrl;
+ let abortPromise;
+ let abortPromiseFromSignal;
+ const e1 = SyntaxError();
+ const e2 = TypeError();
+ const ws = new WritableStream({
+ start(c) { ctrl = c; },
+ });
+
+ const writer = ws.getWriter();
+ ctrl.signal.addEventListener('abort', () => {
+ abortPromiseFromSignal = writer.abort(e2);
+ });
+ abortPromise = writer.abort(e1);
+ assert_true(ctrl.signal.aborted);
+
+ await Promise.all([
+ abortPromise,
+ abortPromiseFromSignal,
+ promise_rejects_exactly(t, e2, writer.closed, 'closed')
+ ]);
+}, 'recursive abort() call from abort() aborting signal (not started)');
+
+promise_test(async t => {
+ let ctrl;
+ let abortPromise;
+ let abortPromiseFromSignal;
const e1 = SyntaxError();
const e2 = TypeError();
const ws = new WritableStream({
start(c) { ctrl = c; },
});
+ await flushAsyncEvents(); // ensure stream is started
+
+ const writer = ws.getWriter();
+ ctrl.signal.addEventListener('abort', () => {
+ abortPromiseFromSignal = writer.abort(e2);
+ });
+ abortPromise = writer.abort(e1);
+ assert_true(ctrl.signal.aborted);
+
+ await Promise.all([
+ abortPromise,
+ abortPromiseFromSignal,
+ promise_rejects_exactly(t, e2, writer.closed, 'closed')
+ ]);
+}, 'recursive abort() call from abort() aborting signal');
+
+promise_test(async t => {
+ let ctrl;
+ let abortPromise;
+ let closePromiseFromSignal;
+ const theError = SyntaxError();
+ const ws = new WritableStream({
+ start(c) { ctrl = c; },
+ });
+
+ const writer = ws.getWriter();
+ ctrl.signal.addEventListener('abort', () => {
+ closePromiseFromSignal = writer.close();
+ });
+ abortPromise = writer.abort(theError);
+ assert_true(ctrl.signal.aborted);
+
+ await Promise.all([
+ abortPromise,
+ promise_rejects_exactly(t, theError, closePromiseFromSignal, 'closed'),
+ promise_rejects_exactly(t, theError, writer.closed, 'closed')
+ ]);
+}, 'recursive close() call from abort() aborting signal (not started)');
+
+promise_test(async t => {
+ let ctrl;
+ let abortPromise;
+ let closePromiseFromSignal;
+ const theError = SyntaxError();
+ const ws = new WritableStream({
+ start(c) { ctrl = c; },
+ });
+ await flushAsyncEvents(); // ensure stream is started
const writer = ws.getWriter();
- ctrl.signal.addEventListener('abort', () => writer.abort(e2));
- writer.abort(e1);
+ ctrl.signal.addEventListener('abort', () => {
+ closePromiseFromSignal = writer.close();
+ });
+ abortPromise = writer.abort(theError);
assert_true(ctrl.signal.aborted);
- await promise_rejects_exactly(t, e2, writer.closed, 'closed');
-}, 'recursive abort() call');
+ await Promise.all([
+ abortPromise,
+ closePromiseFromSignal,
+ writer.closed
+ ]);
+}, 'recursive close() call from abort() aborting signal');
diff --git a/test/fixtures/wpt/streams/writable-streams/close.any.js b/test/fixtures/wpt/streams/writable-streams/close.any.js
index 45261e7ca7e01d..3b2c07cc3bfea1 100644
--- a/test/fixtures/wpt/streams/writable-streams/close.any.js
+++ b/test/fixtures/wpt/streams/writable-streams/close.any.js
@@ -468,3 +468,14 @@ promise_test(t => {
return promise_rejects_js(t, TypeError, ws.close(), 'close should reject');
}, 'close() on a stream with a pending close should reject');
+
+// See https://github.com/whatwg/streams/issues/1341.
+promise_test(async t => {
+ const ws = new WritableStream();
+ const writer = ws.getWriter();
+
+ await writer.write(1);
+ await writer.close();
+
+ return promise_rejects_js(t, TypeError, writer.write(2), 'write should reject');
+}, 'write() on a closed stream should reject');
diff --git a/test/fixtures/wpt/streams/writable-streams/constructor.any.js b/test/fixtures/wpt/streams/writable-streams/constructor.any.js
index 0abc7ef545ea0b..bb382a33b8e0c4 100644
--- a/test/fixtures/wpt/streams/writable-streams/constructor.any.js
+++ b/test/fixtures/wpt/streams/writable-streams/constructor.any.js
@@ -87,6 +87,10 @@ test(() => {
new WritableStream();
}, 'WritableStream should be constructible with no arguments');
+test(() => {
+ assert_throws_js(RangeError, () => new WritableStream({ type: 'bytes' }), 'constructor should throw');
+}, `WritableStream can't be constructed with a defined type`);
+
test(() => {
const underlyingSink = { get start() { throw error1; } };
const queuingStrategy = { highWaterMark: 0, get size() { throw error2; } };
diff --git a/test/fixtures/wpt/streams/writable-streams/crashtests/garbage-collection.any.js b/test/fixtures/wpt/streams/writable-streams/crashtests/garbage-collection.any.js
new file mode 100644
index 00000000000000..9f64e9b7a8ac7b
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/crashtests/garbage-collection.any.js
@@ -0,0 +1,90 @@
+// META: global=window,worker
+// META: script=/common/gc.js
+'use strict';
+
+// See https://crbug.com/390646657 for details.
+promise_test(async () => {
+ const written = new WritableStream({
+ write(chunk) {
+ return new Promise(resolve => {});
+ }
+ }).getWriter().write('just nod if you can hear me');
+ for (let i = 0; i < 5; ++i)
+ await garbageCollect();
+}, 'Garbage-collecting a stream writer with a pending write should not crash');
+
+promise_test(async () => {
+ const closed = new WritableStream({
+ write(chunk) { }
+ }).getWriter().closed;
+ for (let i = 0; i < 5; ++i)
+ await garbageCollect();
+}, 'Garbage-collecting a stream writer should not crash with closed promise is retained');
+
+promise_test(async () => {
+ let writer = new WritableStream({
+ write(chunk) { return new Promise(resolve => {}); },
+ close() { return new Promise(resolve => {}); }
+ }).getWriter();
+ writer.write('is there anyone home?');
+ writer.close();
+ writer = null;
+ for (let i = 0; i < 5; ++i)
+ await garbageCollect();
+}, 'Garbage-collecting a stream writer should not crash with close promise pending');
+
+promise_test(async () => {
+ const ready = new WritableStream({
+ write(chunk) { }
+ }, {highWaterMark: 0}).getWriter().ready;
+ for (let i = 0; i < 5; ++i)
+ await garbageCollect();
+}, 'Garbage-collecting a stream writer should not crash when backpressure is being applied');
+
+// Repro for https://crbug.com/455800266
+promise_test(async () => {
+ // This logic is wrapped in a function to make it easy to garbage collect all
+ // references to the WritableStream.
+ const createWritableStream = async () => {
+ const WRITE_COUNT = 2;
+ let writes_done = 0;
+ const { promise, resolve } = Promise.withResolvers();
+
+ const ws = new WritableStream({
+ write() {
+ if (writes_done === WRITE_COUNT) {
+ // Will never resolve, leaving the write operation pending.
+ return new Promise(resolve => { });
+ }
+ ++writes_done;
+ return promise;
+ }
+ });
+
+ const writer = ws.getWriter();
+ await writer.ready;
+
+ const writeChunks = () => {
+ for (let i = 0; i < WRITE_COUNT; ++i) {
+ const ready = writer.ready;
+ writer.write("chunk");
+ }
+ };
+
+ // Apply backpressure.
+ writeChunks();
+
+ // Release backpressure.
+ resolve();
+ await writer.ready;
+
+ // Apply backpressure again.
+ writeChunks();
+ };
+
+ await createWritableStream();
+
+ for (let i = 0; i < 5; ++i) {
+ await garbageCollect();
+ }
+}, "WritableStream should not crash when garbage collected with backpressure");
diff --git a/test/fixtures/wpt/streams/writable-streams/garbage-collection.any.js b/test/fixtures/wpt/streams/writable-streams/garbage-collection.any.js
new file mode 100644
index 00000000000000..5e39e7c60d2c33
--- /dev/null
+++ b/test/fixtures/wpt/streams/writable-streams/garbage-collection.any.js
@@ -0,0 +1,21 @@
+// META: global=window,worker,shadowrealm
+// META: script=/common/gc.js
+'use strict';
+
+promise_test(async () => {
+
+ let written = false;
+ const promise = (() => {
+ const rs = new WritableStream({
+ write() {
+ written = true;
+ }
+ });
+ const writer = rs.getWriter();
+ return writer.write('something');
+ })();
+ await garbageCollect();
+ await promise;
+ assert_true(written);
+
+}, 'A WritableStream and its writer should not be garbage collected while there is a write promise pending');
diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json
index d627791ed27e0c..68dde063450d82 100644
--- a/test/fixtures/wpt/versions.json
+++ b/test/fixtures/wpt/versions.json
@@ -68,7 +68,7 @@
"path": "resources"
},
"streams": {
- "commit": "bc9dcbbf1a4c2c741ef47f47d6ede6458f40c4a4",
+ "commit": "f8f26a372f357370846226ff64db1f9cc98a1edb",
"path": "streams"
},
"url": {
diff --git a/test/wpt/status/streams.json b/test/wpt/status/streams.json
index 968b3359765189..ffa7e9d65f2338 100644
--- a/test/wpt/status/streams.json
+++ b/test/wpt/status/streams.json
@@ -8,6 +8,15 @@
"readable-streams/cross-realm-crash.window.js": {
"skip": "Browser-specific test"
},
+ "readable-streams/from.any.js": {
+ "fail": {
+ "note": "does not synchronously validate that the value returned by @@iterator/@@asyncIterator is an object",
+ "expected": [
+ "ReadableStream.from throws on invalid iterables; specifically an object with an @@iterator method returning a non-object",
+ "ReadableStream.from throws on invalid iterables; specifically an object with an @@asyncIterator method returning a non-object"
+ ]
+ }
+ },
"readable-streams/owning-type-message-port.any.js": {
"fail": {
"note": "Readable streams with type owning are not yet supported",
@@ -43,5 +52,13 @@
},
"transform-streams/invalid-realm.tentative.window.js": {
"skip": "Browser-specific test"
+ },
+ "writable-streams/aborting.any.js": {
+ "fail": {
+ "note": "Recursive abort() call from within an abort algorithm triggers ERR_INTERNAL_ASSERTION",
+ "expected": [
+ "recursive abort() call from abort() aborting signal"
+ ]
+ }
}
}