Frontend Testing Implementation Stack

1. Jest Vitest Unit Testing Setup

Feature Jest Vitest Use Case
Configuration jest.config.js vitest.config.ts Test environment setup
Speed Standard Fast (Vite) Large test suites
Watch Mode --watch flag Built-in HMR Development workflow
Mocking jest.mock() vi.mock() Module mocking
Snapshot Testing toMatchSnapshot() toMatchSnapshot() UI regression
Coverage Built-in Istanbul c8 or Istanbul Code coverage reports

Example: Jest and Vitest comprehensive unit testing setup

// Jest Configuration (jest.config.js)
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'],
  moduleNameMapper: {
    "\\.(css|less|scss|sass)$": "identity-obj-proxy",
    "^@/(.*)$": "<rootDir>/src/$1",
  },
  collectCoverageFrom: [
    'src/**/*.{ts,tsx}',
    '!src/**/*.d.ts',
    '!src/main.tsx',
  ],
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

// Vitest Configuration (vitest.config.ts)
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: 'jsdom',
    setupFiles: './src/setupTests.ts',
    css: true,
    coverage: {
      provider: 'c8',
      reporter: ['text', 'json', 'html'],
      exclude: ['node_modules/', 'src/setupTests.ts'],
    },
  },
  resolve: {
    alias: {
      '@': '/src',
    },
  },
});

// Setup Tests (src/setupTests.ts)
import '@testing-library/jest-dom';
import { cleanup } from '@testing-library/react';
import { afterEach } from 'vitest';

afterEach(() => {
  cleanup();
});

// Basic Component Test
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { Button } from './Button';

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>);
    expect(screen.getByText('Click me')).toBeInTheDocument();
  });

  it('handles click events', async () => {
    const handleClick = vi.fn();
    render(<Button onClick={handleClick}>Click</Button>);
    
    const button = screen.getByRole('button');
    await userEvent.click(button);
    
    expect(handleClick).toHaveBeenCalledTimes(1);
  });

  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Disabled</Button>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});

// Async Test
import { waitFor } from '@testing-library/react';

describe('UserList', () => {
  it('fetches and displays users', async () => {
    render(<UserList />);
    
    expect(screen.getByText('Loading...')).toBeInTheDocument();
    
    await waitFor(() => {
      expect(screen.getByText('John Doe')).toBeInTheDocument();
    });
  });
});

// Mock Functions
import { vi } from 'vitest';

const mockFetch = vi.fn();
global.fetch = mockFetch;

mockFetch.mockResolvedValueOnce({
  json: async () => ({ name: 'John' }),
});

// Snapshot Testing
it('matches snapshot', () => {
  const { container } = render(<Card title="Test" />);
  expect(container.firstChild).toMatchSnapshot();
});

2. React Testing Library User Events

Method Purpose Async Use Case
userEvent.click() Simulate click Yes Buttons, links
userEvent.type() Simulate typing Yes Input fields
userEvent.clear() Clear input Yes Reset forms
userEvent.selectOptions() Select dropdown Yes Select elements
userEvent.upload() File upload Yes File inputs
userEvent.hover() Hover element Yes Tooltips, dropdowns

Example: React Testing Library comprehensive user interaction tests

// Install
npm install --save-dev @testing-library/react @testing-library/user-event @testing-library/jest-dom

// Form Testing
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

describe('LoginForm', () => {
  it('submits form with user input', async () => {
    const user = userEvent.setup();
    const handleSubmit = vi.fn();
    
    render(<LoginForm onSubmit={handleSubmit} />);
    
    const emailInput = screen.getByLabelText(/email/i);
    const passwordInput = screen.getByLabelText(/password/i);
    const submitButton = screen.getByRole('button', { name: /login/i });
    
    await user.type(emailInput, 'test@example.com');
    await user.type(passwordInput, 'password123');
    await user.click(submitButton);
    
    expect(handleSubmit).toHaveBeenCalledWith({
      email: 'test@example.com',
      password: 'password123',
    });
  });

  it('shows validation errors', async () => {
    const user = userEvent.setup();
    render(<LoginForm />);
    
    const submitButton = screen.getByRole('button', { name: /login/i });
    await user.click(submitButton);
    
    expect(screen.getByText(/email is required/i)).toBeInTheDocument();
    expect(screen.getByText(/password is required/i)).toBeInTheDocument();
  });
});

// Query Methods
// getBy* - Throws error if not found (use for elements that should exist)
const button = screen.getByRole('button', { name: /submit/i });

