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.
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 case | Why it belongs in E2E | Key expectations |
|---|---|---|
| Login to dashboard | Auth, redirects, and permissions meet in one flow | Heading, user state, logout, error alert |
| Checkout or consultation form | Direct revenue or lead impact | Order number, confirmation, no double submit |
| Article to free PDF or training page | Protects non-AdSense monetization | CTA href, tracking event, mobile visibility |
| Admin destructive action | Prevents accidental deletion or privilege leaks | Confirmation dialog, audit log, role checks |
| Localized pages | Translation often breaks links | Locale 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?
| Aspect | Playwright | Cypress |
|---|---|---|
| Browsers | Chromium, Firefox, WebKit | Chrome-family, Firefox, Edge |
| Parallelization | Built into Playwright Test | Often designed with Dashboard |
| Multiple tabs/contexts | Strong support | More constrained |
| Debugging | Trace Viewer, UI mode, HTML report | Friendly interactive GUI |
| Best fit | CI, multi-browser, isolated auth state | Frontend 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.
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.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Obsidian to CLAUDE.md Workflow: Stop Re-explaining Context
Turn Obsidian working notes into concise CLAUDE.md operating notes that make Claude Code sessions easier to resume.
Claude Code Revenue CTA Routing: Send Articles to PDF, Gumroad, and Consultation
A Claude Code workflow for routing article readers to the free PDF, Gumroad products, or consultation by intent.
Claude Code Team Handoff Rules: Review Evidence, Permissions, Rollback, and Revenue Paths
A practical Claude Code handoff format for team review, proof, permission rules, rollback, free PDF, Gumroad, and consultation paths.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.