Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 078 / 100

Using Playwright with AI tools: best practices explained

Speed up test writing.

Expona Admin9 min read

Introduction

AI has fundamentally changed how we approach test automation. Rather than spending hours writing boilerplate locators and test scaffolding, we can now use large language models to generate first drafts, letting us focus on logic and assertions. However, using Playwright with AI tools: best practices explained isn't just about throwing prompts at ChatGPT and calling it a day—it's about establishing a workflow that keeps tests maintainable, readable, and genuinely owned by your team.

Over the past 78 days of this Playwright and browser automation series, we've built a solid foundation in locators, waits, fixtures, and cross-browser testing. Now we're entering a territory where many teams stumble: leveraging AI without losing control of their test suite. This article walks through practical strategies we've refined across production codebases.

Generate first drafts with AI

The real win with AI isn't magical test generation—it's eliminating boilerplate. When you're starting a new flow like user checkout or password reset, describing the happy path to Claude or GPT-4 and getting a working scaffold saves 20–30 minutes of repetitive typing.

Here's a realistic scenario: you need to test a multi-step form that collects shipping details, validates postcodes, and applies regional discounts. Instead of manually structuring the test file and writing mock data, you describe it to AI:

"Write a Playwright test for an e-commerce checkout flow. The user enters shipping address (with postcode validation), selects shipping method, reviews the order total (which includes regional discount), and completes payment. Use page.fill for inputs, check for success message. Mock the API calls for discount calculation."

You'll get back a solid first draft:

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

test('checkout flow with regional discount', async ({ page }) => {
  await page.goto('https://shop.example.com/checkout');
  
  // Fill shipping form
  await page.fill('input[name="address"]', '123 Main St');
  await page.fill('input[name="postcode"]', 'SW1A 1AA');
  await page.fill('input[name="city"]', 'London');
  
  // Validate postcode feedback
  const postcodeError = page.locator('[data-testid="postcode-error"]');
  await expect(postcodeError).not.toBeVisible();
  
  // Select shipping method
  await page.click('label:has-text("Express Delivery")');
  
  // Check discount applied
  const discountAmount = page.locator('[data-testid="discount-amount"]');
  await expect(discountAmount).toContainText('£10.00');
  
  // Complete payment
  await page.click('button:has-text("Complete Purchase")');
  
  // Verify success
  await expect(page).toHaveURL(/\/order-confirmation/);
});

That's genuinely useful. You're not starting from scratch; you're refining something that runs. In our experience, this cuts initial scaffolding time by 40–50%. The key is being specific in your prompt: mention page objects if you use them, specify data-testid patterns your team follows, and include any mocking or fixture setup you need.

Playwright MCP lets agents drive browsers

A newer frontier is Model Context Protocol (MCP)—a standard that lets AI agents interact with Playwright directly. Rather than generating code snippets, the AI actually drives the browser, records its actions, and produces tests from observed behavior.

Tools like Anthropic's Claude with MCP support can be given access to a Playwright browser context, navigate a live staging site, and write tests by example. This is particularly powerful for regression testing and exploratory scenarios where the exact DOM structure is complex or frequently changes.

In practice, you'd set up an MCP server that wraps Playwright:

typescript
// mcp-server.ts - simplified example
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { CallToolRequestSchema, TextContent } from '@modelcontextprotocol/sdk/types.js';
import { chromium, Browser, Page } from 'playwright';

const server = new Server({
  name: 'playwright-mcp',
  version: '1.0.0',
});

let browser: Browser;
let page: Page;

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request;
  
  if (name === 'navigate') {
    if (!page) {
      browser = await chromium.launch();
      page = await browser.newPage();
    }
    await page.goto(args.url);
    return { content: [{ type: 'text', text: 'Navigated to ' + args.url }] };
  }
  
  if (name === 'click') {
    await page.click(args.selector);
    return { content: [{ type: 'text', text: 'Clicked ' + args.selector }] };
  }
  
  if (name === 'fill') {
    await page.fill(args.selector, args.text);
    return { content: [{ type: 'text', text: `Filled ${args.selector} with "${args.text}"` }] };
  }
  
  if (name === 'screenshot') {
    const buffer = await page.screenshot({ path: args.path });
    return { content: [{ type: 'text', text: 'Screenshot saved' }] };
  }
  
  if (name === 'get_locators') {
    const content = await page.content();
    return { content: [{ type: 'text', text: content }] };
  }
  
  return { content: [{ type: 'text', text: 'Unknown tool' }] };
});

const transport = new StdioServerTransport();
server.connect(transport);

With an MCP setup, you can instruct an AI agent: "Navigate to the product page, add an item to the cart, and write me a Playwright test that reproduces those steps." The agent navigates, observes the DOM changes, captures selectors in real time, and generates working test code—often with better locator strategies than a human might guess.

The trade-off: MCP-driven testing is slower and requires infrastructure setup. It's best for complex flows where you want confidence that selectors actually work, or for teams onboarding to Playwright who need to learn patterns by watching the agent work.

Review generated locators carefully

