====== Vite + Vitest Testing Strategy for React SPA ======
===== 1. Context & Goals =====
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).
==== Goals ====
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
===== 3. Recommended Testing Stack =====
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)
===== 4. Test Classification & Scope =====
==== Example Code Snippets (Vite + Vitest + React) ====
=== 4.0.1 Main App & Layout Wiring ===
// src/app/App.tsx\\ import { BrowserRouter } from 'react-router-dom';\\ import RoutesConfig from './routes';\\ \\ export default function App() {\\ return (\\ \\ \\ \\ );\\ }
// 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 = () => (\\ <>\\ \\
\\ \\ \\ \\ \\ >\\ );
// 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 (\\ \\ }>\\ } />\\ } />\\ \\ \\ );\\ }
===== 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'],\\ },\\ },\\ });
==== Test Setup ====
// 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();\\ expect(screen.getByText(/inventory/i)).toBeInTheDocument();\\ });\\ });
===== 12. Integration Test Example (Vitest + RTL + MSW) =====
==== MSW Handler ====
// 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';\\ \\ beforeAll(() => server.listen());\\ afterEach(() => server.resetHandlers());\\ afterAll(() => server.close());
==== 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();\\ \\ await waitFor(() => {\\ expect(screen.getByText('Item A')).toBeInTheDocument();\\ });\\ });
===== 10. Final Recommendation =====
| Area | Choice |
| Build Tool | Vite |
| Unit Tests | Vitest |
| API Mocking | MSW |
| UI Testing | RTL |
| E2E | Playwright |
| Auth Strategy | Shared Token |
===== 11. API Client with Token Interceptor =====
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.use((config) => {\\ const token = sessionStorage.getItem('auth_token');\\ if (token) {\\ config.headers.Authorization = `Bearer ${token}`;\\ }\\ return config;\\ });\\ \\ export default apiClient;
// src/services/inventoryService.ts\\ import apiClient from './apiClient';\\ \\ export const fetchInventory = async () => {\\ const response = await apiClient.get('/inventory');\\ return response.data;\\ };
===== 12. Playwright Global Auth + Hybrid MSW E2E =====
==== Global Authentication Setup ====
// e2e/playwright/global-setup.ts\\ import { chromium } from '@playwright/test';\\ \\ export default async () => {\\ const browser = await chromium.launch();\\ const page = await browser.newPage();\\ \\ await page.goto(process.env.AUTH_URL!);\\ await page.fill('#username', process.env.E2E_USER!);\\ await page.fill('#password', process.env.E2E_PASSWORD!);\\ await page.click('button[type=submit]');\\ \\ await page.waitForURL('**/home');\\ await page.context().storageState({ path: 'e2e/auth.json' });\\ \\ await browser.close();\\ };
// e2e/playwright.config.ts\\ import { defineConfig } from '@playwright/test';\\ \\ export default defineConfig({\\ globalSetup: './e2e/playwright/global-setup.ts',\\ use: {\\ storageState: 'e2e/auth.json',\\ },\\ });
==== Hybrid E2E Test ====
// e2e/inventory.hybrid.spec.ts\\ import { test, expect } from '@playwright/test';\\ \\ test('inventory loads with mocked API', async ({ page }) => {\\ await page.route('**/api/inventory', async (route) => {\\ await route.fulfill({\\ status: 200,\\ contentType: 'application/json',\\ body: JSON.stringify([{ id: 1, name: 'Mock Item' }]),\\ });\\ });\\ \\ await page.goto('/inventory');\\ await expect(page.getByText('Mock Item')).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 Promise((resolve) => queue.push(resolve));\\ }\\ \\ 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.forEach((cb) => cb());\\ queue = [];\\ isRefreshing = false;\\ \\ return apiClient(originalRequest);\\ }\\ \\ return Promise.reject(error);\\ }\\ );\\ \\ export default apiClient;
===== 15. API Contract Versioning Strategy =====
==== Versioning Rules ====
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 }]\\ }\\ }\\ }\\ }
==== Playwright Performance Smoke ====
// 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