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:
- Understanding component structure
- Finding stable selectors
- Predicting async timing issues
- 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:
- Re-prompt with: "Add network idle waits before assertions"
- Specify: "Wait for API responses, not arbitrary timeouts"
- 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