Skip to main content
/tayyab/portfolio — zsh
tayyab
TA
// dispatch.read --classified=false --access-level: public

Using Claude Code to Write Playwright Tests: Practical Guide (2026)

March 26, 2026 EST. READ: 13 MIN #Quality Assurance

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):

  1. Open your test file
  2. Press Cmd+K (Mac) or Ctrl+K (Windows)
  3. Type: \"Generate Playwright tests for the login feature with these scenarios: [paste your requirements]\"
  4. Claude generates inline
  5. Accept or modify the code
  6. Run tests immediately: `npx playwright test`
  7. 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:

  1. Pick one feature to test
  2. Get your actual selectors (use Inspector to verify)
  3. Write a detailed prompt with UI structure + requirements
  4. Ask Claude to generate tests
  5. Copy tests and run them
  6. 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
// author

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.

// feedback_channel

FOUND THIS USEFUL?

Share your thoughts or let's discuss automation testing strategies.

→ Start Conversation
Available for hire