Use Cases (Updated: 6/3/2026)

E2E Testing with Claude Code: Playwright/Cypress and CI Guide

Use Claude Code to design Playwright E2E tests with CI, pitfalls, runnable examples, and CTA checks.

E2E Testing with Claude Code: Playwright/Cypress and CI Guide

Decide what E2E must protect before asking Claude Code

An E2E test is not valuable just because Claude Code generated it and the terminal turned green. End-to-end testing checks a user-facing flow in a real browser, so it is slower and more expensive to debug than unit tests. If you turn every UI detail into E2E, CI becomes noisy and people stop trusting failures.

Use Claude Code for the flows that hurt when they break: login, checkout, lead forms, admin actions, and article CTAs that move readers toward a product or consultation. Masa’s practical lesson from running ClaudeCodeLab is that a page can render correctly while the monetization link at the end of the article is broken. Good E2E testing protects the business path, not only the screen.

This guide uses current Playwright guidance from the official installation docs, locators, CI guide, and Trace Viewer. For adjacent coverage, read API test automation and the broader testing strategies guide.

flowchart LR
  A["Prompt Claude Code"] --> B["Choose three critical flows"]
  B --> C["Implement Playwright tests"]
  C --> D["Debug with trace and HTML report"]
  D --> E["Gate CI"]
  C --> F["Verify CTAs and revenue paths"]

A better prompt for Claude Code

Start with scope, success criteria, failure criteria, allowed files, and verification commands. Without that, Claude Code often writes brittle tests against CSS classes. Playwright recommends locators such as getByRole, getByLabel, getByText, and getByTestId; locators are the element-finding layer that works with Playwright’s auto-waiting and retryable assertions.

Target: E2E tests for login, checkout, and newsletter signup.
Tool: Playwright Test. Add a short Cypress comparison at the end.
Rules:
- Prefer role, label, text, or testid locators over CSS classes.
- Cover failure paths, not only the happy path.
- Use workers: 1 in CI and trace: on-first-retry.
- Verify that the monetization CTA href is still present.
Allowed files:
- tests/e2e/**/*.spec.ts
- playwright.config.ts
Verification:
- npx playwright test
- npx playwright test --headed for local debugging

Also say what is out of scope. Real payments, real email delivery, ad clicks, and CRM writes should usually be test-mode, mocked, or covered by API/contract tests. For those boundaries, pair this with webhook implementation and analytics implementation.

Setup commands

For a new project, these are the practical commands. The official browser docs note that Playwright versions map to specific browser binaries, so reinstall browsers after upgrading Playwright. On Linux CI, install browser dependencies with --with-deps.

npm init playwright@latest
npx playwright install
npx playwright test
npx playwright test --headed
npx playwright test --ui
npx playwright show-report

--headed opens a visible browser for local debugging. --ui opens Playwright’s UI mode. CI should normally run headless. If a test passes locally but fails in CI, suspect animation timing, viewport size, fonts, timezone, CPU speed, or an external API wait.

Runnable Playwright example

The next spec is self-contained. It builds a tiny page with page.setContent, so you can copy it into tests/e2e/claude-code-e2e.spec.ts and run it after installing Playwright. In a real app, replace renderDemoApp(page) with page.goto('/login') and adjust labels or data-testid values.

// tests/e2e/claude-code-e2e.spec.ts
import { test, expect, type Page } from '@playwright/test';

async function renderDemoApp(page: Page) {
  await page.setContent(`
    <!doctype html>
    <main>
      <form id="login-form" aria-label="Login form">
        <label>Email <input id="email" aria-label="Email" /></label>
        <label>Password <input id="password" aria-label="Password" type="password" /></label>
        <button type="submit">Log in</button>
        <p id="login-error" role="alert" hidden></p>
      </form>

      <section id="dashboard" hidden>
        <h1>Dashboard</h1>
        <a data-testid="training-cta" href="/training/">Book Claude Code training</a>
        <button id="add-plan">Add Pro plan to cart</button>
        <span data-testid="cart-count">0</span>
        <button id="buy">Complete purchase</button>
        <p data-testid="order-status" role="status"></p>

        <label>Newsletter email <input id="newsletter-email" aria-label="Newsletter email" /></label>
        <button id="newsletter-button" type="button">Join newsletter</button>
        <p id="newsletter-error" role="alert" hidden></p>
        <p data-testid="newsletter-status" role="status"></p>
      </section>
    </main>

    <script>
      const state = { cart: 0 };
      document.querySelector('#login-form').addEventListener('submit', (event) => {
        event.preventDefault();
        const email = document.querySelector('#email').value;
        const password = document.querySelector('#password').value;
        const error = document.querySelector('#login-error');

        if (email === 'masa@example.com' && password === 'password123') {
          document.querySelector('#login-form').hidden = true;
          document.querySelector('#dashboard').hidden = false;
          error.hidden = true;
          return;
        }

        error.textContent = 'Authentication failed';
        error.hidden = false;
      });

      document.querySelector('#add-plan').addEventListener('click', () => {
        state.cart += 1;
        document.querySelector('[data-testid="cart-count"]').textContent = String(state.cart);
      });

      document.querySelector('#buy').addEventListener('click', () => {
        const status = document.querySelector('[data-testid="order-status"]');
        status.textContent = state.cart === 0 ? 'Cart is empty' : 'Order ORD-1001 completed';
      });

      document.querySelector('#newsletter-button').addEventListener('click', () => {
        const email = document.querySelector('#newsletter-email').value;
        const error = document.querySelector('#newsletter-error');
        const status = document.querySelector('[data-testid="newsletter-status"]');

        if (!email.includes('@')) {
          error.textContent = 'Enter a valid email';
          error.hidden = false;
          status.textContent = '';
          return;
        }

        error.hidden = true;
        status.textContent = 'Thanks, we will send the checklist.';
      });
    </script>
  `);
}

