From 8e3849e9078f14e8301476373156d3c8756f5684 Mon Sep 17 00:00:00 2001 From: "Node.js GitHub Bot" Date: Tue, 21 Apr 2026 12:06:28 +0000 Subject: [PATCH 1/2] test: update WPT for streams to f8f26a372f --- test/fixtures/wpt/README.md | 2 +- test/fixtures/wpt/streams/META.yml | 3 +- test/fixtures/wpt/streams/WEB_FEATURES.yml | 5 + .../wpt/streams/piping/WEB_FEATURES.yml | 3 + .../readable-byte-streams/WEB_FEATURES.yml | 3 + .../bad-buffers-and-views.any.js | 17 +- .../crashtests/tee-locked-stream.any.js | 9 ++ .../readable-byte-streams/general.any.js | 86 ++++++++++ .../readable-byte-streams/templated.any.js | 24 +++ .../streams/readable-streams/WEB_FEATURES.yml | 14 ++ .../crashtests/WEB_FEATURES.yml | 7 + .../exception-during-size-while-pulling.html | 36 +++++ .../crashtests/garbage-collection.any.js | 38 +++++ .../wpt/streams/readable-streams/from.any.js | 151 ++++++++++++++++++ .../garbage-collection.any.js | 19 +++ .../streams/readable-streams/templated.any.js | 8 +- .../streams/resources/rs-test-templates.js | 103 +++++++++--- .../wpt/streams/transferable/WEB_FEATURES.yml | 3 + .../transform-streams/WEB_FEATURES.yml | 8 + .../streams/transform-streams/cancel.any.js | 90 +++++++++++ .../streams/writable-streams/WEB_FEATURES.yml | 3 + .../streams/writable-streams/aborting.any.js | 88 +++++++++- .../wpt/streams/writable-streams/close.any.js | 11 ++ .../writable-streams/constructor.any.js | 4 + .../crashtests/garbage-collection.any.js | 90 +++++++++++ .../garbage-collection.any.js | 21 +++ test/fixtures/wpt/versions.json | 2 +- 27 files changed, 803 insertions(+), 45 deletions(-) create mode 100644 test/fixtures/wpt/streams/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/streams/piping/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/streams/readable-byte-streams/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/streams/readable-byte-streams/crashtests/tee-locked-stream.any.js create mode 100644 test/fixtures/wpt/streams/readable-byte-streams/templated.any.js create mode 100644 test/fixtures/wpt/streams/readable-streams/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/streams/readable-streams/crashtests/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/streams/readable-streams/crashtests/exception-during-size-while-pulling.html create mode 100644 test/fixtures/wpt/streams/readable-streams/crashtests/garbage-collection.any.js create mode 100644 test/fixtures/wpt/streams/transferable/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/streams/transform-streams/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/streams/writable-streams/WEB_FEATURES.yml create mode 100644 test/fixtures/wpt/streams/writable-streams/crashtests/garbage-collection.any.js create mode 100644 test/fixtures/wpt/streams/writable-streams/garbage-collection.any.js 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": { From 8335844da0f774f9ecc0f2408ba0fc2bf5324d09 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 21 Apr 2026 14:07:26 +0200 Subject: [PATCH 2/2] fixup! test: update WPT for streams to f8f26a372f --- test/wpt/status/streams.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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" + ] + } } }