This document defines best practices for testing a React SPA built with Vite. The application is UI-focused, API-driven, and optimized for fast rendering of large datasets (inventory-style screens).
Align testing with the Vite build ecosystem
Ensure fast, reliable, and deterministic tests
Enable auth token reuse across Unit, Integration, and E2E tests
Support CI/CD quality gates
React + TypeScript + Vite
│
├── Unit Tests → Vitest + React Testing Library
├── Integration Tests → Vitest + RTL + MSW
├── API Contract → MSW
├── Hybrid E2E → Playwright + MSW
└── Full E2E → Playwright (real backend)
src/app/App.tsx
import { BrowserRouter } from 'react-router-dom';
import RoutesConfig from './routes';
export default function App() {
return (
<BrowserRouter>
<RoutesConfig />
</BrowserRouter>
);
}
src/layouts/MainLayout/MainLayout.tsx
import { Outlet } from 'react-router-dom';
import { Header } from '@/components/layout/Header';
import { Footer } from '@/components/layout/Footer';
import { Menu } from '@/components/layout/Navigation';
export const MainLayout = () ⇒ (
<>
<Header />
<Menu />
<main>
<Outlet />
</main>
<Footer />
</>
);
src/app/routes.tsx
import { Routes, Route } from 'react-router-dom';
import { MainLayout } from '@/layouts/MainLayout';
import HomePage from '@/pages/Home/HomePage';
import InventoryPage from '@/pages/Inventory/InventoryPage';
export default function RoutesConfig() {
return (
<Routes>
<Route element={<MainLayout />}>
<Route path=“/” element={<HomePage />} />
<Route path=“/inventory” element={<InventoryPage />} />
</Route>
</Routes>
);
}
===== 4. Test Classification & Scope =====
==== 4.1 Unit Tests ====
Purpose: Validate isolated UI logic
What to test - Presentational components - Hooks - Utility functions
What NOT to test - API calls - Auth flows
Tools - Vitest - React Testing Library
==== 4.2 Integration Tests ====
Purpose: Validate UI + API interaction
What to test - API data rendering - Error states - Token attached to requests
Tools - Vitest - RTL - MSW (Mock Service Worker)
==== 4.3 API Contract Tests (via MSW) ====
Validate request headers, query params, payloads
Ensure frontend-backend contract stability
==== 4.4 Hybrid E2E Tests ====
Purpose: Fast, stable E2E without backend dependency
Characteristics - Real browser (Playwright) - Real authentication token - Business APIs mocked via MSW
==== 4.5 Full E2E Tests ====
Purpose: Validate critical production flows
Real backend
Real authentication
Minimal coverage (smoke tests only)
===== 5. Authentication Token Strategy (Single Source of Truth) =====
==== Design Principles ====
Fetch auth token once
Reuse across all test layers
No hardcoding inside tests
==== Flow ====
Auth Token
│
├─ Generated via Playwright global setup
├─ Stored in env / storageState
├─ Consumed by MSW handlers
└─ Used by Vitest integration tests
==== Benefits ====
Consistent auth behavior
Faster test execution
Reduced flakiness
===== 6. Project Directory Structure =====
The following directory structure is the recommended standard for this React + TypeScript + Vite SPA. It cleanly separates app bootstrapping, layouts, pages, features, services, and tests, and scales well for large inventory-style applications.
src/
├── app/
│ ├── App.tsx
│ ├── routes.tsx
│ └── providers.tsx
│
├── layouts/
│ └── MainLayout/
│ ├── MainLayout.tsx
│ └── index.ts
│
├── components/
│ ├── layout/
│ │ ├── Header/
│ │ │ ├── Header.tsx
│ │ │ └── index.ts
│ │ ├── Footer/
│ │ │ ├── Footer.tsx
│ │ │ └── index.ts
│ │ └── Navigation/
│ │ ├── Menu.tsx
│ │ └── index.ts
│ │
│ └── common/
│ ├── Button/
│ ├── Input/
│ ├── Loader/
│ └── Modal/
│
├── pages/
│ ├── Home/
│ │ ├── HomePage.tsx
│ │ └── index.ts
│ │
│ ├── Inventory/
│ │ ├── InventoryPage.tsx
│ │ ├── InventoryTable.tsx
│ │ └── index.ts
│ │
│ └── NotFound/
│ └── NotFoundPage.tsx
│
├── features/
│ └── inventory/
│ ├── components/
│ ├── hooks/
│ ├── services/
│ └── types.ts
│
├── services/
│ ├── apiClient.ts
│ └── inventoryService.ts
│
├── hooks/
│ └── useDebounce.ts
│
├── types/
│ └── common.types.ts
│
├── utils/
│ └── formatters.ts
│
├── styles/
│ └── global.css
│
├── tests/
│ ├── unit/
│ ├── integration/
│ └── msw/
│
├── main.tsx
└── vite-env.d.ts
==== Structural Guidelines ====
app/ – Application bootstrap, routing, and providers
layouts/ – Shared page layouts (Header, Footer, Navigation)
components/ – Reusable UI components
pages/ – Route-level components
features/ – Feature-scoped logic for scalability
services/ – API and backend interaction layer
tests/ – Unit, integration, and MSW setup
===== 7. CI/CD Best Practices =====
==== Pipeline Order ====
Lint + Type Check
Unit Tests (Vitest)
Integration Tests (Vitest + MSW)
Hybrid E2E (Playwright)
Optional Full E2E (nightly)
==== Failure Handling ====
Fail pipeline on any test failure
Upload Playwright traces/screenshots
Publish test reports
===== 8. Performance Considerations =====
Prefer client-side rendering (SPA)
Use virtualization for large datasets
Avoid SSR unless SEO is required
Testing should not depend on real backend performance
===== 10. Tooling Configuration (Vitest + RTL) =====
==== Vitest Configuration ====
vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
setupFiles: './src/tests/setupTests.ts',
globals: true,
css: true,
coverage: {
reporter: ['text', 'html'],
},
},
});
src/tests/setupTests.ts
import '@testing-library/jest-dom';
===== 11. Unit Test Example (Vitest + RTL) =====
src/tests/unit/Header.test.tsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Header } from '@/components/layout/Header';
describe('Header', () ⇒ {
it('renders application title', () ⇒ {
render(<Header />);
expect(screen.getByText(/inventory/i)).toBeInTheDocument();
});
});
src/tests/msw/handlers.ts
import { rest } from 'msw';
export const handlers = [
rest.get('/api/inventory', (req, res, ctx) ⇒ {
const token = req.headers.get('Authorization');
if (!token) return res(ctx.status(401));
return res(
ctx.status(200),
ctx.json([{ id: 1, name: 'Item A' }])
);
}),
];
==== MSW Server Setup ====
src/tests/msw/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(…handlers);
src/tests/setupTests.ts
import '@testing-library/jest-dom';
import { server } from './msw/server';
beforeAll1);
afterEach2);
afterAll3);
==== Integration Test ====
src/tests/integration/InventoryPage.test.tsx
import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import InventoryPage from '@/pages/Inventory/InventoryPage';
it('renders inventory data from API', async () ⇒ {
render(<InventoryPage />);
await waitFor4).toBeInTheDocument();
});
});
| Area | Choice |
| Build Tool | Vite |
| Unit Tests | Vitest |
| API Mocking | MSW |
| UI Testing | RTL |
| E2E | Playwright |
| Auth Strategy | Shared Token |
Centralized API client ensures consistent auth handling, easy mocking, and reuse across Unit, Integration, and E2E tests.
src/services/apiClient.ts
import axios from 'axios';
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
});
apiClient.interceptors.request.use5).toBeVisible();
});
===== 13. CI/CD Pipeline Snippets =====
==== GitHub Actions ====
# .github/workflows/frontend-ci.yml
name: Frontend CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm run test:unit
- run: npm run test:integration
- run: npx playwright install –with-deps
- run: npm run test:e2e
==== GitLab CI ====
frontend_tests:
image: node:18
stage: test
script:
- npm ci
- npm run test:unit
- npm run test:integration
- npx playwright install –with-deps
- npm run test:e2e
artifacts:
when: always
paths:
- playwright-report/
===== 14. Token Refresh & Expiry Handling =====
==== Design Principles ====
Never refresh tokens inside UI components
Centralize refresh logic in API client
Retry failed requests transparently
src/services/apiClient.ts (extended)
import axios from 'axios';
let isRefreshing = false;
let queue: any[] = [];
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
});
apiClient.interceptors.response.use(
(res) ⇒ res,
async (error) ⇒ {
const originalRequest = error.config;
if (error.response?.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise6);
}
originalRequest._retry = true;
isRefreshing = true;
const refreshToken = sessionStorage.getItem('refresh_token');
const response = await axios.post('/auth/refresh', { refreshToken });
sessionStorage.setItem('auth_token', response.data.accessToken);
queue.forEach7);
queue = [];
isRefreshing = false;
return apiClient(originalRequest);
}
return Promise.reject(error);
}
);
export default apiClient;
APIs must be versioned (URI or header based)
Breaking changes require new version
MSW enforces contract compatibility
src/tests/msw/handlers.ts
rest.get('/api/v1/inventory', (req, res, ctx) ⇒ {
return res(ctx.json([{ id: 1, name: 'Item V1' }]));
});
rest.get('/api/v2/inventory', (req, res, ctx) ⇒ {
return res(ctx.json([{ id: 1, name: 'Item V2', stock: 100 }]));
});
===== 16. Performance Testing (Lighthouse + Playwright) =====
==== Lighthouse CI ====
npm install -D @lhci/cli
lighthouserc.json
{
“ci”: {
“collect”: {
“url”: [“http://localhost:5173”],
“startServerCommand”: “npm run preview”
},
“assert”: {
“assertions”: {
“categories:performance”: [“warn”, { “minScore”: 0.8 }]
}
}
}
}
e2e/performance.spec.ts
import { test } from '@playwright/test';
test('home page loads under 2s', async ({ page }) ⇒ {
const start = Date.now();
await page.goto('/');
const duration = Date.now() - start;
expect(duration).toBeLessThan(2000);
});
===== 17. Starter Vite Repository Blueprint =====
==== Scripts ====
{
“scripts”: {
“dev”: “vite”,
“build”: “vite build”,
“preview”: “vite preview”,
“test:unit”: “vitest”,
“test:integration”: “vitest run”,
“test:e2e”: “playwright test”,
“perf”: “lhci autorun”
}
}
==== Environment Files ====
.env
.env.test
.env.e2e
==== Ready-to-Use ====
Vite + React + TS
Vitest + MSW
Playwright (auth + hybrid)
CI/CD ready
Performance gates included