← Back to Blog

Leveraging Playwright in the Age of AI-Assisted Development

AI coding agents can produce and modify code faster than ever. But faster output does not automatically create confidence.

As agents take on more implementation, refactoring, and maintenance work, we need reliable ways to answer two questions:

  1. Does the new functionality work?
  2. Did the change break an existing user flow?

That makes automated testing vitally important in a age where agents are writing more and more code. We need fast feedback loops that tell both developers and their agents when something is wrong—and give them enough information to diagnose, fix, and independently validate the problem.

End-to-end testing with a framework such as Playwright is especially valuable here. Unit tests validate individual pieces of logic. Static-analysis tools such as type checking and linting catch another class of issues. End-to-end tests complement those tools by exercising the application through the same interface a user interacts with.

In this article, we will focus on using Playwright to build feedback loops for AI-assisted development.

Start with the simplest feedback loop

The most direct way to run a Playwright test suite is:

Bash

npx playwright test

This runs the tests and projects configured in your Playwright setup. Tests run headlessly by default, which makes this command well suited for automated agent loops and continuous integration.

For a more visual development and debugging experience, use UI Mode:

Bash

npx playwright test --ui

UI Mode is more than a test runner. It provides watch mode, step-by-step execution, DOM snapshots, and a timeline that lets you move backward and forward through each action. It also displays the trace associated with a test, making it easier to understand what happened before, during, and after a failure.

UI Mode is generally most useful when a human is investigating a problem. For an automated coding-agent loop, the regular headless command is usually the better default.

When you need to record a trace without opening UI Mode, you can run:

Bash

npx playwright test --trace on

The trace can include actions, DOM snapshots, screenshots, console output, and network activity. In continuous integration, Playwright recommends capturing traces on the first retry rather than on every successful test because full tracing adds overhead.

A practical configuration might look like this:

TypeScript

import { defineConfig } from "@playwright/test";

export default defineConfig({
  retries: 1,

  use: {
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "on-first-retry",
  },
});

This gives an agent—or a developer—the evidence needed to investigate a failure without generating unnecessary artifacts for every passing test.

Generate resilient locators

Playwright’s locator picker can generate selectors as you interact with elements on the page.

In UI Mode, select Pick locator, hover over an element in the DOM snapshot, and click it. Playwright adds the generated locator to a playground where you can test and refine it before copying it into your test.

Playwright’s generator prioritizes role, text, and test-ID locators. For example, it will generally prefer:

TypeScript

page.getByRole("button", { name: "Save changes" });

over an implementation-specific selector such as:

TypeScript

page.locator(".settings-form > div:nth-child(3) > button.primary");

The first locator describes the interface from a user’s perspective. The second is coupled to the page’s current HTML and CSS structure. A layout refactor might break the second selector even when the application still works correctly.

Role-based locators also encourage semantic markup and meaningful accessible names. They do not automatically prove that an application is accessible, but they help align tests with the interface exposed to users and assistive technologies. Playwright explicitly recommends testing user-visible behavior and using locators that are resilient to DOM changes.

Generate a starting point with Codegen

Playwright can generate test code while you interact with your application:

Bash

npx playwright codegen http://localhost:3000

This opens a browser alongside the Playwright Inspector. As you click, type, and navigate, Playwright records those interactions and generates corresponding test code.

Codegen can also record several types of assertions, including visibility, text, and input-value checks.

Generated code should be treated as a starting point rather than a finished test. After recording the flow, review it and:

  • Remove unnecessary interactions.
  • Add assertions for the outcomes that actually matter.
  • Replace any fragile selectors.
  • Make the setup and test data deterministic.
  • Give the test a name that explains the behavior being protected.

A generated script that only clicks through the interface is useful for automation, but it is not yet a strong test. The assertions define the contract.

Use Playwright’s test agents

Playwright also provides three specialized test agents:

  • Planner: explores the application and creates a human-readable Markdown test plan.
  • Generator: converts that test plan into executable Playwright tests.
  • Healer: runs failing tests, investigates the current interface, proposes repairs, and reruns the tests.

The agents can be used independently or chained together as an agentic testing workflow.

Agent definitions can be initialized for a supported coding environment. For example, for a Codex-based loop:

Bash

npx playwright init-agents --loop=codex

Playwright also documents initialization options for VS Code, Claude Code, and OpenCode. The generated definitions should be refreshed when Playwright is updated so they include the latest tools and instructions.

A useful workflow is:

  1. Ask the planner to explore a feature and create a test plan.
  2. Review the Markdown plan before generating code.
  3. Ask the generator to turn the approved scenarios into tests.
  4. Run the tests and use the healer to investigate mechanical failures.
  5. Review the resulting tests before adding them to the permanent suite.

