User Tools

Site Tools


react

This is an old revision of the document!


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


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</font>
import { BrowserRouter } from 'react-router-dom';
import RoutesConfig from './routes';

export default function App() {
return (
<BrowserRouter>
<RoutesConfig />
</BrowserRouter>
);
} src/layouts/MainLayout/MainLayout.tsx</font>
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</font>
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 ==== CKGE_TMP_i
vitest.config.ts CKGE_TMP_i
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' ] ,
} ,
} ,
}) ; ==== <font 14pt/Aptos,sans-serif;;#0f4761;;inherit>Test Setup
==== CKGE_TMP_i src/tests/setupTests.ts CKGE_TMP_i</font>
import '@testing-library/jest-dom' ; —- ===== 11. Unit Test Example (Vitest + RTL) ===== src/tests/unit/Header.test.tsx</font>
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();
});
}); —- ===== 12. Integration Test Example (Vitest + RTL + MSW) ===== ==== MSW Handler ==== CKGE_TMP_i
src/tests/msw/handlers.ts CKGE_TMP_i
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' }])
) ;
}) ,
] ; ==== <font 14pt/Aptos,sans-serif;;#0f4761;;inherit>MSW Server Setup
==== CKGE_TMP_i src/tests/msw/server.ts CKGE_TMP_i</font>
import { setupServer } from 'msw/node' ;
import { handlers } from './handlers' ;

export const server = setupServer ( handlers) ; CKGE_TMP_i src/tests/setupTests.ts CKGE_TMP_i</font>
import '@testing-library/jest-dom' ;
import { server } from './msw/server' ;

beforeAll 1)

1)
)</font> server . listen ()) ;
afterEach (()</font> server . resetHandlers ()) ;
afterAll (()</font> server . close ()) ; ==== Integration Test ==== src/tests/integration/InventoryPage.test.tsx</font>
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 waitFor(() ⇒ {</font>
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. CKGE_TMP_i src/services/apiClient.ts CKGE_TMP_i</font>
import axios from 'axios' ;

const apiClient = axios . create ({
baseURL : import . meta . env . VITE_API_BASE_URL ,
}) ;

apiClient . interceptors . request . use ((config)</font> {
const token = sessionStorage . getItem ( 'auth_token' ) ;
if (token) {
config . headers . Authorization = `Bearer ${ token } ` ;
}
return config ;
}) ;

export default apiClient ; CKGE_TMP_i src/services/inventoryService.ts CKGE_TMP_i</font>
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 ==== CKGE_TMP_i e2e/playwright/global-setup.ts CKGE_TMP_i</font>
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'</font> ) ;
await page . context () . storageState ({ path : 'e2e/auth.json' }) ;

await browser . close () ;
} ; CKGE_TMP_i e2e/playwright.config.ts CKGE_TMP_i</font>
import { defineConfig } from '@playwright/test' ;

export default defineConfig ({
globalSetup : './e2e/playwright/global-setup.ts' ,
use : {
storageState : 'e2e/auth.json' ,
} ,
}) ; ==== Hybrid E2E Test ==== CKGE_TMP_i e2e/inventory.hybrid.spec.ts CKGE_TMP_i</font>
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 ==== CKGE_TMP_i # .github/workflows/frontend-ci.yml CKGE_TMP_i
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 CKGE_TMP_i src/services/apiClient.ts (extended) CKGE_TMP_i</font>
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)</font> 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)</font> 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 CKGE_TMP_i src/tests/msw/handlers.ts CKGE_TMP_i</font>
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 </font> lighthouserc.json
{
“ci” : {
“collect” : {
“url” : [ http://localhost:5173 ] ,
“startServerCommand” : “npm run preview”
},
“assert” : {
“assertions” : {
“categories:performance” : [ “warn” , { “minScore” : 0.8 } ]
}
}
}
} ==== Playwright Performance Smoke ==== CKGE_TMP_i e2e/performance.spec.ts CKGE_TMP_i</font>
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 —-
react.1767093010.txt.gz · Last modified: by pradnya