Testing
There are two things worth testing separately: the task graph itself (retry logic,
ordering, failure handling — pure async logic, no React needed), and components that
render <Initializer> or call useInitializer().
Testing tasks directly with createInitializer
Since the runner has no React dependency, the fastest way to test task logic is the core
API directly — no render(), no act(), just await:
import { describe, expect, it, vi } from 'vitest';
import { createInitializer } from '@lamstack/initializer';
it('retries a flaky task and eventually succeeds', async () => {
let attempts = 0;
const initializer = createInitializer({
tasks: [
{
id: 'flaky',
retry: 3,
run: async () => {
attempts += 1;
if (attempts < 3) throw new Error('not yet');
},
},
],
});
await initializer.run();
expect(attempts).toBe(3);
expect(initializer.getSnapshot().status).toBe('completed');
});
it('records which task caused a critical failure', async () => {
const onError = vi.fn();
const initializer = createInitializer({
tasks: [
{
id: 'auth',
run: async () => {
throw new Error('session expired');
},
},
],
onError,
});
await initializer.run();
expect(initializer.getSnapshot().status).toBe('failed');
expect(onError).toHaveBeenCalledWith({ taskId: 'auth', error: expect.any(Error) });
});Testing components that render <Initializer>
<Initializer> withholds children until every task settles, so assertions about your
app need waitFor — the same reason @lamstack/react-dialog’s async open() calls do.
import { render, screen, waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Initializer } from '@lamstack/react-initializer';
it('renders the app once initialization completes', async () => {
render(
<Initializer tasks={[{ id: 'config', run: async () => {} }]}>
<div>App ready</div>
</Initializer>,
);
expect(screen.getByText(/Initializing/)).toBeInTheDocument();
await waitFor(() => expect(screen.getByText('App ready')).toBeInTheDocument());
});For a critical failure, assert on the error screen (or your own, if you passed
errorScreen) rather than on children:
it('shows the error screen when a critical task fails', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
render(
<Initializer
tasks={[
{
id: 'auth',
run: async () => {
throw new Error('boom');
},
},
]}
>
<div>App ready</div>
</Initializer>,
);
await waitFor(() => expect(screen.getByText('Initialization Failed')).toBeInTheDocument());
consoleSpy.mockRestore();
});console.error is spied and silenced above because task failures are logged there by
design (see Concepts) —
without the spy, a deliberately-failing test still prints a real error to the test
runner’s output.
Testing useInitializer() consumers
Since children only mounts after status === 'completed', a component that calls
useInitializer() can be rendered directly under a real <Initializer> with tasks that
resolve immediately — no need to mock the context:
function ReloadButton() {
const { retry } = useInitializer();
return <button onClick={retry}>Reload</button>;
}
it('retry() restarts the sequence', async () => {
let attempts = 0;
render(
<Initializer
tasks={[
{
id: 'a',
run: async () => {
attempts += 1;
},
},
]}
>
<ReloadButton />
</Initializer>,
);
await waitFor(() => expect(screen.getByText('Reload')).toBeInTheDocument());
fireEvent.click(screen.getByText('Reload'));
await waitFor(() => expect(attempts).toBe(2));
});Reference
@lamstack/react-initializer’s own test suite
(packages/react-initializer/src/core
and
.../src/react/Initializer.test.tsx)
covers the full matrix — stage ordering, parallel() concurrency (and concurrency
caps), retry/retryDelay, timeout, critical/non-critical failures, condition, and
cancellation — worth reading if you need to test any of those edge cases in your own app.