// queryBy* - Returns null if not found (use for elements that shouldn't exist)
const error = screen.queryByText(/error/i);
expect(error).not.toBeInTheDocument();

// findBy* - Async, waits for element (use for elements that appear after async operations)
const message = await screen.findByText(/success/i);

// Select Dropdown Testing
it('selects option from dropdown', async () => {
  const user = userEvent.setup();
  render(<CountrySelector />);
  
  const select = screen.getByRole('combobox');
  await user.selectOptions(select, 'usa');
  
  expect(screen.getByRole('option', { name: 'USA' }).selected).toBe(true);
});

// File Upload Testing
it('uploads file', async () => {
  const user = userEvent.setup();
  const file = new File(['hello'], 'hello.png', { type: 'image/png' });
  
  render(<FileUpload />);
  
  const input = screen.getByLabelText(/upload file/i);
  await user.upload(input, file);
  
  expect(input.files[0]).toBe(file);
  expect(input.files).toHaveLength(1);
});

// Checkbox and Radio Testing
it('toggles checkbox', async () => {
  const user = userEvent.setup();
  render(<TermsCheckbox />);
  
  const checkbox = screen.getByRole('checkbox');
  expect(checkbox).not.toBeChecked();
  
  await user.click(checkbox);
  expect(checkbox).toBeChecked();
  
  await user.click(checkbox);
  expect(checkbox).not.toBeChecked();
});

// Hover and Focus Testing
it('shows tooltip on hover', async () => {
  const user = userEvent.setup();
  render(<TooltipButton />);
  
  const button = screen.getByRole('button');
  await user.hover(button);
  
  expect(await screen.findByRole('tooltip')).toBeInTheDocument();
  
  await user.unhover(button);
  expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
});

// Keyboard Navigation
it('navigates with keyboard', async () => {
  const user = userEvent.setup();
  render(<TabNavigation />);
  
  const firstTab = screen.getByRole('tab', { name: /first/i });
  firstTab.focus();
  
  await user.keyboard('{ArrowRight}');
  expect(screen.getByRole('tab', { name: /second/i })).toHaveFocus();
  
  await user.keyboard('{Enter}');
  expect(screen.getByRole('tabpanel')).toHaveTextContent('Second content');
});

// Custom Queries
import { within } from '@testing-library/react';

it('finds nested elements', () => {
  render(<UserCard />);
  
  const card = screen.getByRole('article');
  const heading = within(card).getByRole('heading');
  
  expect(heading).toHaveTextContent('John Doe');
});

3. Cypress Playwright E2E Automation

Feature Cypress Playwright Best For
Browser Support Chrome, Firefox, Edge All + Safari WebKit Cross-browser testing
Execution In-browser Node.js Speed and reliability
Auto-waiting Built-in Built-in Flaky test reduction
Debugging Time-travel UI Inspector, trace viewer Test development
Parallelization Paid (Dashboard) Free (built-in) CI/CD speed
Mobile Testing Viewport only Device emulation Mobile workflows

Example: Cypress and Playwright E2E testing

// Cypress Installation and Setup
npm install --save-dev cypress

// cypress.config.ts
import { defineConfig } from 'cypress';

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000',
    viewportWidth: 1280,
    viewportHeight: 720,
    video: false,
    screenshotOnRunFailure: true,
  },
});

// Cypress Test (cypress/e2e/login.cy.ts)
describe('Login Flow', () => {
  beforeEach(() => {
    cy.visit('/login');
  });

  it('logs in successfully', () => {
    cy.get('[data-testid="email-input"]').type('test@example.com');
    cy.get('[data-testid="password-input"]').type('password123');
    cy.get('[data-testid="login-button"]').click();
    
    cy.url().should('include', '/dashboard');
    cy.contains('Welcome back').should('be.visible');
  });

  it('shows error for invalid credentials', () => {
    cy.get('[data-testid="email-input"]').type('wrong@example.com');
    cy.get('[data-testid="password-input"]').type('wrong');
    cy.get('[data-testid="login-button"]').click();
    
    cy.contains('Invalid credentials').should('be.visible');
  });
});

// Cypress Custom Commands (cypress/support/commands.ts)
Cypress.Commands.add('login', (email, password) => {
  cy.session([email, password], () => {
    cy.visit('/login');
    cy.get('[data-testid="email-input"]').type(email);
    cy.get('[data-testid="password-input"]').type(password);
    cy.get('[data-testid="login-button"]').click();
    cy.url().should('include', '/dashboard');
  });
});

