Skip to content
File

Blob: src/workerd/api/tests/streams-async-iterator-test.js

javascript192 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5// Tests for async iterator edge cases on ReadableStream.
6// These tests focus on iterator protocol methods (return, throw),
7// preventCancel option, and iteration over closed/errored streams.
8//
9// Test inspirations:
10// - Bun: test/js/web/streams/streams.test.js (Symbol.asyncIterator tests)
11// - Deno: tests/unit/streams_test.ts (async iterator tests)
12 
13import { strictEqual, ok, rejects, deepStrictEqual, throws } from 'node:assert';
14 
15// Test that breaking out of for-await-of cancels the stream
16// Inspired by: Bun test/js/web/streams/streams.test.js
17export const asyncIteratorBreakCancels = {
18 async test() {
19 let cancelled = false;
20 let cancelReason = null;
21 
22 const rs = new ReadableStream({
23 pull(controller) {
24 controller.enqueue('chunk');
25 },
26 cancel(reason) {
27 cancelled = true;
28 cancelReason = reason;
29 },
30 });
31 
32 const values = [];
33 for await (const chunk of rs) {
34 values.push(chunk);
35 if (values.length === 3) {
36 break;
37 }
38 }
39 
40 strictEqual(values.length, 3);
41 ok(cancelled, 'stream should be cancelled after break');
42 strictEqual(cancelReason, undefined);
43 },
44};
45 
46// Test calling return() explicitly on async iterator
47// Inspired by: Deno tests/unit/streams_test.ts
48export const asyncIteratorReturnMethod = {
49 async test() {
50 let cancelled = false;
51 
52 const rs = new ReadableStream({
53 pull(controller) {
54 controller.enqueue('chunk');
55 },
56 cancel() {
57 cancelled = true;
58 },
59 });
60 
61 const iterator = rs[Symbol.asyncIterator]();
62 
63 const first = await iterator.next();
64 strictEqual(first.value, 'chunk');
65 strictEqual(first.done, false);
66 
67 const returnResult = await iterator.return('finished');
68 strictEqual(returnResult.done, true);
69 
70 ok(cancelled, 'stream should be cancelled after return()');
71 },
72};
73 
74// Test that return() followed by next() returns done
75// Inspired by: Deno tests/unit/streams_test.ts
76export const asyncIteratorReturnThenNext = {
77 async test() {
78 const rs = new ReadableStream({
79 pull(controller) {
80 controller.enqueue('chunk');
81 },
82 });
83 
84 const iterator = rs[Symbol.asyncIterator]();
85 
86 await iterator.next();
87 await iterator.return();
88 
89 const result = await iterator.next();
90 strictEqual(result.done, true);
91 strictEqual(result.value, undefined);
92 },
93};
94 
95// Test values() with preventCancel: true
96// Inspired by: Bun test/js/web/streams/streams.test.js
97export const asyncIteratorPreventCancel = {
98 async test() {
99 let cancelled = false;
100 
101 const rs = new ReadableStream({
102 pull(controller) {
103 controller.enqueue('chunk');
104 },
105 cancel() {
106 cancelled = true;
107 },
108 });
109 
110 const values = [];
111 for await (const chunk of rs.values({ preventCancel: true })) {
112 values.push(chunk);
113 if (values.length === 3) {
114 break;
115 }
116 }
117 
118 strictEqual(values.length, 3);
119 ok(!cancelled, 'stream should NOT be cancelled with preventCancel: true');
120 ok(!rs.locked, 'stream should be unlocked');
121 
122 const reader = rs.getReader();
123 const { value } = await reader.read();
124 strictEqual(value, 'chunk');
125 reader.releaseLock();
126 },
127};
128 
129// Test iterating over an already-closed stream
130// Inspired by: Deno tests/unit/streams_test.ts
131export const asyncIteratorOnClosedStream = {
132 async test() {
133 const rs = new ReadableStream({
134 start(controller) {
135 controller.enqueue('only-chunk');
136 controller.close();
137 },
138 });
139 
140 const values = [];
141 for await (const chunk of rs) {
142 values.push(chunk);
143 }
144 
145 deepStrictEqual(values, ['only-chunk']);
146 },
147};
148 
149// Test iterating over an already-errored stream
150// Inspired by: Bun test/js/web/streams/streams.test.js
151export const asyncIteratorOnErroredStream = {
152 async test() {
153 const rs = new ReadableStream({
154 start(controller) {
155 controller.error(new Error('Stream error'));
156 },
157 });
158 
159 const values = [];
160 const iterate = async () => {
161 for await (const chunk of rs) {
162 values.push(chunk);
163 }
164 };
165 
166 await rejects(iterate, { message: 'Stream error' });
167 strictEqual(values.length, 0);
168 },
169};
170 
171// Test that getting an async iterator locks the stream
172// Inspired by: Bun test/js/web/streams/streams.test.js
173export const asyncIteratorLocksStream = {
174 async test() {
175 const rs = new ReadableStream({
176 pull(controller) {
177 controller.enqueue('chunk');
178 },
179 });
180 
181 ok(!rs.locked, 'stream should not be locked initially');
182 
183 const iterator = rs[Symbol.asyncIterator]();
184 
185 ok(rs.locked, 'stream should be locked after getting iterator');
186 
187 throws(() => rs[Symbol.asyncIterator](), TypeError);
188 
189 await iterator.return();
190 },
191};