A Frontend Testing Strategy for High-Traffic Checkout
A Frontend Testing Strategy for High-Traffic CheckoutNovember 14, 2025

A Frontend Testing Strategy for High-Traffic Checkout

How we used Jest, Vitest, and Playwright on a medical e-commerce platform: what to unit test, what to leave to E2E, and how we reached 85%+ coverage without slowing the team down.

Table of Contents

  1. The Problem With "Just Add Tests"
  2. Layer the Pyramid on Purpose
  3. Unit Tests With Jest and Vitest
  4. Playwright for Money Paths
  5. Coverage That Means Something
  6. CI Without a 40-Minute Wait

The Problem With "Just Add Tests" {#problem}

A storefront can have 80% coverage and still break checkout. Coverage counts lines. Users count whether they can log in, search, add to cart, and pay.

We needed tests that:

  • Locked business rules (pricing, eligibility, consent)
  • Locked user flows (login, search, checkout)
  • Failed in CI before production
  • Did not require a full staging deploy for every PR

The result: 85%+ coverage on critical modules and about 40% fewer production frontend issues after the suite matured.

Layer the Pyramid on Purpose {#pyramid}

LayerToolWhat it owns
UnitJest / VitestPure logic, hooks, price calculators
ComponentTesting LibraryForms, errors, accessible names
E2EPlaywrightLogin, search, checkout, account

If a bug needs a browser, it is not a unit test. If a function has no UI, it is not an E2E test.

Unit Tests With Jest and Vitest {#unit}

We kept Jest for legacy packages and moved new code to Vitest for speed. The API is close enough that tests port cleanly.

import { describe, expect, it } from 'vitest';
import { getCheckoutTotal } from './pricing';

describe('getCheckoutTotal', () => {
  it('applies a single promo once', () => {
    const total = getCheckoutTotal({
      items: [{ sku: 'A', price: 100, qty: 2 }],
      promo: { type: 'percent', value: 10 },
      taxRate: 0.08,
    });

    expect(total).toBe(194.4);
  });

  it('rejects stacking exclusive promos', () => {
    expect(() =>
      getCheckoutTotal({
        items: [{ sku: 'A', price: 100, qty: 1 }],
        promo: { type: 'stack', codes: ['SAVE10', 'SAVE20'] },
        taxRate: 0,
      })
    ).toThrow(/exclusive/);
  });
});

Test the rules product will argue about in a war room. Skip wrapping className strings.

Component tests

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

test('shows a field error when email is invalid', async () => {
  const user = userEvent.setup();
  render(<LoginForm onSubmit={jest.fn()} />);

  await user.type(screen.getByLabelText('Email'), 'not-an-email');
  await user.click(screen.getByRole('button', { name: 'Sign in' }));

  expect(screen.getByText(/valid email/i)).toBeInTheDocument();
});

Query by role and label. If the test cannot find the control, neither can a screen reader.

Playwright for Money Paths {#e2e}

We automated four flows and treated them as release blockers:

  1. Login / session restore
  2. Search → product → add to cart
  3. Checkout (including address and payment stub)
  4. Order confirmation
import { expect, test } from '@playwright/test';

test('guest can complete checkout with a test card', async ({ page }) => {
  await page.goto('/products/sample-kit');
  await page.getByRole('button', { name: 'Add to cart' }).click();
  await page.getByRole('link', { name: 'Cart' }).click();
  await page.getByRole('button', { name: 'Checkout' }).click();

  await page.getByLabel('Email').fill('qa@example.com');
  await page.getByLabel('Card number').fill('4242424242424242');
  await page.getByRole('button', { name: 'Place order' }).click();

  await expect(page.getByRole('heading', { name: /order confirmed/i })).toBeVisible();
});

Use test cards and a sandbox payment provider. Never hit production processors from CI.

Stability tactics

  • getByRole over CSS selectors
  • Network fixtures for flaky third parties
  • One storageState for authenticated suites
  • Retry only on known infrastructure flakes, not on assertion failures

Coverage That Means Something {#coverage}

We set 85% as a floor for checkout, cart, and auth, not for every marketing component. Global 85% encourages people to test getters.

{
  "coverage": {
    "include": ["src/features/checkout/**", "src/features/auth/**"],
    "thresholds": {
      "lines": 85,
      "functions": 85,
      "branches": 80
    }
  }
}

Uncovered branches in promo stacking got a test. Uncovered SVG icons did not.

CI Without a 40-Minute Wait {#ci}

  • Unit tests on every PR
  • Playwright smoke (login + checkout) on every PR against a preview
  • Full Playwright suite on main and nightly
  • Trace on first retry, video only on failure

Tests that the team trusts get run. Tests that flake get quarantined or deleted. A red build that people ignore is worse than a missing test.

The strategy is simple: unit-test money math, component-test forms, and E2E the paths that lose revenue when they break.