@suerg/test-kit
v0.1.33
Published
Typed, batteries-included helpers for building stable, readable integration tests with React Testing Library on both React (web) and React Native.
Readme
@suerg/test-kit
Typed, batteries-included helpers for building stable, readable integration tests with React Testing Library on both React (web) and React Native.
- Purpose: provide a small, strongly-typed “kit” you compose once per test that bundles common test ergonomics: Redux state setup, API mocking, interactions, router control, time control, and more.
- Design: plugin-based. Add only what you need; bring your own app providers via global setup.
Default plugins included when you create a kit:
- flow, api, interactions, keyboard, date, performance, dnd, page (web)
- flow, api, interactions, date, performance (native)
- state (web and native; requires a Redux store factory via global setup)
- router is available via
routerPlugin(...)when a router environment is configured
Install
React (web):
npm i -D @suerg/test-kit @testing-library/react @testing-library/user-event @reduxjs/toolkit react-redux react-dom
# Optional but common in web apps
npm i -D @testing-library/jest-domReact Native:
npm i -D @suerg/test-kit @testing-library/react-native @reduxjs/toolkit react-redux react-domIf you intend to use the router plugin on web with Next.js, also ensure a single router instance is available to tests (e.g., next-router-mock or Next’s useRouter() module instance).
Global setup (one-time)
Provide your Redux store factory and any app-wide providers. Optionally provide a router getter.
React (example):
// jest.setup.ts
/* eslint-disable */
import React from 'react';
import { setupTestKit, NextRouterLike } from '@suerg/test-kit';
import { configureStore } from '@reduxjs/toolkit';
import { ThemeProvider } from '@mui/material/styles';
import rootReducer from '@/redux/rootReducer';
import { TestQueryClientProvider } from './__tests__/helpers/testQueryClientWrapper';
import theme from './theme';
setupTestKit({
makeStore: (preloaded) =>
configureStore({ reducer: rootReducer, preloadedState: preloaded }),
contextProviders: [
({ children }) =>
React.createElement(ThemeProvider, { theme }, children),
({ children }) =>
React.createElement(TestQueryClientProvider, null, children),
],
router: {
getRouter: (): NextRouterLike | undefined => {
try {
return require('next/router').default as NextRouterLike;
} catch {
return undefined;
}
},
},
});React Native (example):
// jest.setup.(ts|js)
/* eslint-disable */
import React from 'react';
import { setupTestKit } from '@suerg/test-kit';
import { Provider as PaperProvider } from 'react-native-paper';
import {
SafeAreaProvider,
initialWindowMetrics,
} from 'react-native-safe-area-context';
import { makeStore } from '@/src/store/store';
import { theme } from '@/src/constants/theme';
setupTestKit({
makeStore: (preloaded) => makeStore(preloaded),
contextProviders: [
({ children }) =>
React.createElement(PaperProvider, { theme }, children),
({ children }) =>
React.createElement(
SafeAreaProvider,
{ initialMetrics: initialWindowMetrics },
children
),
],
});Notes
- The
statePlugin()uses yourmakeStore(preloaded)and wraps the UI underreact-redux’sProviderplus anycontextProvidersyou pass above. - For web Next.js routing,
routerPlugin({ type: 'next' })requiressetupTestKit({ router: { getRouter } })to return the single live router instance used by tests and app.
React Native router setup (React Navigation):
// jest.setup.(ts|js)
/* eslint-disable */
import { setupTestKit } from '@suerg/test-kit';
import { createNavigationContainerRef } from '@react-navigation/native';
export const navigationRef = createNavigationContainerRef();
setupTestKit({
makeStore: (preloaded) => makeStore(preloaded),
router: {
getRouter: () => navigationRef.current,
},
});In tests, render your navigator with the same ref and use the router plugin:
import { NavigationContainer } from '@react-navigation/native';
import { navigationRef } from '../jest.setup';
const kit = createKitNative(routerPlugin({ type: 'react-navigation' }));
render(
kit.state.renderWithState(
<NavigationContainer ref={navigationRef}>
<AppNavigator />
</NavigationContainer>
)
);
await kit.router.navigate('Details', { id: '1' });Creating a kit
Web:
import {
createKit,
makeKitBuilder,
statePlugin,
routerPlugin,
pagePlugin,
} from '@suerg/test-kit';
// One-off kit
const kit = createKit(statePlugin(), routerPlugin({ type: 'next' }));
// Reusable builder with defaults
export const makeAppKit = makeKitBuilder(
statePlugin(),
routerPlugin({ type: 'next' })
);
const appKit = makeAppKit(
pagePlugin(({ screen, user }) => ({
clickSave: () => user.click(screen.getByText('Save')),
}))
);React Native:
import { createKitNative, statePlugin } from '@suerg/test-kit';
const kit = createKitNative(statePlugin());Rendering and interacting (integration test examples)
Web (Next.js style):
import { render, screen, waitFor } from '@testing-library/react';
import { makeKitBuilder, statePlugin, routerPlugin } from '@suerg/test-kit';
import { MonthView } from '@/features/calendar/components/views/MonthView';
const makeMonthKit = makeKitBuilder(statePlugin(), routerPlugin({ type: 'next' }));
const kit = makeMonthKit();
render(kit.state.renderWithState(<MonthView locationId="1" addShiftMode={false} addBlockMode={false} />));
await kit.flow.act(async (user) => {
await user.click(await screen.findByRole('gridcell', { name: /January 15, 2024/i }));
});
await waitFor(() => expect(kit.router.getLocation().path).toContain('date=2024-01-15'));React Native (React Navigation style):
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react-native';
import { createKitNative, statePlugin } from '@suerg/test-kit';
import RootStackNavigator from '@/src/navigation/RootStackNavigator';
const kit = createKitNative(statePlugin());
render(kit.state.renderWithState(<RootStackNavigator />));
await kit.flow.act(async (user) => {
await user.press(await screen.findByText('Save'));
});
await waitFor(() => expect(screen.getByText('Login')).toBeDefined());If your component relies on selector-driven effects and you need to rerender while preserving the same Redux store, use the stable-store helpers:
import React from 'react';
import { render, act } from '@testing-library/react-native';
import { createKitNative, statePlugin } from '@suerg/test-kit';
const kit = createKitNative(statePlugin());
const ui = <RootStackNavigator />;
const { rerender } = render(kit.state.renderWithStableStore(ui));
// flip a flag in state and rerender without recreating the store
await act(async () => {
kit.state.rerenderWithStableStore(rerender, ui);
});API Plugin (HTTP mocking)
Available as part of defaults (web and native):
- onGet/onPost/onPut/onPatch/onDelete(path, body, status = 200, repeat = 1)
- onGetHang(path)
- chaos: rateLimit(path), serverError(path), timeout(path), networkError(path)
- getCalls(method?, path?) -> ApiCallRecord[]
- expectCalledTimes(method, path, times)
- expectAbortedTimes(method, path, times)
- waitForIdle(), expectNoPending(), clear()
- getAbortedCalls()
Debug logging
- Pass a boolean to enable logs programmatically:
apiPlugin({ debug: true }). - Or set the environment variable to a boolean string:
TEST_KIT_API_DEBUG=true.
Example:
cross-env TEST_KIT_API_DEBUG=true npm testExample:
kit.api.onGet('/api/users', { users: [] }, 200);
await fetch('/api/users');
await kit.api.expectCalledTimes('GET', '/api/users', 1);
await kit.api.expectNoPending();PATCH example:
kit.api.onPatch('/api/items/1', { ok: true }, 200);
await fetch('/api/items/1', { method: 'PATCH' });
await kit.api.expectCalledTimes('PATCH', '/api/items/1', 1);State Plugin (Redux)
Helpers:
- store(): returns the configured store instance created with current presets/patch
- use(preset): register a function that derives a state patch from the initial state
- withPatch(patch): merge-in static preloaded state
- withProviders(providers): add more wrapper providers for this kit
- renderWithState(ui): wraps
uiwithProviderand extra providers - renderWithStableStore(ui): like renderWithState, but keeps a single persistent store across rerenders
- rerenderWithStableStore(rerender, ui): convenience to rerender with the same persistent store
- stubState(path, value) or stubState({ nested: patches })
Example:
const kit = createKit(statePlugin());
kit.state.stubState('auth.user.id', '1');
render(kit.state.renderWithState(<App />));Flow Plugin
- act(async (user) => {
/* interactions */}): wraps in RTLact()and flushes microtasks
Interactions Plugins
Web (interactionsPlugin):
- clickCell(label)
- clickButton(label)
- clickByText(text)
- clickByTestId(testId)
- typeText(labelOrTestId, text)
- selectViaKb(label)
- hoverElement(label)
- hoverText(text)
- clearSelections()
- expectSelected(label)
- expectNotSelected(label)
React Native (interactionsNativePlugin):
- tapByText(text)
- tapByTestId(testId)
- typeText(testIdOrLabel, text)
- longPressByText(text)
- longPressByTestId(testId)
Keyboard Plugin (web)
- keyboard(seq: string): delegates to userEvent.keyboard
await kit.keyboard('{Tab}{Enter}');DnD Plugin (web)
- drop(element, data?): fires dragEnter/dragOver/drop with simple dataTransfer
Performance Plugin
- shouldCompleteWithin(ms)
- shouldRenderWithin(ms)
- shouldUpdateWithin(ms)
- shouldInteractWithin(ms)
- run(testFn)
kit.performance.shouldRenderWithin(25);
await kit.performance.run(async () => {
render(kit.state.renderWithState(<App />));
});Date Plugin
Freezes Date to a fixed moment without switching to fake timers. Useful when you prefer real timers but deterministic time.
import { datePlugin } from '@suerg/test-kit';
const kit = createKit(datePlugin(new Date('2024-02-01T00:00:00Z')));Router Plugin
Use with a configured environment via setupTestKit({ router: { getRouter } }).
Web (Next.js):
import { routerPlugin } from '@suerg/test-kit';
const kit = createKit(routerPlugin({ type: 'next' }));
kit.router.getLocation();
await kit.router.navigate({ pathname: '/users', query: { q: 'joe' } });React Native (React Navigation):
Two options:
- Use a configured router environment (preferred):
import { routerPlugin } from '@suerg/test-kit';
const kit = createKitNative(routerPlugin({ type: 'react-navigation' }));
await kit.router.navigate('Details', { id: '1' });- Or pass an explicit adapter built from a
NavigationContainerref:
import { routerPlugin, createReactNavigationAdapter } from '@suerg/test-kit';
const adapter = createReactNavigationAdapter(navigationRef.current);
const kit = createKitNative(routerPlugin(adapter));
await kit.router.navigate('Details', { id: '1' });Typing your Redux RootState (recommended)
Augment the module so helpers are typed against your real RootState.
// types/test-kit.d.ts
import type rootReducer from '@/redux/rootReducer';
export type RootState = ReturnType<typeof rootReducer>;
declare module '@suerg/test-kit' {
interface TestKitReduxState extends RootState {}
}Writing integration tests
- Use
statePlugin().renderWithState(ui)to render your real component under real providers. - Drive user behavior via
kit.flow.actand interactions helpers instead of manualact()and timers. - Mock APIs with
kit.apiinstead of ad-hoc jest mocks; assert calls withexpectCalledTimes. - For router-aware components, prefer
routerPluginand assert viakit.router.getLocation().
