643 lines
17 KiB
JavaScript
643 lines
17 KiB
JavaScript
// META: global=window,worker,shadowrealm
|
|
// META: script=../resources/test-utils.js
|
|
'use strict';
|
|
|
|
const iterableFactories = [
|
|
['an array of values', () => {
|
|
return ['a', 'b'];
|
|
}],
|
|
|
|
['an array of promises', () => {
|
|
return [
|
|
Promise.resolve('a'),
|
|
Promise.resolve('b')
|
|
];
|
|
}],
|
|
|
|
['an array iterator', () => {
|
|
return ['a', 'b'][Symbol.iterator]();
|
|
}],
|
|
|
|
['a string', () => {
|
|
// This iterates over the code points of the string.
|
|
return 'ab';
|
|
}],
|
|
|
|
['a Set', () => {
|
|
return new Set(['a', 'b']);
|
|
}],
|
|
|
|
['a Set iterator', () => {
|
|
return new Set(['a', 'b'])[Symbol.iterator]();
|
|
}],
|
|
|
|
['a sync generator', () => {
|
|
function* syncGenerator() {
|
|
yield 'a';
|
|
yield 'b';
|
|
}
|
|
|
|
return syncGenerator();
|
|
}],
|
|
|
|
['an async generator', () => {
|
|
async function* asyncGenerator() {
|
|
yield 'a';
|
|
yield 'b';
|
|
}
|
|
|
|
return asyncGenerator();
|
|
}],
|
|
|
|
['a sync iterable of values', () => {
|
|
const chunks = ['a', 'b'];
|
|
const iterator = {
|
|
next() {
|
|
return {
|
|
done: chunks.length === 0,
|
|
value: chunks.shift()
|
|
};
|
|
}
|
|
};
|
|
const iterable = {
|
|
[Symbol.iterator]: () => iterator
|
|
};
|
|
return iterable;
|
|
}],
|
|
|
|
['a sync iterable of promises', () => {
|
|
const chunks = ['a', 'b'];
|
|
const iterator = {
|
|
next() {
|
|
return chunks.length === 0 ? { done: true } : {
|
|
done: false,
|
|
value: Promise.resolve(chunks.shift())
|
|
};
|
|
}
|
|
};
|
|
const iterable = {
|
|
[Symbol.iterator]: () => iterator
|
|
};
|
|
return iterable;
|
|
}],
|
|
|
|
['an async iterable', () => {
|
|
const chunks = ['a', 'b'];
|
|
const asyncIterator = {
|
|
next() {
|
|
return Promise.resolve({
|
|
done: chunks.length === 0,
|
|
value: chunks.shift()
|
|
})
|
|
}
|
|
};
|
|
const asyncIterable = {
|
|
[Symbol.asyncIterator]: () => asyncIterator
|
|
};
|
|
return asyncIterable;
|
|
}],
|
|
|
|
['a ReadableStream', () => {
|
|
return new ReadableStream({
|
|
start(c) {
|
|
c.enqueue('a');
|
|
c.enqueue('b');
|
|
c.close();
|
|
}
|
|
});
|
|
}],
|
|
|
|
['a ReadableStream async iterator', () => {
|
|
return new ReadableStream({
|
|
start(c) {
|
|
c.enqueue('a');
|
|
c.enqueue('b');
|
|
c.close();
|
|
}
|
|
})[Symbol.asyncIterator]();
|
|
}]
|
|
];
|
|
|
|
for (const [label, factory] of iterableFactories) {
|
|
promise_test(async () => {
|
|
|
|
const iterable = factory();
|
|
const rs = ReadableStream.from(iterable);
|
|
assert_equals(rs.constructor, ReadableStream, 'from() should return a ReadableStream');
|
|
|
|
const reader = rs.getReader();
|
|
assert_object_equals(await reader.read(), { value: 'a', done: false }, 'first read should be correct');
|
|
assert_object_equals(await reader.read(), { value: 'b', done: false }, 'second read should be correct');
|
|
assert_object_equals(await reader.read(), { value: undefined, done: true }, 'third read should be done');
|
|
await reader.closed;
|
|
|
|
}, `ReadableStream.from accepts ${label}`);
|
|
}
|
|
|
|
const badIterables = [
|
|
['null', null],
|
|
['undefined', undefined],
|
|
['0', 0],
|
|
['NaN', NaN],
|
|
['true', true],
|
|
['{}', {}],
|
|
['Object.create(null)', Object.create(null)],
|
|
['a function', () => 42],
|
|
['a symbol', Symbol()],
|
|
['an object with a non-callable @@iterator method', {
|
|
[Symbol.iterator]: 42
|
|
}],
|
|
['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) {
|
|
test(() => {
|
|
assert_throws_js(TypeError, () => ReadableStream.from(iterable), 'from() should throw a TypeError')
|
|
}, `ReadableStream.from throws on invalid iterables; specifically ${label}`);
|
|
}
|
|
|
|
test(() => {
|
|
const theError = new Error('a unique string');
|
|
const iterable = {
|
|
[Symbol.iterator]() {
|
|
throw theError;
|
|
}
|
|
};
|
|
|
|
assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
|
|
}, `ReadableStream.from re-throws errors from calling the @@iterator method`);
|
|
|
|
test(() => {
|
|
const theError = new Error('a unique string');
|
|
const iterable = {
|
|
[Symbol.asyncIterator]() {
|
|
throw theError;
|
|
}
|
|
};
|
|
|
|
assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
|
|
}, `ReadableStream.from re-throws errors from calling the @@asyncIterator method`);
|
|
|
|
test(t => {
|
|
const theError = new Error('a unique string');
|
|
const iterable = {
|
|
[Symbol.iterator]: t.unreached_func('@@iterator should not be called'),
|
|
[Symbol.asyncIterator]() {
|
|
throw theError;
|
|
}
|
|
};
|
|
|
|
assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
|
|
}, `ReadableStream.from ignores @@iterator if @@asyncIterator exists`);
|
|
|
|
test(() => {
|
|
const theError = new Error('a unique string');
|
|
const iterable = {
|
|
[Symbol.asyncIterator]: null,
|
|
[Symbol.iterator]() {
|
|
throw theError
|
|
}
|
|
};
|
|
|
|
assert_throws_exactly(theError, () => ReadableStream.from(iterable), 'from() should re-throw the error');
|
|
}, `ReadableStream.from ignores a null @@asyncIterator`);
|
|
|
|
promise_test(async () => {
|
|
|
|
const iterable = {
|
|
async next() {
|
|
return { value: undefined, done: true };
|
|
},
|
|
[Symbol.asyncIterator]: () => iterable
|
|
};
|
|
|
|
const rs = ReadableStream.from(iterable);
|
|
const reader = rs.getReader();
|
|
|
|
const read = await reader.read();
|
|
assert_object_equals(read, { value: undefined, done: true }, 'first read should be done');
|
|
|
|
await reader.closed;
|
|
|
|
}, `ReadableStream.from accepts an empty iterable`);
|
|
|
|
promise_test(async t => {
|
|
|
|
const theError = new Error('a unique string');
|
|
|
|
const iterable = {
|
|
async 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() 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 = {
|
|
next() {
|
|
return new Promise(() => {});
|
|
},
|
|
[Symbol.asyncIterator]: () => iterable
|
|
};
|
|
|
|
const rs = ReadableStream.from(iterable);
|
|
const reader = rs.getReader();
|
|
|
|
await Promise.race([
|
|
reader.read().then(t.unreached_func('read() should not resolve'), t.unreached_func('read() should not reject')),
|
|
reader.closed.then(t.unreached_func('closed should not resolve'), t.unreached_func('closed should not reject')),
|
|
flushAsyncEvents()
|
|
]);
|
|
|
|
}, 'ReadableStream.from: stream stalls when next() never settles');
|
|
|
|
promise_test(async () => {
|
|
|
|
let nextCalls = 0;
|
|
let nextArgs;
|
|
const iterable = {
|
|
async next(...args) {
|
|
nextCalls += 1;
|
|
nextArgs = args;
|
|
return { value: 'a', done: false };
|
|
},
|
|
[Symbol.asyncIterator]: () => iterable
|
|
};
|
|
|
|
const rs = ReadableStream.from(iterable);
|
|
const reader = rs.getReader();
|
|
|
|
await flushAsyncEvents();
|
|
assert_equals(nextCalls, 0, 'next() should not be called yet');
|
|
|
|
const read = await reader.read();
|
|
assert_object_equals(read, { value: 'a', done: false }, 'first read should be correct');
|
|
assert_equals(nextCalls, 1, 'next() should be called after first read()');
|
|
assert_array_equals(nextArgs, [], 'next() should be called with no arguments');
|
|
|
|
}, `ReadableStream.from: calls next() after first read()`);
|
|
|
|
promise_test(async t => {
|
|
|
|
const theError = new Error('a unique string');
|
|
|
|
let returnCalls = 0;
|
|
let returnArgs;
|
|
let resolveReturn;
|
|
const iterable = {
|
|
next: t.unreached_func('next() should not be called'),
|
|
throw: t.unreached_func('throw() should not be called'),
|
|
async return(...args) {
|
|
returnCalls += 1;
|
|
returnArgs = args;
|
|
await new Promise(r => resolveReturn = r);
|
|
return { done: true };
|
|
},
|
|
[Symbol.asyncIterator]: () => iterable
|
|
};
|
|
|
|
const rs = ReadableStream.from(iterable);
|
|
const reader = rs.getReader();
|
|
assert_equals(returnCalls, 0, 'return() should not be called yet');
|
|
|
|
let cancelResolved = false;
|
|
const cancelPromise = reader.cancel(theError).then(() => {
|
|
cancelResolved = true;
|
|
});
|
|
|
|
await flushAsyncEvents();
|
|
assert_equals(returnCalls, 1, 'return() should be called');
|
|
assert_array_equals(returnArgs, [theError], 'return() should be called with cancel reason');
|
|
assert_false(cancelResolved, 'cancel() should not resolve while promise from return() is pending');
|
|
|
|
resolveReturn();
|
|
await Promise.all([
|
|
cancelPromise,
|
|
reader.closed
|
|
]);
|
|
|
|
}, `ReadableStream.from: cancelling the returned stream calls and awaits return()`);
|
|
|
|
promise_test(async t => {
|
|
|
|
let nextCalls = 0;
|
|
let returnCalls = 0;
|
|
|
|
const iterable = {
|
|
async next() {
|
|
nextCalls += 1;
|
|
return { value: undefined, done: true };
|
|
},
|
|
throw: t.unreached_func('throw() should not be called'),
|
|
async return() {
|
|
returnCalls += 1;
|
|
},
|
|
[Symbol.asyncIterator]: () => iterable
|
|
};
|
|
|
|
const rs = ReadableStream.from(iterable);
|
|
const reader = rs.getReader();
|
|
|
|
const read = await reader.read();
|
|
assert_object_equals(read, { value: undefined, done: true }, 'first read should be done');
|
|
assert_equals(nextCalls, 1, 'next() should be called once');
|
|
|
|
await reader.closed;
|
|
assert_equals(returnCalls, 0, 'return() should not be called');
|
|
|
|
}, `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');
|
|
|
|
const iterable = {
|
|
next: t.unreached_func('next() should not be called'),
|
|
throw: t.unreached_func('throw() should not be called'),
|
|
async return() {
|
|
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() fulfills with a non-object`);
|
|
|
|
promise_test(async () => {
|
|
|
|
let nextCalls = 0;
|
|
let reader;
|
|
let values = ['a', 'b', 'c'];
|
|
|
|
const iterable = {
|
|
async next() {
|
|
nextCalls += 1;
|
|
if (nextCalls === 1) {
|
|
reader.read();
|
|
}
|
|
return { value: values.shift(), done: false };
|
|
},
|
|
[Symbol.asyncIterator]: () => iterable
|
|
};
|
|
|
|
const rs = ReadableStream.from(iterable);
|
|
reader = rs.getReader();
|
|
|
|
const read1 = await reader.read();
|
|
assert_object_equals(read1, { value: 'a', done: false }, 'first read should be correct');
|
|
await flushAsyncEvents();
|
|
assert_equals(nextCalls, 2, 'next() should be called two times');
|
|
|
|
const read2 = await reader.read();
|
|
assert_object_equals(read2, { value: 'c', done: false }, 'second read should be correct');
|
|
assert_equals(nextCalls, 3, 'next() should be called three times');
|
|
|
|
}, `ReadableStream.from: reader.read() inside next()`);
|
|
|
|
promise_test(async () => {
|
|
|
|
let nextCalls = 0;
|
|
let returnCalls = 0;
|
|
let reader;
|
|
|
|
const iterable = {
|
|
async next() {
|
|
nextCalls++;
|
|
await reader.cancel();
|
|
assert_equals(returnCalls, 1, 'return() should be called once');
|
|
return { value: 'something else', done: false };
|
|
},
|
|
async return() {
|
|
returnCalls++;
|
|
},
|
|
[Symbol.asyncIterator]: () => iterable
|
|
};
|
|
|
|
const rs = ReadableStream.from(iterable);
|
|
reader = rs.getReader();
|
|
|
|
const read = await reader.read();
|
|
assert_object_equals(read, { value: undefined, done: true }, 'first read should be done');
|
|
assert_equals(nextCalls, 1, 'next() should be called once');
|
|
|
|
await reader.closed;
|
|
|
|
}, `ReadableStream.from: reader.cancel() inside next()`);
|
|
|
|
promise_test(async t => {
|
|
|
|
let returnCalls = 0;
|
|
let reader;
|
|
|
|
const iterable = {
|
|
next: t.unreached_func('next() should not be called'),
|
|
async return() {
|
|
returnCalls++;
|
|
await reader.cancel();
|
|
return { done: true };
|
|
},
|
|
[Symbol.asyncIterator]: () => iterable
|
|
};
|
|
|
|
const rs = ReadableStream.from(iterable);
|
|
reader = rs.getReader();
|
|
|
|
await reader.cancel();
|
|
assert_equals(returnCalls, 1, 'return() should be called once');
|
|
|
|
await reader.closed;
|
|
|
|
}, `ReadableStream.from: reader.cancel() inside return()`);
|
|
|
|
promise_test(async t => {
|
|
|
|
let array = ['a', 'b'];
|
|
|
|
const rs = ReadableStream.from(array);
|
|
const reader = rs.getReader();
|
|
|
|
const read1 = await reader.read();
|
|
assert_object_equals(read1, { value: 'a', done: false }, 'first read should be correct');
|
|
const read2 = await reader.read();
|
|
assert_object_equals(read2, { value: 'b', done: false }, 'second read should be correct');
|
|
|
|
array.push('c');
|
|
|
|
const read3 = await reader.read();
|
|
assert_object_equals(read3, { value: 'c', done: false }, 'third read after push() should be correct');
|
|
const read4 = await reader.read();
|
|
assert_object_equals(read4, { value: undefined, done: true }, 'fourth read should be done');
|
|
|
|
await reader.closed;
|
|
|
|
}, `ReadableStream.from(array), push() to array while reading`);
|