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:
// 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:
// vitest.setup.ts
import '@testing-library/jest-dom';Running tests
# 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:coverageWriting component tests
AAA pattern
Follow the Arrange–Act–Assert pattern for clarity:
// @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:
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:
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:
getByRole— most accessible and resilientgetByLabelText— for labeled form inputsgetByText— for visible textgetByTestId— last resort only
// ✅ preferred
screen.getByRole('button', { name: /submit/i })
screen.getByLabelText('Email')
// ❌ avoid
container.querySelector('.submit-btn')End-to-end tests
Playwright configuration
// 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
# pnpm
pnpm test:e2e # headless
pnpm test:e2e:headed # with browser window
pnpm test:e2e:ui # Playwright UI mode
# npm
npm run test:e2eE2E test example
// 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/);
});The e2e/ directory uses Playwright Page Objects (BasePage.ts, etc.) for reusable navigation
helpers. Create a page object for any multi-step flow you test more than once.
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:
pnpm test:coverage
open coverage/index.htmlThe build will fail in CI if any threshold drops below 80%.