This is where most teams falter. AI loves XPath like `//*[contains(text(), 'Submit')]` and brittle sibling selectors. Generated locators frequently work on the first run but break after a designer tweaks spacing or a developer refactors the DOM.

Consider this generated test:

typescript
// AI-generated (problematic)
await page.click('xpath=//button[contains(@class, "MuiButton") and contains(., "Sign In")]');
await expect(page.locator('xpath=//*[@id="root"]/div[2]/form/div[1]/input')).toBeVisible();

This works today. In three months, after a CSS framework update or a form redesign, it's useless. Here's the better approach: audit and rewrite locators against your team's standards.

Most teams should adopt a simple rule: prefer data-testid or role-based selectors. Before committing AI-generated tests, replace brittle XPath with robust alternatives:

typescript

// Rewritten (maintainable)
await page.click('button:has-text("Sign In")');
await expect(page.locator('[data-testid="email-input"]')).toBeVisible();

// Or using roles
await page.click('role=button[name="Sign In"]');
await expect(page.locator('role=textbox[name="Email"]')).toBeVisible();

If the app doesn't have data-testid attributes, push back. Add them. A 20-minute conversation with your frontend team saves months of flaky test maintenance. We've seen too many automation projects derail because generated tests were treated as finished code rather than a starting point for hardening locators.

Your review checklist for AI-generated tests:

  • Replace absolute XPath with role selectors or data-testid.
  • Check for hardcoded IDs or auto-generated class names that change per session.
  • Ensure wait strategies are explicit—don't rely on implicit waits.
  • Verify assertions match your actual validation logic, not just UI presence.
  • Look for redundant steps or missing edge cases the AI overlooked.

Keep tests readable and owned by humans

AI-generated code often lacks narrative clarity. A test that passes but reads like a machine wrote it—with cryptic variable names and no comments—becomes toxic to your team. The person who needs to maintain or debug it in six months deserves a test that explains intent, not just actions.

After AI generates a draft, invest 10 minutes in making it readable:

typescript
// Bad: readable to AI, opaque to humans
test('test1', async ({ page }) => {
  await page.goto('https://example.com/login');
  await page.fill('input[type="email"]', 'user@test.com');
  await page.fill('input[type="password"]', 'pass123');
  await page.click('button');
  await expect(page).toHaveURL(/dashboard/);
});

// Good: clear, owned by humans
test('user can log in with valid credentials', async ({ page }) => {
  // Navigate to login page
  await page.goto('https://example.com/login');
  
  // Fill in credentials
  await page.fill('[data-testid="email-input"]', 'user@test.com');
  await page.fill('[data-testid="password-input"]', 'pass123');
  
  // Submit and verify redirect to dashboard
  await page.click('button:has-text("Sign In")');
  await expect(page).toHaveURL(/dashboard/);
});

Better still, extract page objects to hide implementation details and make tests read like business requirements:

typescript
import { Page } from '@playwright/test';

export class LoginPage {
  constructor(private page: Page) {}
  
  async navigateTo() {
    await this.page.goto('https://example.com/login');
  }
  
  async login(email: string, password: string) {
    await this.page.fill('[data-testid="email-input"]', email);
    await this.page.fill('[data-testid="password-input"]', password);
    await this.page.click('button:has-text("Sign In")');
  }
}

// In test
test('user can log in with valid credentials', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.navigateTo();
  await loginPage.login('user@test.com', 'pass123');
  await expect(page).toHaveURL(/dashboard/);
});

This is the difference between code you generated and code you own. When an AI generates page objects with intent-driven methods, your test suite stays readable and maintainable.

Common mistakes

Treating AI output as finished code. Many teams run AI-generated tests, see them pass, and commit them without review. This leads to false confidence and breakage in CI. Always treat the first pass as a draft.

Over-relying on visual AI (screenshot-based locators). Tools that generate locators from screenshots are seductive but fragile. A 1-pixel layout shift breaks them. Stick with structural locators whenever possible.

Losing team knowledge. If only one engineer reviews and cleans up AI-generated tests, the rest of the team never learns the patterns. Code review and pair-review of generated tests is essential.

Not feeding back test failures. When an AI-generated test fails in CI, update the prompt and regenerate. Over time, your prompts get more precise and output improves. Treat it as a learning loop.

Skipping accessibility and performance tests. AI generates happy-path tests by default. Add accessibility checks (await expect(page).toPass(injectAxe())) and performance budgets manually.

Key takeaways

  • Use AI to generate scaffolding and first drafts, not finished tests. Save 30–40% of initial writing time by describing flows clearly.
  • Playwright MCP agents can drive browsers and produce tests from observed behavior—powerful for complex UI but requires infrastructure overhead.
  • Always audit and rewrite AI-generated locators. Replace XPath and brittle selectors with data-testid or role-based approaches your team maintains.
  • Invest 10 minutes post-generation to add comments, clear variable names, and page objects. Readable tests are owned tests.
  • Review generated tests as a team, not solo. Pair with AI output to share patterns and prevent one engineer from becoming the locator translator.
  • Treat AI as a starting point, not a destination. The best test suites combine AI speed with human judgment and domain knowledge.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles