AI Tools Compared

Claude Code excels at generating Playwright tests for multi-step form wizards because it understands complex state preservation, conditional navigation, step-specific validation, and asynchronous wait strategies. When given your form structure, Claude produces maintainable tests with proper selectors, correct async handling, and meaningful assertions that account for wizard-specific patterns like progress indicators and data persistence across steps.

Why Multi-Step Form Wizards Need Specialized Testing

Multi-step form wizards differ from simple single-page forms in several critical ways. Each step typically maintains partial form state that must persist when users navigate forward or backward. Conditional fields may appear or disappear based on previous selections. Progress indicators track completion status. Validation rules apply differently at each step, and some fields may only validate after users reach specific stages.

Consider a typical checkout wizard with four steps: Shipping Information, Payment Details, Review Order, and Confirmation. Tests must verify that users cannot proceed without completing required fields, that selected shipping options affect total costs, that users can navigate back without losing entered data, and that the final order contains correct information from all previous steps.

What Makes an AI Assistant Effective for This Use Case

The best AI assistants for Playwright test generation share several characteristics. They understand Playwright’s API including locators, assertions, and configuration options. They recognize common wizard patterns like step indicators, back/next navigation, and form state preservation. They generate maintainable code with proper selectors instead of brittle XPath expressions. They handle async operations and wait strategies correctly. They produce tests that are readable and easy to debug when failures occur.

Poor AI assistants generate tests with hardcoded waits, brittle selectors that break when UI changes, missing assertions, or incorrect handling of form state. These issues create flaky tests that waste development time.

Comparing Top AI Assistants for Playwright Wizard Testing

Claude Code (Anthropic)

Claude Code demonstrates strong understanding of Playwright patterns and generates clean, maintainable test code. When prompted with a multi-step form scenario, it produces well-structured tests with appropriate wait strategies and meaningful test names.

Example output for a three-step registration wizard:

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

test.describe('Registration Wizard', () => {
  test('complete registration flow successfully', async ({ page }) => {
    await page.goto('/register');

    // Step 1: Account Details
    await expect(page.locator('h2')).toContainText('Account Details');
    await page.fill('#username', 'testuser');
    await page.fill('#email', 'test@example.com');
    await page.fill('#password', 'SecurePass123!');
    await page.click('button:has-text("Next")');

    // Step 2: Personal Information
    await expect(page.locator('h2')).toContainText('Personal Information');
    await page.fill('#firstName', 'John');
    await page.fill('#lastName', 'Doe');
    await page.click('button:has-text("Next")');

    // Step 3: Confirmation
    await expect(page.locator('h2')).toContainText('Confirm Details');
    await expect(page.locator('#summary-username')).toContainText('testuser');
    await page.click('button:has-text("Submit")');

    await expect(page.locator('.success-message')).toBeVisible();
  });

  test('validation prevents proceeding with invalid data', async ({ page }) => {
    await page.goto('/register');

    // Try to proceed without filling required fields
    await page.click('button:has-text("Next")');

    await expect(page.locator('#username-error')).toContainText('Required');
    await expect(page.locator('#email-error')).toContainText('Required');
  });
});

Claude Code excels at understanding context and generating relevant follow-up tests. It handles conditional logic well and produces tests that account for wizard-specific behaviors.

GitHub Copilot

Copilot integrates tightly with VS Code and provides real-time suggestions as you write tests. It works well for generating individual test cases but sometimes struggles with complex wizard flows that require understanding across multiple steps.

Strengths include fast suggestions and good autocomplete for Playwright methods. Limitations include occasionally suggesting outdated patterns and less understanding of wizard-specific testing patterns compared to specialized prompts.

Cursor

Cursor combines AI assistance with IDE features, making it useful for test generation and maintenance. It handles refactoring well, which helps when updating wizard tests after UI changes.

For multi-step forms, Cursor performs best when given explicit context about the wizard structure and validation rules. Its agent mode can interact with your application directly, which helps when exploring complex flows.

Practical Strategies for AI-Assisted Wizard Testing

Provide Complete Context

When prompting AI assistants, include the wizard’s step structure, validation requirements, and any conditional logic. Instead of a vague request like “test the checkout flow,” specify: “Create tests for a four-step checkout wizard where step 2 shows different fields based on shipping method selected in step 1, and step 3 displays dynamically calculated totals.”

Use Page Object Patterns

AI-generated tests work best when structured around page objects. This makes tests more maintainable and easier to update when wizard UI changes. Ask AI assistants to generate page object methods for each wizard step:

