Testing

Jumbo React Next ships with two testing layers: Vitest + React Testing Library for unit and component tests, and Playwright for end-to-end tests. Coverage thresholds of 80% are enforced by default.

Unit and component tests

Vitest configuration

vitest.config.ts mirrors the TypeScript path aliases and configures jsdom as the test environment:

typescript
// vitest.config.ts
import react from '@vitejs/plugin-react';
import path from 'path';
import { defineConfig } from 'vitest/config';
 
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './vitest.setup.ts',
    exclude: ['**/node_modules/**', '**/e2e/**'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      thresholds: {
        statements: 80,
        branches:   80,
        functions:  80,
        lines:      80,
      },
    },
  },
  resolve: {
    alias: {
      '@/auth': path.resolve(__dirname, './auth'),
      '@':      path.resolve(__dirname, './src'),
      '@jumbo': path.resolve(__dirname, './@jumbo'),
      '@assets': path.resolve(__dirname, './@assets'),
    },
  },
});

Setup file

vitest.setup.ts imports @testing-library/jest-dom matchers so assertions like toBeInTheDocument() are available globally:

typescript
// vitest.setup.ts
import '@testing-library/jest-dom';

Running tests

bash
# pnpm
pnpm test              # run all unit tests
pnpm test:watch        # watch mode
pnpm test:coverage     # generate coverage report
pnpm test:ui           # open Vitest browser UI
 
# npm
npm run test
npm run test:coverage

Writing component tests

AAA pattern

Follow the Arrange–Act–Assert pattern for clarity:

typescript
// @jumbo/components/__tests__/JumboCard.test.tsx
import { render, screen } from '@testing-library/react';
import { JumboCard } from '../JumboCard';
import { describe, it, expect } from 'vitest';
 
describe('JumboCard', () => {
  it('renders the title prop', () => {
    // Arrange
    render(<JumboCard title="Revenue Overview" />);
 
    // Act + Assert
    expect(screen.getByText('Revenue Overview')).toBeInTheDocument();
  });
 
  it('renders children inside the card body', () => {
    render(
      <JumboCard title="Test Card">
        <p>Card content</p>
      </JumboCard>
    );
 
    expect(screen.getByText('Card content')).toBeInTheDocument();
  });
});

Testing with MUI theme

Wrap the component under test with a minimal ThemeProvider when it relies on theme tokens:

typescript
import { ThemeProvider, createTheme } from '@mui/material/styles';
import { render } from '@testing-library/react';
 
const theme = createTheme();
 
function renderWithTheme(ui: React.ReactElement) {
  return render(<ThemeProvider theme={theme}>{ui}</ThemeProvider>);
}

Testing @jumbo layout context

Components that use useJumboLayoutSidebarOptions or other layout hooks need JumboLayoutProvider in the test wrapper:

typescript
import { JumboLayoutProvider } from '@jumbo/components/JumboLayout';
import { defaultLayoutConfig } from '@/config/layouts/default';
 
function renderWithLayout(ui: React.ReactElement) {
  return render(
    <JumboLayoutProvider defaultLayoutConfig={defaultLayoutConfig}>
      {ui}
    </JumboLayoutProvider>
  );
}

Query priority

Use React Testing Library queries in this order:

  1. getByRole — most accessible and resilient
  2. getByLabelText — for labeled form inputs
  3. getByText — for visible text
  4. getByTestId — last resort only
typescript
// ✅ preferred
screen.getByRole('button', { name: /submit/i })
screen.getByLabelText('Email')
 
// ❌ avoid
container.querySelector('.submit-btn')

End-to-end tests

Playwright configuration

typescript
// playwright.config.ts (excerpt)
import { defineConfig, devices } from '@playwright/test';
 
export default defineConfig({
  testDir:   './e2e',
  baseURL:   'http://localhost:3000',
  use: {
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
  webServer: {
    command: 'npm run build && npm start',
    url:     'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

Running E2E tests

bash
# pnpm
pnpm test:e2e           # headless
pnpm test:e2e:headed    # with browser window
pnpm test:e2e:ui        # Playwright UI mode
 
# npm
npm run test:e2e

E2E test example

typescript
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';
 
test('redirects unauthenticated users to login page', async ({ page }) => {
  await page.goto('/en-US/dashboards/misc');
  await expect(page).toHaveURL(/auth\/login-1/);
});
 
test('logs in and lands on the misc dashboard', async ({ page }) => {
  await page.goto('/en-US/auth/login-1');
  await page.getByLabel('Email').fill('demo@example.com');
  await page.getByLabel('Password').fill('demo123');
  await page.getByRole('button', { name: /sign in/i }).click();
  await expect(page).toHaveURL(/dashboards\/misc/);
});

Coverage report

After running pnpm test:coverage, the HTML report is generated at coverage/index.html. Open it in your browser to see per-file and per-function coverage:

bash
pnpm test:coverage
open coverage/index.html

The build will fail in CI if any threshold drops below 80%.