Writing Playwright tests with Claude Code is the fastest way to build a test suite in 2026.
I use it on every project. In 30 minutes, I generate tests that would take 2 hours manually. But only if you prompt correctly.
This guide shows you the exact workflow, prompts, and patterns I use.
Setup: What You Need
Requirements:
- ✅ Playwright installed (`npm install -D @playwright/test`)
- ✅ Claude Code (free or Claude 3.5 Sonnet subscription)
- ✅ Your application's code (or at least understanding of its UI)
- ✅ 30 minutes to learn the prompting workflow
Tools I recommend:
- Claude.ai/code (free) — Direct Claude conversation
- Cursor IDE ($20/month) — Claude integrated into VS Code
- Claude Code VSCode extension — Claude in your editor
The Core Workflow
Step 1: Give Claude the context
Tell Claude about your application, its UI, and what you're testing.
I'm building test cases for a SaaS product called [ProductName].
Feature: User authentication
Technologies: React frontend, Node.js backend, PostgreSQL database
UI Structure: Login form at /login with email and password fields
Step 2: Describe what you want to test
Be specific about the feature and test scenarios.
Generate test cases for the login feature:
- Valid credentials (happy path)
- Invalid password
- Non-existent user
- Empty fields
- Rate limiting (5 failed attempts blocks for 15 min)
Step 3: Get Claude to generate tests
Claude writes Playwright tests for all scenarios.
Step 4: Review and validate
Copy tests into your project, run them, tweak selectors if needed.
Real Example 1: Login Feature Tests
Your prompt:
Write Playwright tests for a login form with these requirements:
UI selectors:
- Email input: input[data-testid=\"email\"]
- Password input: input[data-testid=\"password\"]
- Login button: button[type=\"submit\"]
- Error message: [role=\"alert\"]
- Success redirect: URL should be /dashboard
Test cases needed:
1. Login with valid credentials (email: user@test.com, password: Test123!)
2. Show error for invalid password
3. Show error for non-existent user
4. Clear error message when user fixes input
5. Disable button while login is in progress
Use TypeScript and Playwright best practices. Each test should be independent.
Claude's output (example):
import { test, expect } from '@playwright/test';
const BASE_URL = 'http://localhost:3000';
test.describe('Login Feature', () => {
test.beforeEach(async ({ page }) => {
await page.goto(`${BASE_URL}/login`);
});
test('should login with valid credentials', async ({ page }) => {
await page.fill('input[data-testid=\"email\"]', 'user@test.com');
await page.fill('input[data-testid=\"password\"]', 'Test123!');
await page.click('button[type=\"submit\"]');
await expect(page).toHaveURL(/.*dashboard/);
});
test('should show error for invalid password', async ({ page }) => {
await page.fill('input[data-testid=\"email\"]', 'user@test.com');
await page.fill('input[data-testid=\"password\"]', 'wrongpassword');
await page.click('button[type=\"submit\"]');
await expect(page.locator('[role=\"alert\"]')).toContainText('Invalid credentials');
});
test('should show error for non-existent user', async ({ page }) => {
await page.fill('input[data-testid=\"email\"]', 'nonexistent@test.com');
await page.fill('input[data-testid=\"password\"]', 'Test123!');
await page.click('button[type=\"submit\"]');
await expect(page.locator('[role=\"alert\"]')).toContainText('User not found');
});
test('should clear error when user corrects input', async ({ page }) => {
// First, trigger an error
await page.fill('input[data-testid=\"email\"]', 'user@test.com');
await page.fill('input[data-testid=\"password\"]', 'wrongpassword');
await page.click('button[type=\"submit\"]');
await expect(page.locator('[role=\"alert\"]')).toBeVisible();
// Now correct the password
await page.fill('input[data-testid=\"password\"]', 'Test123!');
await expect(page.locator('[role=\"alert\"]')).not.toBeVisible();
});
test('should disable button while login is in progress', async ({ page }) => {
await page.fill('input[data-testid=\"email\"]', 'user@test.com');
await page.fill('input[data-testid=\"password\"]', 'Test123!');
const submitButton = page.locator('button[type=\"submit\"]');
// Start the login
await submitButton.click();
// Button should be disabled during request
await expect(submitButton).toBeDisabled();
});
});
Quality assessment:
- ✅ 5 tests generated in 30 seconds (vs 40 minutes manually)
- ✅ Covers main scenarios + edge cases
- ✅ Uses TypeScript best practices
- ✅ Tests are independent (can run in any order)
- ⚠️ One test (button disabled) might be flaky depending on response time — you'd adjust with `waitForLoadState()`
Your next step: Copy the tests, run them with `npx playwright test`, fix selectors if they don't match your actual app.
Real Example 2: API Response Validation
Your prompt (when testing feature that depends on API):
Generate Playwright tests for a product listing page.
The page fetches products from /api/products and displays them in a grid.
API response structure:
{
\"products\": [
{
\"id\": 1,
\"name\": \"Product A\",
\"price\": 99.99,
\"image\": \"url\",
\"inStock\": true
}
]
}
UI elements:
- Loading spinner: [data-testid=\"loading\"]
- Product cards: [data-testid=\"product-card\"]
- Price text in card: .price
- Add to cart button: button[aria-label=\"Add to cart\"]
- Out of stock badge: .badge-out-of-stock
Test scenarios:
1. Load products and display them in grid
2. Show loading state while fetching
3. Display \"out of stock\" badge for unavailable products
4. Click add to cart (should work for in-stock items)
5. Handle API error gracefully
Claude generates:
test('should load and display products', async ({ page }) => {
await page.goto('/products');
// Wait for products to load
await expect(page.locator('[data-testid=\"product-card\"]')).toHaveCount(5);
// Verify first product details
const firstProduct = page.locator('[data-testid=\"product-card\"]').first();
await expect(firstProduct.locator('.price')).toContainText('99.99');
});
test('should show loading spinner', async ({ page }) => {
// Use slow network to see spinner
await page.route('**/api/products', async route => {
await new Promise(resolve => setTimeout(resolve, 1000));
await route.continue();
});
await page.goto('/products');
await expect(page.locator('[data-testid=\"loading\"]')).toBeVisible();
});
test('should show out of stock badge', async ({ page }) => {
await page.goto('/products');
// Find product that's out of stock (inStock: false)
const outOfStockProduct = page.locator('.badge-out-of-stock');
await expect(outOfStockProduct).toBeVisible();
});
test('should allow adding in-stock items to cart', async ({ page }) => {
await page.goto('/products');
// Find first in-stock product
const inStockProduct = page.locator('[data-testid=\"product-card\"]').first();
const addBtn = inStockProduct.locator('button[aria-label=\"Add to cart\"]');
await addBtn.click();
// Verify item was added (button becomes disabled or shows \"added\")
await expect(addBtn).toBeDisabled();
});
test('should handle API errors gracefully', async ({ page }) => {
// Mock API error
await page.route('**/api/products', route => route.abort());
await page.goto('/products');
// Should show error message
await expect(page.locator('[role=\"alert\"]')).toContainText('Failed to load products');
});
Pro Tips for Better Prompts
Tip 1: Provide actual UI selectors
Bad prompt:
Generate tests for a button that deletes a user.
Good prompt:
Generate tests for a delete user button.
UI selectors:
- Delete button: button[data-testid=\"delete-user-btn\"]
- Confirmation dialog: [role=\"dialog\"]
- Confirm button: button[data-testid=\"confirm-delete\"]
- Success message: [role=\"alert\"].success
Expected behavior:
1. Click delete → show confirmation dialog
2. Confirm → delete user and show success message
3. Cancel → close dialog without deleting
Tip 2: Show Claude your code
Paste your actual component or API response so Claude understands the real structure:
Here's my component:
```typescript
export function ProductCard({ product }) {
return (
{product.name}
${product.price}
{!product.inStock && Out of Stock}
);
}
```
Generate tests for this component.
Tip 3: Request specific test patterns
If you want Page Object Model:
Use Page Object Model pattern. Create a LoginPage class with methods like:
- goto()
- login(email, password)
- getErrorMessage()
If you want fixtures:
Create a fixture for authenticated users. The fixture should:
1. Go to login page
2. Enter test credentials
3. Wait for redirect to dashboard
Tip 4: Ask Claude to explain
If generated tests look wrong, ask Claude to explain:
Why did you use `toHaveCount()` instead of `toBeVisible()`?
Claude will explain the reasoning, helping you learn Playwright best practices.
Common Patterns Claude Understands
| Pattern | Example Prompt | Claude's Understanding |
|---|---|---|
| Form validation | \"Generate tests for email validation\" | Tests for empty, invalid, duplicate emails |
| API mocking | \"Mock the API to return 500 error\" | Uses page.route() to intercept and abort |
| Async operations | \"Test async data loading\" | Uses waitFor() to handle async state |
| User flows | \"Test complete checkout flow\" | Multi-step test visiting multiple pages |
| Edge cases | \"Test boundary conditions\" | Tests min/max values, empty states |
The Review Checklist (Before Using Generated Tests)
After Claude generates tests, review them:
✅ Structure:
- Does each test test ONE thing?
- Are tests independent (can run in any order)?
- Is setup in `beforeEach` consistent?
✅ Selectors:
- Are selectors using `data-testid` (stable)?
- Are selectors valid for your app?
- Do they match your actual DOM?
✅ Assertions:
- Does each test assert on the right thing?
- Are assertions testing behavior, not implementation?
- Would a human catch this failure and understand it?
✅ Realism:
- Would this test actually catch a bug?
- Is the test flaky or brittle?
- Are timeouts and waits reasonable?
Workflow: Claude Code IDE Integration
Using Cursor IDE (my recommendation for speed):
- Open your test file
- Press Cmd+K (Mac) or Ctrl+K (Windows)
- Type: \"Generate Playwright tests for the login feature with these scenarios: [paste your requirements]\"
- Claude generates inline
- Accept or modify the code
- Run tests immediately: `npx playwright test`
- Adjust selectors if they don't match your app
Time investment: 5 minutes for full test suite generation vs 60 minutes manual
Frequently Asked Questions
Can Claude generate tests for my specific app?
Yes, but Claude needs to understand your app first. Paste your component code, API responses, and UI structure. More context = better tests.
Are Claude-generated tests production-ready?
85-90% of the time, yes. 10-15% of the time you'll need to adjust selectors, timeouts, or assertions. Review before using.
What if Claude generates tests that don't match my app?
Provide more context. Show Claude your actual component code or a screenshot of the UI. Be specific about selectors.
Should I keep Claude's test structure or refactor?
Keep it if it's readable and works. Only refactor if you need Page Object Model or other specific patterns. Don't over-engineer.
Next Steps
This week:
- Pick one feature to test
- Get your actual selectors (use Inspector to verify)
- Write a detailed prompt with UI structure + requirements
- Ask Claude to generate tests
- Copy tests and run them
- Measure time saved (should be 70-80% faster than manual)
Next week: Expand to more features. Build your full test suite with Claude's help.
Need help setting up Playwright with Claude Code? I offer team training on AI-assisted test writing.
Let's build your test suite faster →
Related Articles:
Tayyab Akmal
AI & QA Automation Engineer
Automation & AI Engineer with 6+ years in scalable test automation and real-world AI solutions. I build intelligent frameworks, QA pipelines, and AI agents that make testing faster, smarter, and more reliable.