// Use custom command
it('accesses protected page', () => {
  cy.login('test@example.com', 'password123');
  cy.visit('/profile');
  cy.contains('Profile').should('be.visible');
});

// Playwright Installation and Setup
npm install --save-dev @playwright/test

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
  },
});

// Playwright Test (tests/login.spec.ts)
import { test, expect } from '@playwright/test';

test.describe('Login Flow', () => {
  test('logs in successfully', async ({ page }) => {
    await page.goto('/login');
    
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="login-button"]');
    
    await expect(page).toHaveURL(/.*dashboard/);
    await expect(page.locator('text=Welcome back')).toBeVisible();
  });

  test('handles network errors', async ({ page }) => {
    await page.route('**/api/login', route => route.abort());
    
    await page.goto('/login');
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="login-button"]');
    
    await expect(page.locator('text=Network error')).toBeVisible();
  });
});

// Playwright Fixtures (Custom Setup)
import { test as base } from '@playwright/test';

type MyFixtures = {
  authenticatedPage: Page;
};

export const test = base.extend<MyFixtures>({
  authenticatedPage: async ({ page }, use) => {
    await page.goto('/login');
    await page.fill('[data-testid="email-input"]', 'test@example.com');
    await page.fill('[data-testid="password-input"]', 'password123');
    await page.click('[data-testid="login-button"]');
    await page.waitForURL('**/dashboard');
    await use(page);
  },
});

// Use fixture
test('accesses profile', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/profile');
  await expect(authenticatedPage.locator('h1')).toContainText('Profile');
});

4. Storybook Component Testing

Feature Purpose Benefit Use Case
Component Isolation Develop in isolation Focus on single component UI development
Stories Component states Document all variants Design system
Controls Interactive props Live editing Testing variations
Actions Event logging Track interactions Event handlers
Docs Auto-generated docs Component documentation Team collaboration
Addons Extend functionality A11y, testing, design Enhanced workflow

Example: Storybook setup and component stories

// Install Storybook
npx storybook@latest init

// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
  ],
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },
};

export default config;

// Button Component (src/components/Button.tsx)
import React from 'react';

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'sm' | 'md' | 'lg';
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
}

export const Button: React.FC<ButtonProps> = ({
  variant = 'primary',
  size = 'md',
  children,
  onClick,
  disabled,
}) => {
  return (
    <button
      className={`btn btn-${variant} btn-${size}`}
      onClick={onClick}
      disabled={disabled}
    >
      {children}
    </button>
  );
};

// Button Stories (src/components/Button.stories.tsx)
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';

const meta: Meta<typeof Button> = {
  title: 'Components/Button',
  component: Button,
  parameters: {
    layout: 'centered',
  },
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'danger'],
    },
    size: {
      control: 'select',
      options: ['sm', 'md', 'lg'],
    },
  },
  args: {
    onClick: fn(),
  },
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  args: {
    variant: 'primary',
    children: 'Primary Button',
  },
};

export const Secondary: Story = {
  args: {
    variant: 'secondary',
    children: 'Secondary Button',
  },
};

export const Danger: Story = {
  args: {
    variant: 'danger',
    children: 'Danger Button',
  },
};

export const Small: Story = {
  args: {
    size: 'sm',
    children: 'Small Button',
  },
};

export const Large: Story = {
  args: {
    size: 'lg',
    children: 'Large Button',
  },
};

export const Disabled: Story = {
  args: {
    disabled: true,
    children: 'Disabled Button',
  },
};

// Interaction Testing
import { within, userEvent } from '@storybook/test';

export const WithInteractions: Story = {
  args: {
    children: 'Click me',
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    const button = canvas.getByRole('button');
    
    await userEvent.click(button);
  },
};

// Complex Story with Decorators
export const WithContext: Story = {
  args: {
    children: 'Themed Button',
  },
  decorators: [
    (Story) => (
      <div style={{ padding: '3rem', background: '#f0f0f0' }}>
        <Story />
      </div>
    ),
  ],
};

// Form Story (src/components/LoginForm.stories.tsx)
import { LoginForm } from './LoginForm';

const meta: Meta<typeof LoginForm> = {
  title: 'Forms/LoginForm',
  component: LoginForm,
};

export default meta;

export const Default: Story = {};

export const WithError: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    await userEvent.click(canvas.getByRole('button', { name: /login/i }));
    
    // Verify error messages appear
    await canvas.findByText(/email is required/i);
  },
};

export const SuccessfulLogin: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    
    await userEvent.type(
      canvas.getByLabelText(/email/i),
      'test@example.com'
    );
    await userEvent.type(
      canvas.getByLabelText(/password/i),
      'password123'
    );
    await userEvent.click(canvas.getByRole('button', { name: /login/i }));
  },
};

