Comprehensive rule set for writing fast, reliable, and maintainable front-end tests using JavaScript/TypeScript. Covers unit, integration, end-to-end, accessibility, performance, and CI/CD integration.
You know the drill. Your test suite takes 15 minutes to run, fails randomly on CI, and breaks every time someone refactors a component. You're spending more time maintaining tests than writing features, and your team has lost faith in the green checkmarks.
It doesn't have to be this way.
Most teams approach front-end testing backwards. They write tests that mirror implementation details instead of user behavior. They rely on flaky selectors, ignore accessibility, and treat test code like throwaway scripts. The result? Test suites that provide false confidence and slow down development.
Here's what's actually happening:
These Cursor Rules implement a battle-tested approach to front-end testing that solves these core problems. Instead of testing implementation details, you'll test user behavior. Instead of brittle selectors, you'll use semantic queries. Instead of flaky tests, you'll build deterministic suites that run fast and fail meaningfully.
What you get:
Unit tests complete in under 4 seconds, integration tests in under 15 seconds. No more context switching while waiting for test results.
describe('UserProfile', () => {
it('shows loading state while fetching data', async () => {
// Arrange
const user = buildUser({ name: 'John Doe' })
server.use(rest.get('/api/user', (req, res, ctx) =>
res(ctx.delay(100), ctx.json(user))
))
// Act
render(<UserProfile userId="123" />)
// Assert
expect(screen.getByRole('progressbar')).toBeInTheDocument()
await waitFor(() =>
expect(screen.getByText('John Doe')).toBeInTheDocument()
)
})
})
Focus on behavior over implementation. Tests use semantic queries that mirror how users interact with your app.
// ❌ Brittle - breaks when CSS changes
expect(container.querySelector('.btn-primary')).toBeInTheDocument()
// ✅ Resilient - tests user behavior
expect(screen.getByRole('button', { name: /save changes/i })).toBeEnabled()
Every component test automatically validates WCAG 2.1 AA compliance, preventing accessibility regressions from reaching production.
it('passes accessibility audit', async () => {
const { container } = render(<ColorPicker />)
expect(await axe(container)).toHaveNoViolations()
})
Tests are designed to be CI-ready from day one. No hidden dependencies, no order coupling, deterministic results every time.
Before: You write a test that checks if a specific CSS class exists. Later, a designer updates the styling, your test breaks, and you spend 20 minutes debugging a perfectly functional component.
After: Your test focuses on user interactions and semantic meaning:
describe('SearchInput', () => {
it('filters results as user types', async () => {
const user = userEvent.setup()
const mockOnFilter = jest.fn()
render(<SearchInput onFilter={mockOnFilter} />)
const searchBox = screen.getByRole('searchbox', { name: /search products/i })
await user.type(searchBox, 'laptop')
expect(mockOnFilter).toHaveBeenCalledWith('laptop')
})
})
The styling can change completely, but as long as the search functionality works, your test passes.
Before: You mock every API call manually, leading to tests that don't catch real integration issues and break when API contracts change.
After: Use MSW to create realistic API scenarios:
// Setup realistic server responses
const server = setupServer(
rest.get('/api/products', (req, res, ctx) => {
const query = req.url.searchParams.get('q')
const filtered = products.filter(p => p.name.includes(query))
return res(ctx.json(filtered))
})
)
it('displays filtered products from API', async () => {
render(<ProductSearch />)
const user = userEvent.setup()
await user.type(screen.getByRole('searchbox'), 'macbook')
await waitFor(() => {
expect(screen.getByText('MacBook Pro')).toBeInTheDocument()
expect(screen.queryByText('iPad')).not.toBeInTheDocument()
})
})
Before: Flaky Cypress tests that fail randomly, use brittle selectors, and take 10 minutes to debug when they break.
After: Reliable Playwright tests with proper isolation and semantic selectors:
test('user can complete checkout flow', async ({ page }) => {
await page.goto('/products')
// Use semantic selectors
await page.getByRole('button', { name: /add to cart/i }).first().click()
await page.getByRole('link', { name: /cart/i }).click()
await page.getByRole('button', { name: /checkout/i }).click()
// Isolated state - no dependency on previous tests
await page.fill('[data-testid="email"]', '[email protected]')
await page.fill('[data-testid="card-number"]', '4242424242424242')
await page.getByRole('button', { name: /complete order/i }).click()
await expect(page.getByText(/order confirmed/i)).toBeVisible()
})
npm install --save-dev jest @testing-library/react @testing-library/user-event @testing-library/jest-dom msw jest-axe playwright
Create your test configuration:
// tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"esModuleInterop": false
},
"include": ["src/**/*", "tests/**/*"]
}
src/
components/
Button/
Button.tsx
Button.test.tsx # Co-located unit tests
hooks/
useAuth/
useAuth.ts
useAuth.test.ts
tests/
integration/ # Cross-component tests
e2e/ # End-to-end scenarios
support/
fixtures/
accessibility/ # Dedicated a11y tests
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
bail: 1, // Fail fast in CI
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts'
],
coverageThreshold: {
global: {
branches: 90,
functions: 90,
lines: 90,
statements: 90
}
}
}
Start with the factory pattern for test data:
// tests/factories/user.ts
export const buildUser = (overrides?: Partial<User>): User => ({
id: nanoid(),
name: faker.person.firstName(),
email: faker.internet.email(),
role: 'user',
...overrides,
})
Create custom render utilities:
// tests/utils/render.tsx
const AllProviders = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>
<Router>
{children}
</Router>
</QueryClientProvider>
)
export const renderWithProviders = (ui: React.ReactElement) =>
rtlRender(ui, { wrapper: AllProviders })
# .github/workflows/test.yml
name: Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run unit tests
run: pnpm test:unit --coverage
- name: Run integration tests
run: pnpm test:integration
- name: Run e2e tests
run: pnpm test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
Teams using these patterns typically see:
Your front-end testing doesn't have to be a bottleneck. With the right patterns and tooling, it becomes your safety net for shipping high-quality, accessible applications with confidence.
Ready to transform your testing workflow? Copy these rules into your Cursor editor and start building tests that actually work.
You are an expert in JavaScript / TypeScript testing, React, Jest, React Testing Library, Cypress, Playwright, Vitest, Vite, Storybook, TailwindCSS.
Key Principles
- Apply the F.I.R.S.T principles: Fast, Isolated, Repeatable, Self-validating, Thorough.
- Follow the Testing Pyramid: >70 % unit, 20 % integration, <10 % end-to-end.
- Test behaviour, not implementation; focus on what the user sees and does.
- Keep tests deterministic: freeze time, mock network, random, and OS-specific APIs.
- Each test must be CI-ready: no hidden dependencies, no order coupling, <5 s runtime.
- Prefer TypeScript in tests; treat test code as production code.
- Automate everything in CI/CD; block merges on red pipelines.
- Treat accessibility as a first-class citizen; fail builds on a11y regressions.
- Use risk-based testing: cover critical paths first, edge cases second.
- Delete or fix flaky tests within 24 h; never quarantine long-term.
JavaScript / TypeScript Rules
- File naming: `*.test.ts`, `*.spec.tsx` for unit/integration; `*.e2e.ts` for Cypress/Playwright.
- Co-locate unit tests beside source files; keep e2e in `/e2e` folder.
- Use top-level `describe` mirroring file path; use human-readable test names:
```ts
describe('components/Button', () => {
it('disables while loading', async () => { /* … */ })
})
```
- Strict TS config: `noImplicitAny`, `strictNullChecks`, `esModuleInterop: false` inside `tsconfig.test.json`.
- Prefer `const` over `let`; no var.
- Avoid global `beforeAll` that mutates shared state; use per-test fixtures.
- Use factory helpers:
```ts
const buildUser = (overrides?: Partial<User>): User => ({
id: nanoid(),
name: faker.person.firstName(),
role: 'user',
...overrides,
})
```
- Mock network with `msw` in unit/integration tests; never stub `fetch` manually.
- For jest mocks use `jest.mock()` only at module scope; reset with `afterEach(jest.resetAllMocks)`.
- Keep assertion count explicit when multiple async paths: `expect.assertions(2)`.
- Fail fast: configure Jest `bail=1`, Vitest `--bail` in CI.
Error Handling and Validation
- Assertions must be self-explanatory; include context in message when using `expect.extend`.
- Test error branches explicitly:
```ts
await expect(api.getUser('invalid')).rejects.toMatchObject({ status: 404 })
```
- Use pattern `AAA` (Arrange-Act-Assert) with visual separators or comments.
- Timeouts: <4 s unit, <15 s integration, <60 s e2e. Increase only per-test via `jest.setTimeout`.
- Flakiness mitigation: retry flaky network only in e2e (`cy.retry` or Playwright `retries: 2`). Never in unit.
- Cypress: capture screenshots and videos on failure; attach artifacts to CI.
Framework-Specific Rules
React Testing Library (RTL)
- Use `screen` queries; prefer `getByRole` → `getByLabelText` → `getByText` hierarchy.
- Simulate user with `@testing-library/user-event` not `fireEvent`.
- Never query by `id`, `class`, or test-ids unless no semantic alternative.
- Avoid `wrapper` patterns; test component in runtime context via custom render:
```ts
const renderWithProviders = (ui: React.ReactElement) =>
rtlRender(ui, { wrapper: ({ children }) => <QueryClientProvider client={client}>{children}</QueryClientProvider> })
```
Jest / Vitest
- Run in watch mode locally (`--watch`); coverage in CI (`--coverage`).
- Snapshot tests only for static, stable output; limit to <5 % of suite. Review snapshots in PR.
- Prefer `.toMatchInlineSnapshot()` for small values; commit updated snapshots explicitly.
Cypress
- Use `data-cy` attributes for selectors; never rely on DOM structure.
- Isolate state per test with `cy.session()` and DB reset API.
- Group heavy login flow in `beforeEach` custom command: `Cypress.Commands.add('login', ...)`.
- Use `cy.intercept()` to stub external 3rd-party calls.
- Run in parallel using Cypress Cloud or `--parallel --record` with named machines.
Playwright
- Configure `test.describe.parallel` for independent suites.
- Use built-in fixtures `page`, `context`; avoid global `browser` mutations.
- Take selective trace: `trace: 'retain-on-failure'`.
- Use `*.pw.ts` naming if mixed with Cypress.
Additional Sections
Accessibility Testing
- Inject `axe-core` in RTL tests:
```ts
import { axe, toHaveNoViolations } from 'jest-axe'
expect.extend({ toHaveNoViolations })
const { container } = render(<ColorPicker />)
expect(await axe(container)).toHaveNoViolations()
```
- In e2e, run `cy.injectAxe(); cy.checkA11y();` or Playwright-axe plugin.
- Fail CI when WCAG 2.1 AA violations >0.
Performance & Web Vitals
- Trigger Lighthouse CI in PR; budget: FCP <1.8 s, TTI <3 s, CLS <0.1.
- Use `@web/test-runner` with `--coverage` for perf regression alerts.
Visual Regression
- Integrate Percy or Playwright `expect(page).toHaveScreenshot()`; capture only critical flows.
- Keep baseline images in `/__screenshots__`; review diffs on PR.
Continuous Integration
- Matrix: { os: [ubuntu-latest], node: [lts/*, latest] }.
- Steps: install → lint → type-check → unit tests → build → integration → e2e (parallel) → Lighthouse → deploy.
- Cache `~/.pnpm-store` or `~/.npm` and Playwright browsers to cut build time.
- Upload code coverage to Codecov; enforce 90 % global, 100 % for critical utils.
Security & SAST
- Run `npm audit --production` and `gh secret-scan` in pipeline.
- Include SAST tool (e.g., SonarJS) as blocking step when severity ≥ High.
Maintenance
- Use Renovate to auto-update dependencies; auto-create PRs with updated snapshots and failing tests.
- Remove skipped tests older than 30 days via eslint-plugin-jest rule `no-disabled-tests`.
- Tag flaky tests with `@flaky`; nightly job runs `--retries 5` to track stability.
Directory Structure
```
src/
components/
hooks/
utils/
styles/
tests/
unit/ # mirrors src tree
integration/
e2e/
support/
fixtures/
accessibility/
visual/
```
Reference Versions
- Node ≥ 18, pnpm ≥ 8
- Jest 29, React Testing Library 14
- Cypress 13 or Playwright 1.40
- Vitest 1.x, Vite 5
Follow this rule set to achieve fast feedback loops, high confidence releases, and accessible, high-quality front-end applications.