async function loginAsDemoUser(page: Page) {
  await page.getByLabel('Email').fill('masa@example.com');
  await page.getByLabel('Password').fill('password123');
  await page.getByRole('button', { name: 'Log in' }).click();
}

test.describe('Claude Code E2E starter', () => {
  test('use case 1: login shows dashboard and keeps the training CTA', async ({ page }) => {
    await renderDemoApp(page);
    await loginAsDemoUser(page);

    await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
    await expect(page.getByTestId('training-cta')).toHaveAttribute('href', '/training/');
  });

  test('use case 2: checkout flow creates an order number', async ({ page }) => {
    await renderDemoApp(page);
    await loginAsDemoUser(page);

    await page.getByRole('button', { name: 'Add Pro plan to cart' }).click();
    await expect(page.getByTestId('cart-count')).toHaveText('1');
    await page.getByRole('button', { name: 'Complete purchase' }).click();
    await expect(page.getByTestId('order-status')).toContainText('ORD-1001');
  });

  test('use case 3: newsletter validation blocks invalid leads', async ({ page }) => {
    await renderDemoApp(page);
    await loginAsDemoUser(page);

    await page.getByRole('button', { name: 'Join newsletter' }).click();
    await expect(page.getByRole('alert')).toContainText('Enter a valid email');

    await page.getByLabel('Newsletter email').fill('reader@example.com');
    await page.getByRole('button', { name: 'Join newsletter' }).click();
    await expect(page.getByTestId('newsletter-status')).toContainText('Thanks');
  });
});

This single spec covers three concrete use cases: login, checkout, and lead capture. It also checks the training CTA href, which is the detail thin E2E suites often miss.

Practical Playwright config

Use the config to keep evidence when CI fails. Playwright’s CI docs recommend workers: 1 for stability and reproducibility unless you have enough infrastructure to parallelize safely.

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

export default defineConfig({
  testDir: './tests/e2e',
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  use: {
    baseURL: process.env.BASE_URL ?? 'http://127.0.0.1:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
});

trace: 'on-first-retry' gives Claude Code useful evidence: what was visible, which click failed, and which assertion timed out. That is much better than pasting a vague CI error.

GitHub Actions CI

The official Playwright CI examples install dependencies, install browsers with system dependencies, run tests, and upload the HTML report. Start simple before adding browser caching or sharding.

# .github/workflows/playwright.yml
name: Playwright Tests

on:
  push:
    branches: [main, master]
  pull_request:
    branches: [main, master]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install --with-deps
      - name: Run E2E tests
        run: npx playwright test
      - uses: actions/upload-artifact@v5
        if: ${{ !cancelled() }}
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Tell Claude Code whether CI should test a local dev server, a deployed preview URL, or a staging environment. For Vercel or Cloudflare Pages previews, pass BASE_URL into the workflow. For local dev, add Playwright’s webServer config.

Use cases worth testing

Use caseWhy it belongs in E2EKey expectations
Login to dashboardAuth, redirects, and permissions meet in one flowHeading, user state, logout, error alert
Checkout or consultation formDirect revenue or lead impactOrder number, confirmation, no double submit
Article to free PDF or training pageProtects non-AdSense monetizationCTA href, tracking event, mobile visibility
Admin destructive actionPrevents accidental deletion or privilege leaksConfirmation dialog, audit log, role checks
Localized pagesTranslation often breaks linksLocale URL, translated copy, external docs

Give this table to Claude Code before asking for test files. Otherwise it may click buttons without proving anything meaningful.

Failure cases to avoid

Avoid waitForTimeout(3000). Fixed sleeps pass on one machine and fail on CI. Wait for the state that matters with retryable expectations.

Avoid CSS selectors like .btn-primary:nth-child(2). Prefer role, label, text, and stable data-testid values. Use CSS only for low-risk implementation details.

Avoid shared state between tests. A cart, cookie, or database record created by one test should not be required by the next. Parallel execution and retries make shared state painful.

Do not ignore headed versus headless differences. Fonts, GPU behavior, scroll, clipboard, file uploads, and viewport size can differ in CI. Use headless for CI, then --headed or --ui for diagnosis.

Do not hit real payment, email, ad, or CRM services from every E2E run. Use test mode, mocks, or separate API contract tests.

Playwright or Cypress?

AspectPlaywrightCypress
BrowsersChromium, Firefox, WebKitChrome-family, Firefox, Edge
ParallelizationBuilt into Playwright TestOften designed with Dashboard
Multiple tabs/contextsStrong supportMore constrained
DebuggingTrace Viewer, UI mode, HTML reportFriendly interactive GUI
Best fitCI, multi-browser, isolated auth stateFrontend teams with existing Cypress workflows

Cypress remains a solid choice. If your team already has Cypress coverage, do not migrate just for novelty. Add Playwright to one critical flow and compare flake rate, trace quality, and CI time. Always verify behavior against the official Playwright and Cypress docs.

Monetization CTA and result

E2E testing also protects revenue. A broken consultation link, product card, free cheatsheet form, or training CTA can cost more than a visual bug. Solo builders can start with the free Claude Code cheatsheet, then use products and templates for repeatable prompts. Teams that need CI gates, CLAUDE.md, review rules, and ownership around E2E should use Claude Code training and consultation.

I tested the article’s sample by extracting the Playwright spec and config as TypeScript snippets for syntax validation. The useful habit was naming the business expectation directly: “keeps the training CTA.” In real projects, start with three tests only: login, checkout or lead form, and article CTA. Expand after those are stable in CI.

#Claude Code #E2E testing #Playwright #Cypress #CI
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.