Skip to content

Implement iterateFormatted async iter value formatting helper #11

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions spec/tests/Iterate.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,7 @@ describe('`Iterate` component', () => {

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

const [channel1IterCloseSpy, channel2IterCloseSpy] = [
const [channelReturnSpy1, channelReturnSpy2] = [
vi.spyOn(channel1, 'return'),
vi.spyOn(channel2, 'return'),
];
Expand All @@ -519,8 +519,8 @@ describe('`Iterate` component', () => {
{
rendered.rerender(buildTestContent(channel1));

expect(channel1IterCloseSpy).not.toHaveBeenCalled();
expect(channel2IterCloseSpy).not.toHaveBeenCalled();
expect(channelReturnSpy1).not.toHaveBeenCalled();
expect(channelReturnSpy2).not.toHaveBeenCalled();
expect(lastRenderFnInput).toStrictEqual({
value: undefined,
pendingFirst: true,
Expand All @@ -543,8 +543,8 @@ describe('`Iterate` component', () => {
{
rendered.rerender(buildTestContent(channel2));

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

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

const channel = new IterableChannelTestHelper<string>();
const channelIterCloseSpy = vi.spyOn(channel, 'return');
const channelReturnSpy = vi.spyOn(channel, 'return');

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

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

{
rendered.unmount();
expect(channelIterCloseSpy).toHaveBeenCalledOnce();
expect(channelReturnSpy).toHaveBeenCalledOnce();
}
});
});
Expand Down
166 changes: 166 additions & 0 deletions spec/tests/iterateFormatted.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { it, describe, expect, afterEach, vi } from 'vitest';
import { gray } from 'colorette';
import { render, cleanup as cleanupMountedReactTrees, act } from '@testing-library/react';
import { iterateFormatted, Iterate } from '../../src/index.js';
import { pipe } from '../utils/pipe.js';
import { asyncIterToArray } from '../utils/asyncIterToArray.js';
import { IterableChannelTestHelper } from '../utils/IterableChannelTestHelper.js';

afterEach(() => {
cleanupMountedReactTrees();
});

describe('`iterateFormatted` function', () => {
it(gray('When called on some plain value it formats and returns that on the spot'), () => {
const multiFormattedPlainValue = pipe(
'a',
$ => iterateFormatted($, (value, i) => `${value} formatted once (idx: ${i})`),
$ => iterateFormatted($, (value, i) => `${value} and formatted twice (idx: ${i})`)
);
expect(multiFormattedPlainValue).toStrictEqual(
'a formatted once (idx: 0) and formatted twice (idx: 0)'
);
});

it(
gray(
'When the resulting object is iterated manually (without the library tools) it still has the provided formatting applied'
),
async () => {
const multiFormattedIter = pipe(
(async function* () {
yield* ['a', 'b', 'c'];
})(),
$ => iterateFormatted($, (value, i) => `${value} formatted once (idx: ${i})`),
$ => iterateFormatted($, (value, i) => `${value} and formatted twice (idx: ${i})`)
);

const yielded = await asyncIterToArray(multiFormattedIter);

expect(yielded).toStrictEqual([
'a formatted once (idx: 0) and formatted twice (idx: 0)',
'b formatted once (idx: 1) and formatted twice (idx: 1)',
'c formatted once (idx: 2) and formatted twice (idx: 2)',
]);
}
);

it(
gray(
'When the wrapped source is used normally with library tools it is rendered and formatted correctly'
),
async () => {
const channel = new IterableChannelTestHelper<string>();

const rendered = render(
<Iterate
value={pipe(
channel,
$ => iterateFormatted($, (value, i) => `${value} formatted once (idx: ${i})`),
$ => iterateFormatted($, (value, i) => `${value} and formatted twice (idx: ${i})`)
)}
>
{next => <p>Rendered: {next.value}</p>}
</Iterate>
);

expect(rendered.container.innerHTML).toStrictEqual('<p>Rendered: </p>');

for (const [i, value] of ['a', 'b', 'c'].entries()) {
await act(() => channel.put(value));
expect(rendered.container.innerHTML).toStrictEqual(
`<p>Rendered: ${value} formatted once (idx: ${i}) and formatted twice (idx: ${i})</p>`
);
}
}
);

it(
gray(
'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'
),
async () => {
const [channel1, channel2] = [
new IterableChannelTestHelper<string>(),
new IterableChannelTestHelper<string>(),
];

const [channelReturnSpy1, channelReturnSpy2] = [
vi.spyOn(channel1, 'return'),
vi.spyOn(channel2, 'return'),
];

const rebuildTestContent = (it: AsyncIterable<string>) => (
<Iterate
value={pipe(
it,
$ => iterateFormatted($, (value, i) => `${value} formatted once (idx: ${i})`),
$ => iterateFormatted($, (value, i) => `${value} and formatted twice (idx: ${i})`)
)}
>
{next => <p>Rendered: {next.value}</p>}
</Iterate>
);

const rendered = render(<></>);

rendered.rerender(rebuildTestContent(channel1));
expect(channelReturnSpy1).not.toHaveBeenCalled();

rendered.rerender(rebuildTestContent(channel1));
expect(channelReturnSpy1).not.toHaveBeenCalled();

rendered.rerender(rebuildTestContent(channel2));
expect(channelReturnSpy1).toHaveBeenCalledOnce();
expect(channelReturnSpy2).not.toHaveBeenCalled();

rendered.rerender(rebuildTestContent(channel2));
expect(channelReturnSpy2).not.toHaveBeenCalled();
}
);

it(
gray(
'Always the latest closure passed in as the format function will be the one to format the next-arriving source value'
),
async () => {
const channel = new IterableChannelTestHelper<string>();

const Wrapper = (props: { outerValue: string }) => (
<Iterate
value={pipe(
channel,
$ =>
iterateFormatted(
$,
(value, i) => `${value} formatted once (idx: ${i}, outer val: ${props.outerValue})`
),
$ =>
iterateFormatted(
$,
(value, i) =>
`${value} and formatted twice (idx: ${i}, outer val: ${props.outerValue})`
)
)}
>
{next => <p>Rendered: {next.value}</p>}
</Iterate>
);

const rendered = render(<></>);

for (const [i, [nextYield, nextProp]] of [
['yield_a', 'prop_a'],
['yield_b', 'prop_b'],
['yield_c', 'prop_c'],
].entries()) {
rendered.rerender(<Wrapper outerValue={nextProp} />);
await act(() => channel.put(nextYield));

expect(rendered.container.innerHTML).toStrictEqual(
`<p>Rendered: ${nextYield} formatted once (idx: ${i}, outer val: ${nextProp}) and formatted twice (idx: ${i}, outer val: ${nextProp})</p>`
);
}
}
);
});
22 changes: 11 additions & 11 deletions spec/tests/useAsyncIter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,15 +330,15 @@ describe('`useAsyncIter` hook', () => {

it(
gray(
"When consequtively updated with new iterables will close the previous one's iterator every time and render accordingly"
"When consecutively updated with new iterables will close the previous one's iterator every time and render accordingly"
),
async () => {
const [channel1, channel2] = [
new IterableChannelTestHelper<string>(),
new IterableChannelTestHelper<string>(),
];

const [channel1IterCloseSpy, channel2IterCloseSpy] = [
const [channelReturnSpy1, channelReturnSpy2] = [
vi.spyOn(channel1, 'return'),
vi.spyOn(channel2, 'return'),
];
Expand All @@ -352,8 +352,8 @@ describe('`useAsyncIter` hook', () => {
{
renderedHook.rerender({ value: channel1 });

expect(channel1IterCloseSpy).not.toHaveBeenCalled();
expect(channel2IterCloseSpy).not.toHaveBeenCalled();
expect(channelReturnSpy1).not.toHaveBeenCalled();
expect(channelReturnSpy2).not.toHaveBeenCalled();
expect(renderedHook.result.current).toStrictEqual({
value: undefined,
pendingFirst: true,
Expand All @@ -374,8 +374,8 @@ describe('`useAsyncIter` hook', () => {
{
renderedHook.rerender({ value: channel2 });

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

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

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

const renderedHook = renderHook(({ value }) => useAsyncIter(value), {
initialProps: {
Expand All @@ -421,7 +421,7 @@ describe('`useAsyncIter` hook', () => {
{
renderedHook.rerender({ value: channel });

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

{
renderedHook.unmount();
expect(channelIterCloseSpy).toHaveBeenCalledOnce();
expect(channelReturnSpy).toHaveBeenCalledOnce();
}
});
});
Expand Down
28 changes: 14 additions & 14 deletions spec/utils/IterableChannelTestHelper.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export { IterableChannelTestHelper };

class IterableChannelTestHelper<T> implements AsyncIterableIterator<T>, AsyncDisposable {
isChannelClosed = false;
nextIteration = Promise.withResolvers<IteratorResult<T>>();
#isChannelClosed = false;
#nextIteration = Promise.withResolvers<IteratorResult<T>>();

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

get isClosed(): boolean {
return this.isChannelClosed;
return this.#isChannelClosed;
}

put(value: T): void {
if (this.isChannelClosed) {
if (this.#isChannelClosed) {
return;
}
this.nextIteration.resolve({ done: false, value });
this.nextIteration = Promise.withResolvers();
this.#nextIteration.resolve({ done: false, value });
this.#nextIteration = Promise.withResolvers();
}

complete(): void {
this.isChannelClosed = true;
this.nextIteration.resolve({ done: true, value: undefined });
this.#isChannelClosed = true;
this.#nextIteration.resolve({ done: true, value: undefined });
}

error(errValue?: unknown): void {
this.isChannelClosed = true;
this.nextIteration.reject(errValue);
this.nextIteration = Promise.withResolvers();
this.nextIteration.resolve({ done: true, value: undefined });
this.#isChannelClosed = true;
this.#nextIteration.reject(errValue);
this.#nextIteration = Promise.withResolvers();
this.#nextIteration.resolve({ done: true, value: undefined });
}

async next(): Promise<IteratorResult<T, void>> {
return this.nextIteration.promise;
return this.#nextIteration.promise;
}

async return(): Promise<IteratorReturnResult<void>> {
this.complete();
const res = await this.nextIteration.promise;
const res = await this.#nextIteration.promise;
return res as typeof res & { done: true };
}
}
9 changes: 9 additions & 0 deletions spec/utils/asyncIterToArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export { asyncIterToArray };

async function asyncIterToArray<T>(source: AsyncIterable<T>): Promise<T[]> {
const values: T[] = [];
for await (const value of source) {
values.push(value);
}
return values;
}
Loading
Loading