5. Istanbul Coverage.js Test Coverage

Metric Definition Threshold Importance
Line Coverage % of lines executed 80-90% Basic coverage metric
Branch Coverage % of if/else branches 75-85% Logic coverage
Function Coverage % of functions called 80-90% API coverage
Statement Coverage % of statements executed 80-90% Code execution
Uncovered Lines Lines not executed Minimize Gap identification
Coverage Reports HTML/JSON/LCOV CI integration Tracking trends

Example: Istanbul coverage configuration and reporting

// Jest with Coverage (package.json)
{
  "scripts": {
    "test": "jest",
    "test:coverage": "jest --coverage",
    "test:coverage:watch": "jest --coverage --watchAll"
  },
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.{js,jsx,ts,tsx}",
      "!src/**/*.d.ts",
      "!src/**/*.stories.tsx",
      "!src/main.tsx",
      "!src/vite-env.d.ts"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      },
      "src/components/": {
        "branches": 90,
        "functions": 90,
        "lines": 90,
        "statements": 90
      }
    },
    "coverageReporters": [
      "text",
      "text-summary",
      "html",
      "lcov",
      "json"
    ],
    "coveragePathIgnorePatterns": [
      "/node_modules/",
      "/dist/",
      "/coverage/"
    ]
  }
}

// Vitest with c8 Coverage (vitest.config.ts)
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    coverage: {
      provider: 'c8',
      reporter: ['text', 'json', 'html', 'lcov'],
      exclude: [
        'node_modules/',
        'src/setupTests.ts',
        '**/*.d.ts',
        '**/*.config.ts',
        '**/mockData',
        '**/*.stories.tsx',
      ],
      all: true,
      lines: 80,
      functions: 80,
      branches: 80,
      statements: 80,
      perFile: true,
    },
  },
});

// GitHub Actions CI with Coverage (..github/workflows/test.yml)
name: Test Coverage

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run tests with coverage
        run: npm run test:coverage
        
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
          flags: unittests
          name: codecov-umbrella
          
      - name: Comment PR with coverage
        uses: romeovs/lcov-reporter-action@v0.3.1
        with:
          lcov-file: ./coverage/lcov.info
          github-token: ${{ secrets.GITHUB_TOKEN }}

// Custom Coverage Script (scripts/coverage-check.js)
const fs = require('fs');
const path = require('path');

const coverageSummary = JSON.parse(
  fs.readFileSync(path.join(__dirname, '../coverage/coverage-summary.json'), 'utf8')
);

const thresholds = {
  lines: 80,
  statements: 80,
  functions: 80,
  branches: 80,
};

let failed = false;

Object.entries(thresholds).forEach(([metric, threshold]) => {
  const coverage = coverageSummary.total[metric].pct;
  console.log(`${metric}: ${coverage}% (threshold: ${threshold}%)`);
  
  if (coverage < threshold) {
    console.error(`❌ ${metric} coverage ${coverage}% is below threshold ${threshold}%`);
    failed = true;
  } else {
    console.log(`✅ ${metric} coverage passed`);
  }
});

if (failed) {
  process.exit(1);
}

// Exclude files from coverage
/* istanbul ignore next */
function debugOnlyFunction() {
  console.log('Debug info');
}

// Ignore specific lines
/* istanbul ignore next */
if (process.env.NODE_ENV === 'development') {
  console.log('Dev mode');
}

// Ignore branches
/* istanbul ignore else */
if (condition) {
  doSomething();
}

// Package.json scripts for coverage gates
{
  "scripts": {
    "test:coverage": "vitest run --coverage",
    "test:coverage:check": "npm run test:coverage && node scripts/coverage-check.js",
    "precommit": "npm run test:coverage:check"
  }
}

6. Visual Regression Testing Chromatic

Tool Method Platform Use Case
Chromatic Cloud-based snapshots Storybook integration UI review workflow
Percy Visual diffs CI/CD integration Pull request reviews
BackstopJS Screenshot comparison Headless browser Local testing
Playwright Screenshots Built-in snapshots Test suite E2E visual testing
Jest Image Snapshot Image comparison Unit tests Component snapshots
Applitools AI-powered diffs Cross-browser Enterprise testing

Example: Visual regression testing setup

// Chromatic Setup
npm install --save-dev chromatic

// package.json
{
  "scripts": {
    "chromatic": "chromatic --project-token=<your-project-token>"
  }
}

