Writing Tests Intermediate

Learn how to use Claude Code to generate comprehensive Playwright tests from plain English descriptions. From basic page tests to complex user flows, visual regression, API testing, and accessibility audits.

1. Generating Tests from Descriptions

The most powerful feature of combining Playwright with Claude Code is the ability to describe what you want to test and have the test generated automatically. Claude Code reads your application code to understand the UI structure and generates appropriate selectors and assertions.

Claude Code Session
Claude > Write a Playwright test suite for the shopping cart page.
         Test these scenarios:
         1. Adding an item to the cart
         2. Removing an item from the cart
         3. Updating item quantity
         4. Verifying the total price calculation
         5. Applying a discount code
         Look at src/pages/Cart.tsx and src/components/CartItem.tsx
         to understand the UI structure.

Claude Code will read the referenced files, identify the elements, and generate a complete test suite:

TypeScript (tests/e2e/cart.spec.ts)
import { test, expect } from '@playwright/test';

test.describe('Shopping Cart', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/cart');
  });

  test('can add item to cart', async ({ page }) => {
    await page.goto('/products/1');
    await page.getByRole('button', { name: 'Add to Cart' }).click();
    await expect(page.getByTestId('cart-count')).toHaveText('1');
  });

  test('can remove item from cart', async ({ page }) => {
    await page.getByRole('button', { name: 'Remove' }).first().click();
    await expect(page.getByText('Your cart is empty')).toBeVisible();
  });

  test('can update item quantity', async ({ page }) => {
    await page.getByLabel('Quantity').first().fill('3');
    await page.getByLabel('Quantity').first().press('Enter');
    await expect(page.getByTestId('item-subtotal').first())
      .toContainText('$59.97');
  });

  test('calculates total price correctly', async ({ page }) => {
    const total = await page.getByTestId('cart-total').textContent();
    expect(parseFloat(total!.replace('$', ''))).toBeGreaterThan(0);
  });

  test('can apply discount code', async ({ page }) => {
    await page.getByPlaceholder('Discount code').fill('SAVE10');
    await page.getByRole('button', { name: 'Apply' }).click();
    await expect(page.getByText('10% discount applied')).toBeVisible();
  });
});

2. Page Object Model Generation

Claude Code can generate Page Object Model (POM) classes that encapsulate page interactions, making tests more maintainable:

Claude Code Session
Claude > Read src/pages/Login.tsx and create a Page Object class
         for the login page. Include methods for filling credentials,
         submitting the form, and checking for error messages.
         Save it as tests/pages/login.page.ts
TypeScript (tests/pages/login.page.ts)
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.submitButton = page.getByRole('button', { name: 'Sign In' });
    this.errorMessage = page.getByRole('alert');
  }

  async goto() {
    await this.page.goto('/login');
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }

  async expectRedirectTo(url: string) {
    await expect(this.page).toHaveURL(url);
  }
}

3. Testing User Flows

Claude Code excels at generating tests for complex multi-step user flows. Here are common patterns:

Login Flow

TypeScript
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/login.page';

test('successful login redirects to dashboard', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'password123');
  await loginPage.expectRedirectTo('/dashboard');
});

test('invalid credentials show error', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.goto();
  await loginPage.login('user@example.com', 'wrong');
  await loginPage.expectError('Invalid credentials');
});

Checkout Flow

Claude Code Session
Claude > Write a Playwright test for the complete checkout flow:
         1. Add a product to the cart from the product listing page
         2. Go to the cart and verify the item is there
         3. Click checkout and fill in shipping details
         4. Enter payment information
         5. Confirm the order
         6. Verify the order confirmation page

Form Validation

TypeScript
test('form validation shows correct error messages', async ({ page }) => {
  await page.goto('/register');

  // Submit empty form
  await page.getByRole('button', { name: 'Register' }).click();

  // Check validation messages
  await expect(page.getByText('Email is required')).toBeVisible();
  await expect(page.getByText('Password is required')).toBeVisible();

  // Test invalid email
  await page.getByLabel('Email').fill('not-an-email');
  await page.getByRole('button', { name: 'Register' }).click();
  await expect(page.getByText('Please enter a valid email')).toBeVisible();
});

4. Visual Testing

Playwright supports visual regression testing with screenshot comparison. Claude Code can set this up for you:

TypeScript
test('homepage matches visual snapshot', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('homepage.png', {
    maxDiffPixelRatio: 0.01,
  });
});

test('product card component snapshot', async ({ page }) => {
  await page.goto('/products');
  const card = page.getByTestId('product-card').first();
  await expect(card).toHaveScreenshot('product-card.png');
});

5. API Testing

Playwright can also test APIs directly, and Claude Code can generate these tests alongside UI tests:

TypeScript
import { test, expect } from '@playwright/test';

test.describe('API Tests', () => {
  test('GET /api/products returns product list', async ({ request }) => {
    const response = await request.get('/api/products');
    expect(response.ok()).toBeTruthy();
    const products = await response.json();
    expect(products.length).toBeGreaterThan(0);
    expect(products[0]).toHaveProperty('name');
    expect(products[0]).toHaveProperty('price');
  });

  test('POST /api/orders creates an order', async ({ request }) => {
    const response = await request.post('/api/orders', {
      data: { productId: 1, quantity: 2 }
    });
    expect(response.status()).toBe(201);
  });
});

6. Accessibility Testing

Use Playwright with @axe-core/playwright for automated accessibility testing:

Claude Code Session
Claude > Install @axe-core/playwright and write accessibility tests
         for the homepage, login page, and product listing page.
         Check for WCAG 2.1 AA compliance.
TypeScript
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test('homepage has no accessibility violations', async ({ page }) => {
  await page.goto('/');
  const results = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa'])
    .analyze();
  expect(results.violations).toEqual([]);
});

Practice Exercise

Ask Claude Code to generate a test suite for a form on your application. Include tests for successful submission, validation errors, and accessibility. Run the tests and iterate on any failures.

Next: Debugging →