export class CheckoutWizard {
  constructor(private page: Page) {}

  async selectShippingMethod(method: 'standard' | 'express') {
    await this.page.click(`#shipping-${method}`);
  }

  async proceedToPayment() {
    await this.page.click('button:has-text("Continue to Payment")');
  }

  async getTotal(): Promise<number> {
    const totalText = await this.page.locator('#order-total').textContent();
    return parseFloat(totalText.replace('$', ''));
  }
}

Test Edge Cases Specifically

Wizard flows have numerous edge cases that AI sometimes overlooks. Explicitly request tests for: back navigation preserving entered data, session timeout handling, browser refresh mid-wizard, concurrent tab modifications, and step skipping attempts via direct URL access.

Validate State Transitions

Ensure your tests verify that wizard state updates correctly. Check that progress indicators reflect actual completion, that URL parameters track current step, and that form data persists appropriately between steps:

test('form data persists when navigating back', async ({ page }) => {
  await page.goto('/wizard/step-1');
  await page.fill('#name', 'Test User');
  await page.click('button:has-text("Next")');

  await page.click('button:has-text("Back")');

  await expect(page.locator('#name')).toHaveValue('Test User');
});

Recommendations

For teams working primarily with multi-step form wizards, Claude Code currently provides the best balance of understanding, code quality, and maintainability. Its context window allows it to grasp complex wizard structures, and its generated tests follow Playwright best practices.

However, the ideal choice depends on your workflow. Teams already using VS Code may prefer Copilot’s tight integration. Those valuing refactoring capabilities might lean toward Cursor. The key is selecting a tool that produces tests your team can maintain long-term, not just generate quickly.

Regardless of which assistant you choose, provide detailed context about your wizard’s specific behaviors. The more precisely you describe your multi-step form’s logic, validation rules, and expected user interactions, the more accurate and useful the generated tests will be.

Testing Dynamic Field Visibility in Wizards

Many wizards show/hide fields based on previous selections. AI can help generate tests for these conditional scenarios:

test('conditional fields appear based on shipping method', async ({ page }) => {
  await page.goto('/checkout');

  // Step 1: Select shipping method
  await expect(page.locator('h2')).toContainText('Shipping');
  await page.click('input[value="express"]');

  // Verify conditional fields appear
  await expect(page.locator('[data-field="overnight-by"]')).toBeVisible();
  await expect(page.locator('[data-field="signature-required"]')).toBeVisible();

  // Step 2: Select standard shipping
  await page.click('button:has-text("Modify Shipping")');
  await page.click('input[value="standard"]');

  // Verify different conditional fields appear
  await expect(page.locator('[data-field="overnight-by"]')).toBeHidden();
  await expect(page.locator('[data-field="estimated-delivery"]')).toBeVisible();
});

test('conditional validation triggers for conditional fields', async ({ page }) => {
  await page.goto('/checkout');

  // Select option requiring conditional field
  await page.click('input[value="express"]');

  // Try to proceed without filling conditional field
  await page.click('button:has-text("Next")');

  // Verify validation error appears only for conditional field
  await expect(page.locator('[data-error="overnight-by"]')).toContainText('Required');
});

Managing State Across Browser Navigation

Test that wizard state persists when users leave and return:

test('wizard state persists when navigating away and returning', async ({ page, context }) => {
  await page.goto('/wizard/step-1');

  // Fill first step
  await page.fill('#email', 'test@example.com');
  await page.fill('#password', 'SecurePass123!');

  // Navigate to email verification page
  await page.goto('/verify-email');
  await expect(page.locator('h1')).toContainText('Verify Email');

  // Return to wizard
  await page.back();

  // Verify data persisted
  await expect(page.locator('#email')).toHaveValue('test@example.com');
  await expect(page.locator('#password')).toHaveValue('SecurePass123!');
});

test('wizard state survives page refresh', async ({ page }) => {
  await page.goto('/wizard/step-2');

  // Fill current step
  await page.fill('#first-name', 'John');
  await page.fill('#last-name', 'Doe');

  // Refresh page
  await page.reload();

  // Verify data still present
  await expect(page.locator('#first-name')).toHaveValue('John');
  await expect(page.locator('#last-name')).toHaveValue('Doe');

  // Verify we're still on step 2
  await expect(page.locator('[data-step-indicator]')).toContainText('Step 2');
});

Testing Wizard Progress Indicators

Verify progress tracking works correctly:

