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. They follow the same principle as the core helpers: render the component into a fresh, isolated registry so each test starts from a known set of bloc instances.
import { renderWithBloc, renderWithRegistry } from '@blac/react/testing';renderWithBloc(ui, options)
Section titled “renderWithBloc(ui, options)”function renderWithBloc<T extends StateContainerConstructor>( ui: ReactElement, options: { bloc: T; state?: Partial<ExtractState<T>>; methods?: Partial<Record<MethodKeys<InstanceType<T>>, Function>>; args?: ExtractArgs<T>; deps?: Partial<ExtractDeps<T>>; },): 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 options (
state,methods,args,deps) - Registers it as an override keyed by the resolved
argsvalue - 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.
Aside from bloc, the options are exactly the createCubitStub options — including args (to run init()) and deps (to fire onDepsChanged). See Inputs for what those lanes mean.
Basic usage
Section titled “Basic usage”import { it, expect } from 'vite-plus/test';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
Section titled “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
Section titled “Mocking methods”Pass methods to replace specific methods on the stub. This is useful for intercepting side effects like API calls or navigation:
import { it, expect, vi } from 'vite-plus/test';import { screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';
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
Section titled “Named instances”If your component keys a bloc by args (e.g. useBloc(EditorCubit, { args: { docId } })), pass the same args so the stub registers under the matching instance key:
it('renders the correct editor', () => { renderWithBloc(<Editor docId="doc-42" />, { bloc: EditorCubit, state: { content: 'Hello world' }, args: { docId: 'doc-42' }, }); expect(screen.getByText('Hello world')).toBeInTheDocument();});args- and deps-based blocs
Section titled “args- and deps-based blocs”When a component renders a bloc that takes args or deps, you can pre-build the stub with those lanes so the component sees fully-initialized state. Pass args (runs init()) and deps (fires onDepsChanged) right alongside state:
it('renders a profile seeded from args and deps', () => { // function Profile() { // const [s] = useBloc(ProfileCubit, { args: { name: 'Alice' } }); // // deps are wired from a mount effect in app code; the test seeds them below. // ... // } const { bloc } = renderWithBloc(<Profile />, { bloc: ProfileCubit, args: { name: 'Alice' }, deps: { token: 'secret' }, });
expect(bloc.state.displayName).toBe('Alice'); expect(screen.getByText('Alice')).toBeInTheDocument();});Because the stub is seeded before render, the component reads the initialized state on its very first paint — no flush() needed for the initial values.
renderWithRegistry(ui, setup)
Section titled “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
Section titled “Multi-bloc components”import { it, expect } from 'vite-plus/test';import { screen } from '@testing-library/react';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
Section titled “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
Section titled “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
Section titled “Common patterns”Testing loading states
Section titled “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
Section titled “Testing user interactions”import { it, expect } from 'vite-plus/test';import { screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';
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
Section titled “Testing components with onMount”Components that use onMount to trigger data loading can have that method mocked:
import { it, expect, vi } from 'vite-plus/test';
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
Section titled “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
Section titled “See also”- Core Testing API — the stub/override/seeding primitives these helpers wrap
- Testing Overview — registry isolation and the import convention
- useBloc — the hook your components use, and how it resolves instance identity
- Inputs — the
args/depslanes you seed in tests