Testing

Jumbo React Vite uses Vitest 4 as the test runner with React Testing Library 16 for component tests and @testing-library/user-event for simulating real user interactions.

Test stack

ToolVersionRole
Vitest4.1.xTest runner, coverage (v8)
React Testing Library16.3.xComponent query utilities
@testing-library/user-event14.6.xUser interaction simulation
@testing-library/jest-dom6.9.xCustom DOM matchers
jsdom29.xBrowser-like DOM environment

Running tests

bash
# Interactive watch mode
pnpm test
 
# Single run (CI)
pnpm test:run
 
# With coverage report
pnpm test:coverage

Coverage reports are written to ./coverage/ in text, json, and html formats.

Vitest configuration

typescript
// vitest.config.ts (key sections)
export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: ['./src/test/setup.ts'],
    css: true,
    coverage: {
      provider: 'v8',
      include: [
        'src/utilities/helpers/index.tsx',
        'src/translations/**/*.ts',
        '@jumbo/utilities/formatHelpers.ts',
        '@jumbo/utilities/helpers.ts',
        '@jumbo/utilities/styleHelpers.ts',
        '@jumbo/utilities/systemHelpers.ts',
        '@jumbo/utilities/cookies.ts',
      ],
      thresholds: {
        statements: 80,
        branches: 80,
        functions: 80,
        lines: 80,
      },
    },
  },
});

The include list scopes coverage to utilities and translation files. Component coverage is not currently enforced but is encouraged on new feature code.

renderWithProviders

Always use renderWithProviders instead of bare render from @testing-library/react. It wraps the component with the three providers required for most components in this project:

typescript
// src/test/renderWithProviders.tsx
const AllProviders = ({ children }: { children: ReactNode }) => (
  <LocalizationProvider dateAdapter={AdapterDayjs}>
    <ThemeProvider theme={defaultTheme}>
      <BrowserRouter>{children}</BrowserRouter>
    </ThemeProvider>
  </LocalizationProvider>
);
 
export const renderWithProviders = (ui, options?) =>
  render(ui, { wrapper: AllProviders, ...options });

Import it from @/test/renderWithProviders:

typescript
import { screen } from '@testing-library/react';
import { renderWithProviders } from '@/test/renderWithProviders';

Only use bare render for components that explicitly require zero providers (pure utility components, static display components).

Writing component tests

  1. Create a co-located test file

    Place the test file next to the component:

    src/components/MyWidget/
      MyWidget.tsx
      MyWidget.test.tsx
    
  2. Write the test

    tsx
    // src/components/MyWidget/MyWidget.test.tsx
    import { screen } from '@testing-library/react';
    import userEvent from '@testing-library/user-event';
    import { renderWithProviders } from '@/test/renderWithProviders';
    import { MyWidget } from './MyWidget';
     
    describe('MyWidget', () => {
      it('renders the widget title', () => {
        renderWithProviders(<MyWidget title='Revenue' value={42_500} />);
        expect(screen.getByText('Revenue')).toBeInTheDocument();
      });
     
      it('calls onAction when the button is clicked', async () => {
        const user = userEvent.setup();
        const handleAction = vi.fn();
     
        renderWithProviders(<MyWidget title='Revenue' onAction={handleAction} />);
     
        await user.click(screen.getByRole('button', { name: /refresh/i }));
        expect(handleAction).toHaveBeenCalledOnce();
      });
    });

Writing utility tests

Pure utility functions are tested without renderWithProviders:

typescript
// @jumbo/utilities/cookies.test.ts
import { getCookie, setCookie, eraseCookie } from '@jumbo/utilities/cookies';
 
describe('setCookie / getCookie', () => {
  afterEach(() => {
    eraseCookie('test-cookie');
  });
 
  it('stores and retrieves a string value', () => {
    setCookie('test-cookie', 'hello', 1);
    expect(getCookie('test-cookie')).toBe('hello');
  });
 
  it('returns null for a missing cookie', () => {
    expect(getCookie('nonexistent')).toBeNull();
  });
});

Coverage targets

Every file in the include list must maintain:

MetricThreshold
Statements80%
Branches80%
Functions80%
Lines80%

If pnpm test:coverage fails a threshold, the exit code is non-zero and CI will block the merge.