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, wrapped in aRegistryProviderfor that 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”Neither helper changes the global registry: stubs are set up under a temporary registry (like withTestRegistry) and the component reads it from a RegistryProvider. Nothing needs restoring when Testing Library’s cleanup() unmounts the tree, even if setup throws.
Core helpers called from the test body (acquire, borrow, watch) use the global registry, not the one the component renders into. Use the returned bloc, or the registry passed to the renderWithRegistry callback, to reach the test instances.
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