// .github/workflows/chromatic.yml
name: Chromatic

on: push

jobs:
  visual-regression:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
          
      - name: Install dependencies
        run: npm ci
        
      - name: Publish to Chromatic
        uses: chromaui/action@v1
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
          autoAcceptChanges: main

// Playwright Visual Regression
import { test, expect } from '@playwright/test';

test('homepage visual regression', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png');
});

test('button states visual regression', async ({ page }) => {
  await page.goto('/components/button');
  
  const button = page.locator('[data-testid="primary-button"]');
  await expect(button).toHaveScreenshot('button-default.png');
  
  await button.hover();
  await expect(button).toHaveScreenshot('button-hover.png');
  
  await button.focus();
  await expect(button).toHaveScreenshot('button-focus.png');
});

// Playwright Config for Visual Tests
import { defineConfig } from '@playwright/test';

export default defineConfig({
  expect: {
    toHaveScreenshot: {
      maxDiffPixels: 100,
      threshold: 0.2,
    },
  },
  use: {
    screenshot: 'only-on-failure',
  },
});

// Update screenshots command
// npx playwright test --update-snapshots

// BackstopJS Configuration (backstop.json)
{
  "id": "frontend_visual_test",
  "viewports": [
    {
      "label": "desktop",
      "width": 1280,
      "height": 720
    },
    {
      "label": "tablet",
      "width": 768,
      "height": 1024
    },
    {
      "label": "mobile",
      "width": 375,
      "height": 667
    }
  ],
  "scenarios": [
    {
      "label": "Homepage",
      "url": "http://localhost:3000",
      "referenceUrl": "",
      "readyEvent": "",
      "readySelector": "",
      "delay": 500,
      "hideSelectors": [],
      "removeSelectors": [],
      "hoverSelector": "",
      "clickSelector": "",
      "postInteractionWait": 0,
      "selectors": ["document"],
      "selectorExpansion": true,
      "misMatchThreshold": 0.1
    },
    {
      "label": "Button Hover",
      "url": "http://localhost:3000/components",
      "hoverSelector": ".btn-primary",
      "delay": 200
    }
  ],
  "paths": {
    "bitmaps_reference": "backstop_data/bitmaps_reference",
    "bitmaps_test": "backstop_data/bitmaps_test",
    "engine_scripts": "backstop_data/engine_scripts",
    "html_report": "backstop_data/html_report",
    "ci_report": "backstop_data/ci_report"
  },
  "report": ["browser", "CI"],
  "engine": "puppeteer"
}

// BackstopJS Commands
// Reference (baseline): npx backstop reference
// Test: npx backstop test
// Approve changes: npx backstop approve

// Jest Image Snapshot
npm install --save-dev jest-image-snapshot

// setupTests.ts
import { toMatchImageSnapshot } from 'jest-image-snapshot';

expect.extend({ toMatchImageSnapshot });

// Component visual test
import { render } from '@testing-library/react';
import { Button } from './Button';

it('matches visual snapshot', () => {
  const { container } = render(<Button>Click me</Button>);
  
  expect(container.firstChild).toMatchImageSnapshot({
    customSnapshotsDir: '__image_snapshots__',
    customDiffDir: '__image_snapshots__/__diff_output__',
    failureThreshold: 0.01,
    failureThresholdType: 'percent',
  });
});

// Percy Configuration (.percy.yml)
version: 2
static:
  files: '**/*.html'
  ignore-files: '**/node_modules/**'
snapshot:
  widths:
    - 375
    - 768
    - 1280
  min-height: 1024
  percy-css: |
    .animation { animation: none !important; }
    .transition { transition: none !important; }

// Percy with Storybook
npx percy storybook http://localhost:6006

Frontend Testing Best Practices Summary

  • Unit Testing - Jest/Vitest for fast unit tests, mock dependencies, 80%+ coverage, test pure functions and business logic
  • Component Testing - React Testing Library with userEvent for realistic user interactions, avoid implementation details, query by role/label
  • E2E Testing - Cypress for quick setup with time-travel debugging, Playwright for multi-browser with built-in parallelization
  • Storybook - Develop components in isolation, document all states, interaction testing, visual testing with Chromatic
  • Coverage Metrics - Track line, branch, function, statement coverage; set thresholds per directory; integrate with CI/CD
  • Visual Regression - Chromatic for Storybook, Playwright screenshots, Percy for PR reviews, catch unintended UI changes
  • Testing Strategy - Testing pyramid: many unit tests, some integration tests, few E2E tests; fast feedback, reliable CI