Use AI to Write Playwright E2E Tests in 20 Minutes

Generate Playwright tests for complex UIs using Claude and GPT-4. Save hours on test writing with AI-powered selector generation and assertion logic.

Problem: Writing E2E Tests Takes Forever

You need comprehensive Playwright tests for a complex dashboard with nested modals, dynamic tables, and async state - but writing selectors and assertions manually will take days.

You'll learn:

  • How to feed UI context to AI effectively
  • Generate reliable selectors that don't break
  • Create assertions for complex async behavior
  • When AI helps vs when you need manual testing

Time: 20 min | Level: Intermediate


Why This Happens

Traditional E2E test writing requires:

  1. Understanding component structure
  2. Finding stable selectors
  3. Predicting async timing issues
  4. Writing edge case assertions

AI can handle 1-3 if given proper context, saving 70% of the manual work.

Common pain points:

  • Fragile CSS selectors that break on UI changes
  • Missing async waits causing flaky tests
  • Incomplete edge case coverage
  • Copy-pasting boilerplate across 50+ tests

Solution

Step 1: Capture UI Context

Generate an HTML snapshot of your component for AI analysis:

// capture-ui.ts
import { chromium } from '@playwright/test';

async function captureUI() {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  
  await page.goto('http://localhost:3000/dashboard');
  await page.waitForLoadState('networkidle');
  
  // Get full DOM with data attributes
  const html = await page.content();
  const snapshot = {
    html,
    url: page.url(),
    viewport: page.viewportSize()
  };
  
  await fs.writeFile('ui-snapshot.json', JSON.stringify(snapshot, null, 2));
  await browser.close();
}

Why this works: AI needs the actual DOM structure to generate accurate selectors, not just screenshots.

Expected: Creates ui-snapshot.json with full page HTML including data-testid attributes.


Step 2: Create AI Prompt Template

// prompts/test-generator.ts
export const PLAYWRIGHT_PROMPT = `
You are a Playwright test expert. Generate E2E tests using this context:

## UI Snapshot
{UI_HTML}

## Requirements
- Component: {COMPONENT_NAME}
- User flow: {USER_FLOW}
- Critical paths: {CRITICAL_PATHS}

## Rules
1. Use data-testid selectors (prefer) or role-based selectors
2. Add explicit waits for async operations
3. Include negative test cases
4. Use page object pattern
5. No hard-coded waits (no page.waitForTimeout())

Generate tests in Playwright TypeScript format.
`;

Why structured prompts: Generic "write a test" produces brittle code. Specific constraints get production-ready output.


Step 3: Generate Tests with Claude

// generate-tests.ts
import Anthropic from '@anthropic-ai/sdk';
import { PLAYWRIGHT_PROMPT } from './prompts/test-generator';

async function generateTests(component: string, userFlow: string) {
  const anthropic = new Anthropic({
    apiKey: process.env.ANTHROPIC_API_KEY,
  });

  const snapshot = JSON.parse(await fs.readFile('ui-snapshot.json', 'utf-8'));
  
  const message = await anthropic.messages.create({
    model: 'claude-sonnet-4-20250514',
    max_tokens: 4000,
    messages: [{
      role: 'user',
      content: PLAYWRIGHT_PROMPT
        .replace('{UI_HTML}', snapshot.html)
        .replace('{COMPONENT_NAME}', component)
        .replace('{USER_FLOW}', userFlow)
        .replace('{CRITICAL_PATHS}', 'login, data validation, error states')
    }]
  });

  const testCode = message.content[0].text;
  
  // Extract code blocks
  const codeMatch = testCode.match(/```typescript\n([\s\S]+?)\n```/);
  if (codeMatch) {
    await fs.writeFile(
      `tests/${component}.spec.ts`,
      codeMatch[1]
    );
  }
}

generateTests('UserDashboard', 'User logs in, views analytics, exports CSV');

Expected: Generates tests/UserDashboard.spec.ts with complete test file.

If it fails:

  • Error: "Missing data-testid": Add them to your components first
  • Generic selectors: Provide more specific UI snapshot with only relevant HTML
  • No async waits: Add explicit requirement in prompt

Step 4: Add Page Objects (AI-Generated)

// Prompt for page objects
const PAGE_OBJECT_PROMPT = `
Given this Playwright test:
{GENERATED_TEST}

Extract a page object model following this pattern:
- One class per page/component
- Methods for user actions (click, fill, select)
- Getters for elements
- No assertions in page object

Generate TypeScript page object class.
`;

Example AI output:

// pages/dashboard.page.ts
export class DashboardPage {
  constructor(private page: Page) {}

  // Locators
  get analyticsTable() {
    return this.page.getByTestId('analytics-table');
  }

  get exportButton() {
    return this.page.getByRole('button', { name: /export/i });
  }

  // Actions
  async navigateTo() {
    await this.page.goto('/dashboard');
    await this.page.waitForSelector('[data-testid="analytics-table"]');
  }

  async exportData(format: 'csv' | 'json') {
    await this.exportButton.click();
    await this.page.getByRole('menuitem', { name: format }).click();
    
    // Wait for download to start
    const downloadPromise = this.page.waitForEvent('download');
    await this.page.getByRole('button', { name: 'Confirm' }).click();
    return await downloadPromise;
  }

  async getRowCount() {
    await this.analyticsTable.waitFor();
    return await this.analyticsTable.locator('tbody tr').count();
  }
}

Why this works: Page objects make tests readable and reduce duplication when AI generates multiple test scenarios.


Step 5: Validate and Iterate

Run AI-generated tests and fix common issues:

# Run in headed mode to see what AI wrote
npx playwright test --headed --project=chromium

# Check for flaky tests
npx playwright test --repeat-each=5

