React Testing
The React testing utilities build on top of @blac/core/testing and @testing-library/react to provide a simple way to render components with controlled bloc state.
import { renderWithBloc, renderWithRegistry } from '@blac/react/testing';TIP
@testing-library/react is an optional peer dependency of @blac/react. Install it alongside your test runner to use these utilities.
renderWithBloc(ui, options)
function renderWithBloc<T extends StateContainerConstructor>(
ui: ReactElement,
options: {
bloc: T;
state?: Partial<ExtractState<T>>;
methods?: Partial<Record<MethodKeys<InstanceType<T>>, Function>>;
instanceKey?: string;
},
): RenderResult & { bloc: InstanceType<T> };Renders a React component with a single bloc pre-configured in an isolated registry. Under the hood it:
- Creates a fresh test registry
- Creates a cubit stub with the provided
stateandmethods - Registers it as an override
- Renders the component via
@testing-library/react - Wraps
unmount()to restore the previous registry
The returned object is the standard RenderResult from Testing Library, plus a bloc property containing the stub instance.
Basic usage
import { it, expect } from 'vitest';
import { screen } from '@testing-library/react';
import { renderWithBloc } from '@blac/react/testing';
it('displays the count', () => {
renderWithBloc(<Counter />, {
bloc: CounterCubit,
state: { count: 7 },
});
expect(screen.getByText('7')).toBeInTheDocument();
});Interacting with the bloc
The returned bloc is the live stub instance. Mutate it to test how your component responds to state changes:
import { act } from '@testing-library/react';
it('updates when count changes', () => {
const { bloc } = renderWithBloc(<Counter />, {
bloc: CounterCubit,
state: { count: 0 },
});
act(() => bloc.increment());
expect(screen.getByText('1')).toBeInTheDocument();
});Mocking methods
Pass methods to replace specific methods on the stub. This is useful for intercepting side effects like API calls or navigation:
it('calls save on form submit', async () => {
const mockSave = vi.fn();
renderWithBloc(<SettingsForm />, {
bloc: SettingsCubit,
state: { theme: 'dark', locale: 'en' },
methods: { save: mockSave },
});
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
expect(mockSave).toHaveBeenCalled();
});Named instances
If your component uses useBloc(Cubit, { instanceId }), pass the same key:
it('renders the correct editor', () => {
renderWithBloc(<Editor instanceId="doc-42" />, {
bloc: EditorCubit,
state: { content: 'Hello world' },
instanceKey: 'doc-42',
});
expect(screen.getByText('Hello world')).toBeInTheDocument();
});renderWithRegistry(ui, setup)
function renderWithRegistry(
ui: ReactElement,
setup: (registry: StateContainerRegistry) => void,
): RenderResult;Renders a component with a fresh registry that you configure via a callback. Use this when a component depends on multiple blocs, or when you need more control than renderWithBloc provides.
Multi-bloc components
import { renderWithRegistry } from '@blac/react/testing';
import { createCubitStub, registerOverride } from '@blac/core/testing';
it('shows dashboard with user and cart data', () => {
renderWithRegistry(<Dashboard />, () => {
registerOverride(
AuthCubit,
createCubitStub(AuthCubit, {
state: { loggedIn: true, name: 'Alice' },
}),
);
registerOverride(
CartCubit,
createCubitStub(CartCubit, {
state: { items: [{ id: '1', name: 'Widget', price: 9.99 }] },
}),
);
});
expect(screen.getByText('Alice')).toBeInTheDocument();
expect(screen.getByText('$9.99')).toBeInTheDocument();
});Mixing with core helpers
The setup callback runs with the test registry active, so all @blac/core/testing helpers work inside it:
import { withBlocState, withBlocMethod } from '@blac/core/testing';
it('renders notification list', () => {
renderWithRegistry(<NotificationPanel />, () => {
withBlocState(AuthCubit, { loggedIn: true, userId: 'u1' });
withBlocState(NotificationCubit, {
items: [
{ id: '1', message: 'Welcome!', read: false },
{ id: '2', message: 'New feature', read: true },
],
});
});
expect(screen.getByText('Welcome!')).toBeInTheDocument();
expect(screen.getAllByRole('listitem')).toHaveLength(2);
});Cleanup
Both renderWithBloc and renderWithRegistry wrap the Testing Library unmount() to restore the original registry. This means cleanup happens automatically when:
- You call
unmount()on the render result - Testing Library's
cleanup()runs (automatic in most setups)
If you're also using blacTestSetup() in the same file, that's fine — they don't conflict. The afterEach hook from blacTestSetup provides an extra safety net.
Common patterns
Testing loading states
it('shows a spinner while loading', () => {
renderWithBloc(<ArticleList />, {
bloc: ArticleCubit,
state: { articles: [], status: 'loading', error: null },
});
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});
it('shows articles after load', () => {
renderWithBloc(<ArticleList />, {
bloc: ArticleCubit,
state: {
articles: [{ id: '1', title: 'Hello' }],
status: 'success',
error: null,
},
});
expect(screen.getByText('Hello')).toBeInTheDocument();
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
});
it('shows error message on failure', () => {
renderWithBloc(<ArticleList />, {
bloc: ArticleCubit,
state: { articles: [], status: 'error', error: 'Network error' },
});
expect(screen.getByText('Network error')).toBeInTheDocument();
});Testing user interactions
it('adds a todo item', async () => {
const { bloc } = renderWithBloc(<TodoApp />, {
bloc: TodoCubit,
state: { items: [], filter: 'all' },
});
await userEvent.type(screen.getByRole('textbox'), 'Buy milk');
await userEvent.click(screen.getByRole('button', { name: 'Add' }));
expect(bloc.state.items).toContainEqual(
expect.objectContaining({ text: 'Buy milk' }),
);
});Testing components with onMount
Components that use onMount to trigger data loading can have that method mocked:
it('calls fetchData on mount', () => {
const mockFetch = vi.fn();
renderWithBloc(<DataView />, {
bloc: DataCubit,
state: { items: [], status: 'idle' },
methods: { fetchData: mockFetch },
});
expect(mockFetch).toHaveBeenCalledOnce();
});Testing components that read getters
Components using bloc.total or similar getters work naturally since the stub is a real instance:
it('displays computed total', () => {
renderWithBloc(<CartSummary />, {
bloc: CartCubit,
state: {
items: [
{ id: '1', price: 10 },
{ id: '2', price: 20 },
],
},
});
expect(screen.getByText('$30')).toBeInTheDocument();
});The getter runs against real state, so you test real logic — not a mocked return value.
See also: Testing Overview, Core Testing API, useBloc