Files
2026-07-13 13:23:39 +08:00

251 lines
7.5 KiB
TypeScript

import { asyncifyIterable, chunkedAsyncIterable, peekableAsyncIterable } from '@crawlee/utils';
import { describe, expect, it } from 'vitest';
describe('asyncifyIterable', () => {
it('should convert a regular array to async iterable', async () => {
const asyncIterable = asyncifyIterable([1, 2, 3, 4, 5]);
const result = [];
for await (const item of asyncIterable) {
result.push(item);
}
expect(result).toEqual([1, 2, 3, 4, 5]);
});
it('should handle empty arrays', async () => {
const asyncIterable = asyncifyIterable([]);
const result: unknown[] = [];
for await (const item of asyncIterable) {
result.push(item);
}
expect(result).toEqual([]);
});
it('should work with Set', async () => {
const asyncIterable = asyncifyIterable(new Set([1, 2, 3, 2, 1]));
const result = [];
for await (const item of asyncIterable) {
result.push(item);
}
expect(result).toEqual([1, 2, 3]);
});
it('should work with generator function', async () => {
function* generator() {
yield 1;
yield 2;
yield 3;
}
const asyncIterable = asyncifyIterable(generator());
const result = [];
for await (const item of asyncIterable) {
result.push(item);
}
expect(result).toEqual([1, 2, 3]);
});
});
describe('chunkedAsyncIterable', () => {
it('should chunk an async iterable into specified sizes', async () => {
const result = [];
for await (const chunk of chunkedAsyncIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3)) {
result.push(chunk);
}
expect(result).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10]]);
});
it('should handle chunk size of 1', async () => {
const result = [];
for await (const chunk of chunkedAsyncIterable([1, 2, 3], 1)) {
result.push(chunk);
}
expect(result).toEqual([[1], [2], [3]]);
});
it('should handle chunk size larger than input', async () => {
const chunked = chunkedAsyncIterable([1, 2, 3], 10);
const result = [];
for await (const chunk of chunked) {
result.push(chunk);
}
expect(result).toEqual([[1, 2, 3]]);
});
it('should handle empty async iterable', async () => {
const result = [];
for await (const chunk of chunkedAsyncIterable([], 3)) {
result.push(chunk);
}
expect(result).toEqual([]);
});
it('should accept a callback for dynamic chunk size', async () => {
let size = 3;
const result = [];
for await (const chunk of chunkedAsyncIterable([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], () => size)) {
result.push(chunk);
size = 2; // shrink after first chunk
}
expect(result).toEqual([[1, 2, 3], [4, 5], [6, 7], [8, 9], [10]]);
});
it('should stop iterating when dynamic chunk size drops to zero', async () => {
let size = 2;
const result = [];
for await (const chunk of chunkedAsyncIterable([1, 2, 3, 4, 5, 6], () => size)) {
result.push(chunk);
size = 0; // signal stop after first chunk
}
expect(result).toEqual([[1, 2]]);
});
it('should leave the underlying iterator drainable after partial consumption', async () => {
async function* source() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}
const iterator = source();
// Consume only the first chunk via chunkedAsyncIterable
let size = 2;
for await (const _ of chunkedAsyncIterable(iterator, () => size)) {
size = 0; // stop after first chunk
}
// The underlying iterator should still be drainable
const remaining: number[] = [];
for await (const value of iterator) {
remaining.push(value);
}
expect(remaining).toEqual([3, 4, 5]);
});
it('should throw error for invalid chunk size', async () => {
await expect(
(async () => {
for await (const _ of chunkedAsyncIterable([1, 2, 3], 0)) {
// Empty block
}
})(),
).rejects.toThrow();
await expect(
(async () => {
for await (const _ of chunkedAsyncIterable([1, 2, 3], -1)) {
// Empty block
}
})(),
).rejects.toThrow();
});
});
describe('peekableAsyncIterable', () => {
it('should allow peeking at the next value without advancing', async () => {
const iterable = peekableAsyncIterable([1, 2, 3]);
const iterator = iterable[Symbol.asyncIterator]();
const peeked = await iterator.peek();
expect(peeked).toBe(1);
// Peeking again should return the same value
const peekedAgain = await iterator.peek();
expect(peekedAgain).toBe(1);
// Now iterate and verify we get the peeked value first
const results = [];
for await (const value of iterable) {
results.push(value);
}
expect(results).toEqual([1, 2, 3]);
});
it('should return undefined when peeking at empty iterable', async () => {
const iterable = peekableAsyncIterable([]);
const iterator = iterable[Symbol.asyncIterator]();
const peeked = await iterator.peek();
expect(peeked).toBeUndefined();
});
it('should handle peek after exhausting the iterable', async () => {
const iterable = peekableAsyncIterable([1]);
// Consume the iterable
const results = [];
for await (const value of iterable) {
results.push(value);
}
expect(results).toEqual([1]);
// Get a fresh iterator and peek should return undefined
const iterator = iterable[Symbol.asyncIterator]();
const peeked = await iterator.peek();
expect(peeked).toBeUndefined();
});
it('should work with manual iteration', async () => {
const iterable = peekableAsyncIterable([10, 20, 30]);
const iterator = iterable[Symbol.asyncIterator]();
// Peek first
expect(await iterator.peek()).toBe(10);
const first = await iterator.next();
expect(first.value).toBe(10);
expect(first.done).toBe(false);
expect(await iterator.peek()).toBe(20);
const second = await iterator.next();
expect(second.value).toBe(20);
expect(second.done).toBe(false);
expect(await iterator.peek()).toBe(30);
const third = await iterator.next();
expect(third.value).toBe(30);
expect(third.done).toBe(false);
expect(await iterator.peek()).toBe(undefined);
const done = await iterator.next();
expect(done.done).toBe(true);
expect(await iterator.peek()).toBe(undefined);
});
it('should handle peek on single element iterable', async () => {
const iterable = peekableAsyncIterable([42]);
const iterator = iterable[Symbol.asyncIterator]();
expect(await iterator.peek()).toBe(42);
const results = [];
for await (const value of iterable) {
results.push(value);
}
expect(results).toEqual([42]);
// Get a fresh iterator and peek after exhaustion
const newIterator = iterable[Symbol.asyncIterator]();
expect(await newIterator.peek()).toBeUndefined();
});
});