Skip to content

Commit dfc7ab7

Browse files
authored
feat: implement iterateFormatted async iter value formatting helper (#11)
1 parent 062d2c4 commit dfc7ab7

12 files changed

+542
-83
lines changed

spec/tests/Iterate.spec.tsx

+11-11
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ describe('`Iterate` component', () => {
488488

489489
it(
490490
gray(
491-
"When consequtively updated with new iterables will close the previous one's iterator every time and render accordingly"
491+
"When consecutively updated with new iterables will close the previous one's iterator every time and render accordingly"
492492
),
493493
async () => {
494494
let lastRenderFnInput: undefined | IterationResult<string>;
@@ -498,7 +498,7 @@ describe('`Iterate` component', () => {
498498
new IterableChannelTestHelper<string>(),
499499
];
500500

501-
const [channel1IterCloseSpy, channel2IterCloseSpy] = [
501+
const [channelReturnSpy1, channelReturnSpy2] = [
502502
vi.spyOn(channel1, 'return'),
503503
vi.spyOn(channel2, 'return'),
504504
];
@@ -519,8 +519,8 @@ describe('`Iterate` component', () => {
519519
{
520520
rendered.rerender(buildTestContent(channel1));
521521

522-
expect(channel1IterCloseSpy).not.toHaveBeenCalled();
523-
expect(channel2IterCloseSpy).not.toHaveBeenCalled();
522+
expect(channelReturnSpy1).not.toHaveBeenCalled();
523+
expect(channelReturnSpy2).not.toHaveBeenCalled();
524524
expect(lastRenderFnInput).toStrictEqual({
525525
value: undefined,
526526
pendingFirst: true,
@@ -543,8 +543,8 @@ describe('`Iterate` component', () => {
543543
{
544544
rendered.rerender(buildTestContent(channel2));
545545

546-
expect(channel1IterCloseSpy).toHaveBeenCalledOnce();
547-
expect(channel2IterCloseSpy).not.toHaveBeenCalled();
546+
expect(channelReturnSpy1).toHaveBeenCalledOnce();
547+
expect(channelReturnSpy2).not.toHaveBeenCalled();
548548
expect(lastRenderFnInput).toStrictEqual({
549549
value: 'a',
550550
pendingFirst: true,
@@ -567,8 +567,8 @@ describe('`Iterate` component', () => {
567567
{
568568
rendered.rerender(buildTestContent((async function* () {})()));
569569

570-
expect(channel1IterCloseSpy).toHaveBeenCalledOnce();
571-
expect(channel2IterCloseSpy).toHaveBeenCalledOnce();
570+
expect(channelReturnSpy1).toHaveBeenCalledOnce();
571+
expect(channelReturnSpy2).toHaveBeenCalledOnce();
572572
expect(lastRenderFnInput).toStrictEqual({
573573
value: 'b',
574574
pendingFirst: true,
@@ -584,7 +584,7 @@ describe('`Iterate` component', () => {
584584
let lastRenderFnInput: undefined | IterationResult<string>;
585585

586586
const channel = new IterableChannelTestHelper<string>();
587-
const channelIterCloseSpy = vi.spyOn(channel, 'return');
587+
const channelReturnSpy = vi.spyOn(channel, 'return');
588588

589589
const buildTestContent = (value: AsyncIterable<string>) => {
590590
return (
@@ -602,7 +602,7 @@ describe('`Iterate` component', () => {
602602
{
603603
rendered.rerender(buildTestContent(channel));
604604

605-
expect(channelIterCloseSpy).not.toHaveBeenCalled();
605+
expect(channelReturnSpy).not.toHaveBeenCalled();
606606
expect(lastRenderFnInput).toStrictEqual({
607607
value: undefined,
608608
pendingFirst: true,
@@ -624,7 +624,7 @@ describe('`Iterate` component', () => {
624624

625625
{
626626
rendered.unmount();
627-
expect(channelIterCloseSpy).toHaveBeenCalledOnce();
627+
expect(channelReturnSpy).toHaveBeenCalledOnce();
628628
}
629629
});
630630
});

spec/tests/iterateFormatted.spec.tsx

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { it, describe, expect, afterEach, vi } from 'vitest';
2+
import { gray } from 'colorette';
3+
import { render, cleanup as cleanupMountedReactTrees, act } from '@testing-library/react';
4+
import { iterateFormatted, Iterate } from '../../src/index.js';
5+
import { pipe } from '../utils/pipe.js';
6+
import { asyncIterToArray } from '../utils/asyncIterToArray.js';
7+
import { IterableChannelTestHelper } from '../utils/IterableChannelTestHelper.js';
8+
9+
afterEach(() => {
10+
cleanupMountedReactTrees();
11+
});
12+
13+
describe('`iterateFormatted` function', () => {
14+
it(gray('When called on some plain value it formats and returns that on the spot'), () => {
15+
const multiFormattedPlainValue = pipe(
16+
'a',
17+
$ => iterateFormatted($, (value, i) => `${value} formatted once (idx: ${i})`),
18+
$ => iterateFormatted($, (value, i) => `${value} and formatted twice (idx: ${i})`)
19+
);
20+
expect(multiFormattedPlainValue).toStrictEqual(
21+
'a formatted once (idx: 0) and formatted twice (idx: 0)'
22+
);
23+
});
24+
25+
it(
26+
gray(
27+
'When the resulting object is iterated manually (without the library tools) it still has the provided formatting applied'
28+
),
29+
async () => {
30+
const multiFormattedIter = pipe(
31+
(async function* () {
32+
yield* ['a', 'b', 'c'];
33+
})(),
34+
$ => iterateFormatted($, (value, i) => `${value} formatted once (idx: ${i})`),
35+
$ => iterateFormatted($, (value, i) => `${value} and formatted twice (idx: ${i})`)
36+
);
37+
38+
const yielded = await asyncIterToArray(multiFormattedIter);
39+
40+
expect(yielded).toStrictEqual([
41+
'a formatted once (idx: 0) and formatted twice (idx: 0)',
42+
'b formatted once (idx: 1) and formatted twice (idx: 1)',
43+
'c formatted once (idx: 2) and formatted twice (idx: 2)',
44+
]);
45+
}
46+
);
47+
48+
it(
49+
gray(
50+
'When the wrapped source is used normally with library tools it is rendered and formatted correctly'
51+
),
52+
async () => {
53+
const channel = new IterableChannelTestHelper<string>();
54+
55+
const rendered = render(
56+
<Iterate
57+
value={pipe(
58+
channel,
59+
$ => iterateFormatted($, (value, i) => `${value} formatted once (idx: ${i})`),
60+
$ => iterateFormatted($, (value, i) => `${value} and formatted twice (idx: ${i})`)
61+
)}
62+
>
63+
{next => <p>Rendered: {next.value}</p>}
64+
</Iterate>
65+
);
66+
67+
expect(rendered.container.innerHTML).toStrictEqual('<p>Rendered: </p>');
68+
69+
for (const [i, value] of ['a', 'b', 'c'].entries()) {
70+
await act(() => channel.put(value));
71+
expect(rendered.container.innerHTML).toStrictEqual(
72+
`<p>Rendered: ${value} formatted once (idx: ${i}) and formatted twice (idx: ${i})</p>`
73+
);
74+
}
75+
}
76+
);
77+
78+
it(
79+
gray(
80+
'When re-rendering with a new wrapped iterable each time, as long as they wrap the same source iterable, the same source iteration process will persist across these re-renderings'
81+
),
82+
async () => {
83+
const [channel1, channel2] = [
84+
new IterableChannelTestHelper<string>(),
85+
new IterableChannelTestHelper<string>(),
86+
];
87+
88+
const [channelReturnSpy1, channelReturnSpy2] = [
89+
vi.spyOn(channel1, 'return'),
90+
vi.spyOn(channel2, 'return'),
91+
];
92+
93+
const rebuildTestContent = (it: AsyncIterable<string>) => (
94+
<Iterate
95+
value={pipe(
96+
it,
97+
$ => iterateFormatted($, (value, i) => `${value} formatted once (idx: ${i})`),
98+
$ => iterateFormatted($, (value, i) => `${value} and formatted twice (idx: ${i})`)
99+
)}
100+
>
101+
{next => <p>Rendered: {next.value}</p>}
102+
</Iterate>
103+
);
104+
105+
const rendered = render(<></>);
106+
107+
rendered.rerender(rebuildTestContent(channel1));
108+
expect(channelReturnSpy1).not.toHaveBeenCalled();
109+
110+
rendered.rerender(rebuildTestContent(channel1));
111+
expect(channelReturnSpy1).not.toHaveBeenCalled();
112+
113+
rendered.rerender(rebuildTestContent(channel2));
114+
expect(channelReturnSpy1).toHaveBeenCalledOnce();
115+
expect(channelReturnSpy2).not.toHaveBeenCalled();
116+
117+
rendered.rerender(rebuildTestContent(channel2));
118+
expect(channelReturnSpy2).not.toHaveBeenCalled();
119+
}
120+
);
121+
122+
it(
123+
gray(
124+
'Always the latest closure passed in as the format function will be the one to format the next-arriving source value'
125+
),
126+
async () => {
127+
const channel = new IterableChannelTestHelper<string>();
128+
129+
const Wrapper = (props: { outerValue: string }) => (
130+
<Iterate
131+
value={pipe(
132+
channel,
133+
$ =>
134+
iterateFormatted(
135+
$,
136+
(value, i) => `${value} formatted once (idx: ${i}, outer val: ${props.outerValue})`
137+
),
138+
$ =>
139+
iterateFormatted(
140+
$,
141+
(value, i) =>
142+
`${value} and formatted twice (idx: ${i}, outer val: ${props.outerValue})`
143+
)
144+
)}
145+
>
146+
{next => <p>Rendered: {next.value}</p>}
147+
</Iterate>
148+
);
149+
150+
const rendered = render(<></>);
151+
152+
for (const [i, [nextYield, nextProp]] of [
153+
['yield_a', 'prop_a'],
154+
['yield_b', 'prop_b'],
155+
['yield_c', 'prop_c'],
156+
].entries()) {
157+
rendered.rerender(<Wrapper outerValue={nextProp} />);
158+
await act(() => channel.put(nextYield));
159+
160+
expect(rendered.container.innerHTML).toStrictEqual(
161+
`<p>Rendered: ${nextYield} formatted once (idx: ${i}, outer val: ${nextProp}) and formatted twice (idx: ${i}, outer val: ${nextProp})</p>`
162+
);
163+
}
164+
}
165+
);
166+
});

spec/tests/useAsyncIter.spec.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -330,15 +330,15 @@ describe('`useAsyncIter` hook', () => {
330330

331331
it(
332332
gray(
333-
"When consequtively updated with new iterables will close the previous one's iterator every time and render accordingly"
333+
"When consecutively updated with new iterables will close the previous one's iterator every time and render accordingly"
334334
),
335335
async () => {
336336
const [channel1, channel2] = [
337337
new IterableChannelTestHelper<string>(),
338338
new IterableChannelTestHelper<string>(),
339339
];
340340

341-
const [channel1IterCloseSpy, channel2IterCloseSpy] = [
341+
const [channelReturnSpy1, channelReturnSpy2] = [
342342
vi.spyOn(channel1, 'return'),
343343
vi.spyOn(channel2, 'return'),
344344
];
@@ -352,8 +352,8 @@ describe('`useAsyncIter` hook', () => {
352352
{
353353
renderedHook.rerender({ value: channel1 });
354354

355-
expect(channel1IterCloseSpy).not.toHaveBeenCalled();
356-
expect(channel2IterCloseSpy).not.toHaveBeenCalled();
355+
expect(channelReturnSpy1).not.toHaveBeenCalled();
356+
expect(channelReturnSpy2).not.toHaveBeenCalled();
357357
expect(renderedHook.result.current).toStrictEqual({
358358
value: undefined,
359359
pendingFirst: true,
@@ -374,8 +374,8 @@ describe('`useAsyncIter` hook', () => {
374374
{
375375
renderedHook.rerender({ value: channel2 });
376376

377-
expect(channel1IterCloseSpy).toHaveBeenCalledOnce();
378-
expect(channel2IterCloseSpy).not.toHaveBeenCalled();
377+
expect(channelReturnSpy1).toHaveBeenCalledOnce();
378+
expect(channelReturnSpy2).not.toHaveBeenCalled();
379379
expect(renderedHook.result.current).toStrictEqual({
380380
value: 'a',
381381
pendingFirst: true,
@@ -396,8 +396,8 @@ describe('`useAsyncIter` hook', () => {
396396
{
397397
renderedHook.rerender({ value: (async function* () {})() });
398398

399-
expect(channel1IterCloseSpy).toHaveBeenCalledOnce();
400-
expect(channel2IterCloseSpy).toHaveBeenCalledOnce();
399+
expect(channelReturnSpy1).toHaveBeenCalledOnce();
400+
expect(channelReturnSpy2).toHaveBeenCalledOnce();
401401
expect(renderedHook.result.current).toStrictEqual({
402402
value: 'b',
403403
pendingFirst: true,
@@ -410,7 +410,7 @@ describe('`useAsyncIter` hook', () => {
410410

411411
it(gray('When unmounted will close the last active iterator it held'), async () => {
412412
const channel = new IterableChannelTestHelper<string>();
413-
const channelIterCloseSpy = vi.spyOn(channel, 'return');
413+
const channelReturnSpy = vi.spyOn(channel, 'return');
414414

415415
const renderedHook = renderHook(({ value }) => useAsyncIter(value), {
416416
initialProps: {
@@ -421,7 +421,7 @@ describe('`useAsyncIter` hook', () => {
421421
{
422422
renderedHook.rerender({ value: channel });
423423

424-
expect(channelIterCloseSpy).not.toHaveBeenCalled();
424+
expect(channelReturnSpy).not.toHaveBeenCalled();
425425
expect(renderedHook.result.current).toStrictEqual({
426426
value: undefined,
427427
pendingFirst: true,
@@ -441,7 +441,7 @@ describe('`useAsyncIter` hook', () => {
441441

442442
{
443443
renderedHook.unmount();
444-
expect(channelIterCloseSpy).toHaveBeenCalledOnce();
444+
expect(channelReturnSpy).toHaveBeenCalledOnce();
445445
}
446446
});
447447
});
+14-14
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
export { IterableChannelTestHelper };
22

33
class IterableChannelTestHelper<T> implements AsyncIterableIterator<T>, AsyncDisposable {
4-
isChannelClosed = false;
5-
nextIteration = Promise.withResolvers<IteratorResult<T>>();
4+
#isChannelClosed = false;
5+
#nextIteration = Promise.withResolvers<IteratorResult<T>>();
66

77
[Symbol.asyncIterator]() {
88
return this;
@@ -13,36 +13,36 @@ class IterableChannelTestHelper<T> implements AsyncIterableIterator<T>, AsyncDis
1313
}
1414

1515
get isClosed(): boolean {
16-
return this.isChannelClosed;
16+
return this.#isChannelClosed;
1717
}
1818

1919
put(value: T): void {
20-
if (this.isChannelClosed) {
20+
if (this.#isChannelClosed) {
2121
return;
2222
}
23-
this.nextIteration.resolve({ done: false, value });
24-
this.nextIteration = Promise.withResolvers();
23+
this.#nextIteration.resolve({ done: false, value });
24+
this.#nextIteration = Promise.withResolvers();
2525
}
2626

2727
complete(): void {
28-
this.isChannelClosed = true;
29-
this.nextIteration.resolve({ done: true, value: undefined });
28+
this.#isChannelClosed = true;
29+
this.#nextIteration.resolve({ done: true, value: undefined });
3030
}
3131

3232
error(errValue?: unknown): void {
33-
this.isChannelClosed = true;
34-
this.nextIteration.reject(errValue);
35-
this.nextIteration = Promise.withResolvers();
36-
this.nextIteration.resolve({ done: true, value: undefined });
33+
this.#isChannelClosed = true;
34+
this.#nextIteration.reject(errValue);
35+
this.#nextIteration = Promise.withResolvers();
36+
this.#nextIteration.resolve({ done: true, value: undefined });
3737
}
3838

3939
async next(): Promise<IteratorResult<T, void>> {
40-
return this.nextIteration.promise;
40+
return this.#nextIteration.promise;
4141
}
4242

4343
async return(): Promise<IteratorReturnResult<void>> {
4444
this.complete();
45-
const res = await this.nextIteration.promise;
45+
const res = await this.#nextIteration.promise;
4646
return res as typeof res & { done: true };
4747
}
4848
}

spec/utils/asyncIterToArray.ts

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export { asyncIterToArray };
2+
3+
async function asyncIterToArray<T>(source: AsyncIterable<T>): Promise<T[]> {
4+
const values: T[] = [];
5+
for await (const value of source) {
6+
values.push(value);
7+
}
8+
return values;
9+
}

0 commit comments

Comments
 (0)