The human review between planning and generation matters. It prevents the agent from treating whatever the application currently does as the intended behavior.

The same principle applies to healing. A healer should repair a stale locator, incorrect wait, or test-data problem. It should not quietly weaken an assertion to make broken functionality pass.

A passing test is only valuable when the test still represents the behavior we intended to protect.

Create disposable verification tests for refactors

Not every useful Playwright test has to become part of the permanent test suite.

Suppose an agent is migrating a page to a new frontend framework, replacing a state-management library, or refactoring a large component. Before beginning, you can use Codegen to capture the most important existing flow.

Then add assertions that describe the behavior that must survive the refactor:

TypeScript

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

test("refactor guard: user can complete checkout", async ({ page }) => {
  await page.goto("http://localhost:3000/products");

  await page
    .getByRole("button", { name: "Add Noise-Canceling Headphones to cart" })
    .click();

  await page.getByRole("link", { name: "Cart" }).click();

  await expect(page.getByRole("heading", { name: "Your cart" })).toBeVisible();

  await expect(page.getByText("Noise-Canceling Headphones")).toBeVisible();

  await page.getByRole("button", { name: "Checkout" }).click();

  await expect(page).toHaveURL(/\/checkout/);
  await expect(page.getByRole("heading", { name: "Checkout" })).toBeVisible();
});

First, establish a passing baseline:

Bash

npx playwright test tests/checkout-refactor-guard.spec.ts \
  --project=chromium

Only then should the agent start modifying the implementation.

You can give the coding agent instructions such as:

Refactor the checkout page to the new component architecture. Before changing the implementation, run npx playwright test tests/checkout-refactor-guard.spec.ts --project=chromium and confirm that it passes. After every meaningful code change, run the same test again. Do not remove or weaken its assertions. When it fails, inspect the Playwright trace and fix the production code. Finish only when the Playwright test, type checking, and unit tests all pass.

This creates a closed loop:

text

Change code
    ↓
Run the end-to-end test
    ↓
Inspect the failure and artifacts
    ↓
Fix the implementation
    ↓
Run the test again

The test might be temporary. Once the migration is complete, you can decide whether it protects an important long-term behavior.

If the scenario represents a critical user journey or a regression that could happen again, keep it. If it only verified a one-time migration and duplicates stronger existing coverage, delete it.

The value of a test is not determined by how long it remains in the repository. A disposable test can still provide high confidence during a risky change.

Screenshots are evidence, not necessarily assertions

Screenshots can be helpful when an agent is changing visual components. You can attach one to the test output:

TypeScript

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

test("settings page renders after the refactor", async ({ page }, testInfo) => {
  await page.goto("http://localhost:3000/settings");

  await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();

  await page.screenshot({
    path: testInfo.outputPath("settings-page.png"),
    fullPage: true,
  });
});

This gives the developer or agent something to inspect, but merely producing a screenshot does not validate its contents.

When the visual appearance itself is part of the contract, use a screenshot assertion:

TypeScript

await expect(page).toHaveScreenshot("settings-page.png");

On the first run, Playwright creates a reference image. Later runs compare the rendered page with that reference. Because rendering can vary by browser, operating system, fonts, and environment, visual comparisons should run in a controlled and consistent environment.

For many functional changes, ordinary assertions are more stable and informative:

TypeScript

await expect(page.getByRole("button", { name: "Save changes" })).toBeEnabled();

await expect(page.getByText("Changes saved")).toBeVisible();

Use screenshots to validate visual appearance. Use behavioral assertions to validate functionality. Use traces to understand how a failure happened.

Keep the loop trustworthy

Agent-generated tests should follow the same testing principles as human-written tests.

Focus on user-visible outcomes instead of implementation details. Keep tests isolated so one test does not depend on the data, cookies, or execution order of another. Prefer Playwright’s retrying, web-first assertions instead of arbitrary timeouts.

Most importantly, separate the implementation from the contract.

An agent that is allowed to modify both the application and the test without restrictions can “solve” a failure by changing the expected result. For important workflows, tell the agent that the assertions are fixed unless it first explains why the product requirement itself is incorrect.

The safest agent loop is not:

text

Make the test green by any means necessary.

It is:

text

Preserve this user-visible behavior while changing the implementation.

Playwright as executable context for agents

In AI-assisted development, Playwright tests are more than a final CI gate. They are executable context.

A good test tells an agent:

  • How a user reaches a feature.
  • Which interactions matter.
  • What successful behavior looks like.
  • Which regressions are unacceptable.
  • How to verify its own work.

That is what makes Playwright such a useful complement to coding agents. It turns a vague instruction such as “make sure checkout still works” into a repeatable, observable, and enforceable workflow.