Common AI mistakes to fix:

// ❌ AI often generates this (flaky)
await page.click('[data-testid="submit"]');
await expect(page.locator('.success')).toBeVisible();

// ✅ Add explicit state wait
await page.click('[data-testid="submit"]');
await page.waitForResponse(resp => 
  resp.url().includes('/api/submit') && resp.status() === 200
);
await expect(page.locator('.success')).toBeVisible();

If tests are flaky:

  1. Re-prompt with: "Add network idle waits before assertions"
  2. Specify: "Wait for API responses, not arbitrary timeouts"
  3. Include failed test output in prompt for AI to fix

Step 6: Generate Edge Cases

const EDGE_CASE_PROMPT = `
Given this happy path test:
{EXISTING_TEST}

Generate edge case tests for:
- Empty states (no data)
- Loading states (slow network)
- Error states (API failures)
- Permission denied
- Concurrent actions
- Invalid inputs

Use the same page object pattern.
`;

Example output:

test.describe('Dashboard Edge Cases', () => {
  test('shows empty state when no data', async ({ page }) => {
    // Mock empty API response
    await page.route('**/api/analytics', route =>
      route.fulfill({ json: { data: [] } })
    );

    const dashboard = new DashboardPage(page);
    await dashboard.navigateTo();

    await expect(page.getByText('No data available')).toBeVisible();
    await expect(dashboard.exportButton).toBeDisabled();
  });

  test('handles slow network gracefully', async ({ page }) => {
    // Throttle network
    await page.route('**/api/analytics', route =>
      route.fulfill({ 
        json: { data: mockData },
        delay: 5000 // 5s delay
      })
    );

    const dashboard = new DashboardPage(page);
    await dashboard.navigateTo();

    // Should show loading state
    await expect(page.getByTestId('loading-spinner')).toBeVisible();
    
    // Then data after delay
    await expect(dashboard.analyticsTable).toBeVisible({ timeout: 10000 });
  });
});

Verification

Run the full test suite with coverage:

# Run all generated tests
npx playwright test

# Check coverage (requires instrumentation)
npx playwright test --coverage

You should see:

  • 90%+ tests passing on first run
  • Clear failure messages for the 10% that need manual fixes
  • Page objects reusable across multiple test files

Success criteria:

Running 24 tests using 3 workers

  23 passed (1.2m)
  1 failed  # Expected - fix manually
    tests/UserDashboard.spec.ts:45:5 › handles concurrent exports

What You Learned

  • AI excels at: Selector generation, boilerplate reduction, edge case brainstorming
  • You still need to: Validate async logic, test flakiness, domain-specific assertions
  • Best workflow: AI writes 80%, you refine 20%

Limitations:

  • AI can't know your exact API contracts (use mocks in prompt)
  • Complex auth flows need manual setup
  • Visual regression still requires human judgment

When NOT to use AI:

  • Security-critical flows (manual verification required)
  • Tests that need real API integration
  • Performance testing (need specific metrics)

Advanced: Continuous Test Generation

Set up a workflow to auto-generate tests when components change:

// scripts/auto-generate-tests.ts
import chokidar from 'chokidar';

const watcher = chokidar.watch('src/components/**/*.tsx');

watcher.on('change', async (path) => {
  console.log(`Component changed: ${path}`);
  
  // Extract component name
  const componentName = path.match(/\/([^/]+)\.tsx$/)?.[1];
  
  if (componentName) {
    // Capture new UI snapshot
    await captureUI();
    
    // Regenerate tests
    await generateTests(
      componentName,
      'Review existing functionality and add new scenarios'
    );
    
    console.log(`✓ Tests updated for ${componentName}`);
  }
});

Run in development:

npm run test:watch

This keeps tests in sync with UI changes automatically.


Real-World Example

Before AI (2 hours):

// Manually written test - took forever to get selectors right
test('exports data', async ({ page }) => {
  await page.goto('/dashboard');
  await page.click('button:has-text("Export")'); // Breaks if text changes
  await page.click('text=CSV'); // Fragile selector
  // Forgot to wait for download...
  const download = await page.waitForEvent('download'); // Added after test failed
  expect(download.suggestedFilename()).toContain('data.csv');
});

After AI (15 minutes including validation):

// AI-generated with page object
test('exports data in CSV format', async ({ page }) => {
  const dashboard = new DashboardPage(page);
  await dashboard.navigateTo();
  
  const download = await dashboard.exportData('csv');
  
  expect(download.suggestedFilename()).toMatch(/analytics-\d{8}\.csv/);
  
  // AI added this edge case I didn't think of
  const content = await download.path();
  const csv = await fs.readFile(content, 'utf-8');
  expect(csv.split('\n').length).toBeGreaterThan(1); // Has data
});

Improvement: Stable selectors, proper async handling, edge case coverage - all generated in seconds.


Troubleshooting

AI generates incorrect selectors

Problem: AI uses CSS classes that change between builds.

Fix: Add to prompt:

CRITICAL: Never use CSS classes for selectors.
Only use: data-testid, getByRole, getByLabel, getByText (exact strings only)

Tests pass locally but fail in CI

Problem: AI doesn't account for CI environment differences.

Fix: Provide CI context:

const CI_CONTEXT = `
Environment: GitHub Actions, headless Chrome
Viewport: 1280x720
No animations (prefers-reduced-motion)
Network: Throttled to 3G
`;

Too many tests generated

Problem: AI creates 50 test cases for a simple form.

Fix: Be specific:

Generate exactly 5 tests:
1. Happy path
2. Validation error
3. Network error
4. Empty state
5. Permission denied

Tested with Playwright 1.48, Claude Sonnet 4, Node.js 22.x Example repo: github.com/example/ai-playwright-demo