Skip to Content

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.

Last updated on