test('progress indicator updates correctly', async ({ page }) => {
  await page.goto('/wizard');

  const progressBar = page.locator('[data-progress-bar]');

  // Step 1: Should show 25% (1 of 4 steps)
  await expect(progressBar).toHaveAttribute('aria-valuenow', '25');
  await expect(page.locator('[data-step-indicator]')).toContainText('Step 1 of 4');

  // Move to step 2
  await page.fill('#step-1-field', 'value');
  await page.click('button:has-text("Next")');

  // Should show 50% (2 of 4)
  await expect(progressBar).toHaveAttribute('aria-valuenow', '50');
  await expect(page.locator('[data-step-indicator]')).toContainText('Step 2 of 4');

  // Move to step 3
  await page.fill('#step-2-field', 'value');
  await page.click('button:has-text("Next")');

  // Should show 75% (3 of 4)
  await expect(progressBar).toHaveAttribute('aria-valuenow', '75');

  // Go back to step 1
  await page.click('button:has-text("Back")');
  await page.click('button:has-text("Back")');

  // Should show 25% again
  await expect(progressBar).toHaveAttribute('aria-valuenow', '25');
});

Testing Error Recovery in Multi-Step Forms

Handle error scenarios gracefully:

test('recovers from server error and allows retry', async ({ page }) => {
  await page.goto('/wizard/step-1');

  await page.fill('#email', 'test@example.com');
  await page.fill('#password', 'SecurePass123!');

  // Mock server error on first attempt
  let requestCount = 0;
  await page.route('**/api/wizard/validate-step-1', route => {
    requestCount++;
    if (requestCount === 1) {
      route.abort('failed');
    } else {
      route.continue();
    }
  });

  await page.click('button:has-text("Next")');

  // Verify error message
  await expect(page.locator('[role="alert"]')).toContainText('Network error');

  // Retry should succeed
  await page.click('button:has-text("Retry")');
  await expect(page.locator('h2')).toContainText('Step 2');
});

test('handles timeout during form submission', async ({ page }) => {
  await page.goto('/wizard/step-1');

  // Mock slow response
  await page.route('**/api/wizard/**', route => {
    route.abort('timedout');
  });

  await page.fill('#email', 'test@example.com');
  await page.click('button:has-text("Next")');

  // Verify timeout message
  await expect(page.locator('[role="alert"]')).toContainText('Request timed out');

  // Verify retry button available
  await expect(page.locator('button:has-text("Retry")')).toBeEnabled();
});

Performance Testing for Large Wizards

Test that wizards with many steps remain performant:

test('large wizard (20+ steps) maintains acceptable performance', async ({ page }) => {
  await page.goto('/wizard');

  const stepTimes = [];

  for (let step = 1; step <= 20; step++) {
    const startTime = performance.now();

    // Fill step field
    await page.fill('input[name="step-field"]', `value-${step}`);

    // Click next
    await page.click('button:has-text("Next")');

    // Wait for step to load
    await expect(page.locator('h2')).toContainText(`Step ${step + 1}`);

    const duration = performance.now() - startTime;
    stepTimes.push(duration);

    // Assert step transitions in under 500ms
    expect(duration).toBeLessThan(500);
  }

  // Verify no significant slowdown in later steps
  const firstFiveAvg = stepTimes.slice(0, 5).reduce((a, b) => a + b) / 5;
  const lastFiveAvg = stepTimes.slice(-5).reduce((a, b) => a + b) / 5;

  expect(lastFiveAvg).toBeLessThan(firstFiveAvg * 1.5);
});

Testing Accessibility in Multi-Step Forms

Ensure wizards are accessible to all users:

test('wizard is fully keyboard navigable', async ({ page }) => {
  await page.goto('/wizard');

  // Start with focus on first field
  await page.press('body', 'Tab');
  await expect(page.locator('#step-1-field')).toBeFocused();

  // Tab through form
  await page.press('body', 'Tab');
  await page.press('body', 'Tab');

  // Next button should be focused
  await expect(page.locator('button:has-text("Next")')).toBeFocused();

  // Enter activates next
  await page.press('body', 'Enter');

  // Verify moved to step 2
  await expect(page.locator('h2')).toContainText('Step 2');
});

test('wizard announcements for screen readers', async ({ page }) => {
  await page.goto('/wizard');

  const liveRegion = page.locator('[aria-live="polite"]');

  // Announce step completion
  await page.fill('#step-1-field', 'value');
  await page.click('button:has-text("Next")');

  // Verify announcement
  const announcement = await liveRegion.textContent();
  expect(announcement).toContain('Step 1 complete');
  expect(announcement).toContain('Now on Step 2');
});

Built by theluckystrike — More at zovo.one