AI will happily write you a Playwright test in ten seconds. The trouble shows up in week three, when half the suite is flaky, a quarter asserts nothing useful, and nobody remembers what the tests were meant to prove. This guide is about writing Playwright tests with AI in a way that survives code review: let the model look at the real app, make it commit to assertions, and keep it out of your CI loop.
Explore with Playwright MCP, write a plan in plain Markdown, generate tests from the plan, run them with plain Playwright, and have a human review every test and every “heal”. The AI writes. Playwright runs. You decide what is correct.
npx playwright init-agents --loop=claude on Playwright 1.65.0 and read every file it generated. I wrote the example test below myself, using most of its locators from what Playwright MCP returned while driving the TodoMVC demo, then ran it with Playwright’s test runner. I did not run the planner, generator and healer agents end to end with a model, so descriptions of their behaviour come from their generated definitions and the official docs, not from my own sessions with them.Why AI-written tests fail, and the fix for each
Ask a chat model to “write a Playwright test for login” without letting it see your app, and you get a confident guess. The failures are predictable:
| Failure | Why it happens | Workflow fix |
|---|---|---|
| Invented selectors | The model never saw the DOM, so it guesses #submit-btn | Generate against the live app so locators come from real snapshots |
| Vacuous assertions | It asserts that a click happened, not that something changed | Require an explicit expected result for every step in the plan |
| Assumed behaviour | It writes what a login usually does, not what yours does | Write the expectations in a plan you review before any code exists |
| Brittle waits | Hard-coded sleeps copied from old tutorials | Rely on Playwright’s auto-waiting and web-first assertions; reject waitForTimeout in review |
| Silent “healing” | An agent changes the expected value so the test passes | Review every healer diff, and tell it never to edit expectations without asking |
The five-stage workflow
- Explore. Let an agent drive the app through Playwright MCP and report what it finds. Every action comes back with the Playwright code that performed it.
- Plan. (You review.) Turn the exploration into a Markdown test plan with steps and expected results. This is the cheapest place to catch a wrong assumption.
- Generate. Produce one test per plan item, executed live against the app so locators and assertions are verified as they are written.
- Run. Execute with plain
npx playwright test. No model involved. - Heal and review. (You review.) When something fails, diagnose first. Fix locator drift with an agent if you like, but a human decides whether the product or the test is wrong.
Option A: Playwright Test Agents (planner, generator, healer)
Playwright ships three agents that implement exactly that loop, for VS Code, Claude Code, opencode and Codex. Set them up inside an existing Playwright project:
npx playwright init-agents --loop=claude
# other loops: --loop=vscode, --loop=opencode, --loop=codexHere is the real output from that command on my test project:
🎭 Using project "" as a primary project
📝 specsREADME.md - directory for test plans
🤖 .claudeagentsplaywright-test-generator.md - agent definition
🤖 .claudeagentsplaywright-test-healer.md - agent definition
🤖 .claudeagentsplaywright-test-planner.md - agent definition
🔧 .mcp.json - mcp configuration
✅ Done.What it actually installs
Three agent definitions, a specs/ folder for plans, and an .mcp.json. The detail most guides miss is that the .mcp.json registers a separate MCP server named playwright-test, started with npx playwright run-test-mcp-server. It is not the same as the general-purpose @playwright/mcp server. It exposes the browser tools plus test-specific ones. On Windows, the generated config wraps npx in cmd /c:
{
"mcpServers": {
"playwright-test": {
"command": "cmd",
"args": ["/c", "npx", "playwright", "run-test-mcp-server"]
}
}
}| Agent | Job | Special tools in its definition | Output |
|---|---|---|---|
| Planner | Explores the app and writes a Markdown test plan. Told to prefer snapshots and avoid screenshots | planner_setup_page, planner_save_plan | A plan in specs/ |
| Generator | Executes each plan step live, reads the log of what ran, then writes one test per file with a comment before each step | generator_setup_page, generator_read_log, generator_write_test, plus browser_verify_* assertions | Test files in tests/ |
| Healer | Runs the suite, debugs failures, edits the test, reruns | test_run, test_debug, test_list, browser_generate_locator | A passing test, or a skipped one if the feature looks broken |
Two practical notes from reading the generated files. First, each definition pins model: sonnet in its header. That is a setting you can change, so pick the model you can afford for long exploration. Second, regenerate the definitions after you upgrade Playwright, since the docs say they pick up new tools and instructions that way.
The seed test matters more than it looks
The planner and generator both start from tests/seed.spec.ts. It gives the agents a ready page and runs whatever setup your tests need: fixtures, global setup, project dependencies, hooks. If your app needs login, seed is where login lives. Mine was a single line:
import { test } from '@playwright/test';
test('seed', async ({ page }) => {
await page.goto('./');
});Prompts that work with the three agents
# Planner
Using tests/seed.spec.ts as the starting point, explore the guest
checkout flow and save a test plan to specs/guest-checkout.md. Include
at least two negative scenarios and an expected result for every step.
# Generator
Generate tests from specs/guest-checkout.md. One test per scenario.
Use role and label locators, no hard-coded waits, and a web-first
assertion after every state-changing step.
# Healer
Run the suite and fix failing tests. Before changing any expected
value, stop and tell me what the app actually did and why you think
the test is wrong. Never delete an assertion to make a test pass.Option B: A lighter setup with Playwright MCP alone
You don’t need the full agent trio to get most of the benefit. If you already have Playwright MCP installed, start it with the testing capability:
claude mcp add playwright -- npx @playwright/mcp@latest --caps=testingThat adds five tools to the default 25 (30 in total, as I counted on 0.0.84): browser_generate_locator and four assertions, browser_verify_element_visible, browser_verify_text_visible, browser_verify_list_visible and browser_verify_value. I called the locator generator on the TodoMVC input and it returned this:
getByRole('textbox', { name: 'What needs to be done?' })That’s a role-based locator, which is what you want in a test that has to survive a redesign. Then prompt for the artifact directly:
Explore the todo list: add two items, complete one, filter by Completed,
and try submitting an empty item. Then write a single Playwright spec in
TypeScript using only locators you actually used, with an assertion
after each step. Do not use waitForTimeout.Use Option A for a growing suite where plans live in version control and several people contribute. Use Option B for a quick spec on one flow.
A worked example: TodoMVC
This is the plan-to-test step on the public Playwright demo. First, a plan I wrote in the shape the generator’s own example uses (a seed reference and numbered steps), plus an expected result for each scenario:
### 1. Add and complete todos
**Seed:** `tests/seed.spec.ts`
#### 1.1 Add todos and see the remaining count
**Steps:**
1. Type "Buy milk" in "What needs to be done?" and press Enter
2. Type "Write article" and press Enter
**Expected:** both items are listed in order; the footer says "2 items left"
#### 1.2 Complete a todo
**Steps:**
1. Add "Buy milk" and "Write article"
2. Tick "Buy milk"; open the Completed filter
**Expected:** the footer says "1 item left"; Completed shows only "Buy milk"
#### 1.3 Empty input is rejected (negative case)
**Steps:**
1. Focus the input and press Enter with nothing typed
**Expected:** no todo is createdAnd the test that came out of it. The textbox, checkbox and link locators are the ones MCP reported; the todo-title test id is one I added myself from the demo app’s markup. Every scenario ends in a web-first assertion:
import { test, expect } from '@playwright/test';
test.describe('TodoMVC: add and complete', () => {
test.beforeEach(async ({ page }) => {
await page.goto('./');
});
test('adds a todo and shows the remaining count', async ({ page }) => {
const input = page.getByRole('textbox', { name: 'What needs to be done?' });
await input.fill('Buy milk');
await input.press('Enter');
await input.fill('Write article');
await input.press('Enter');
await expect(page.getByTestId('todo-title')).toHaveText(['Buy milk', 'Write article']);
await expect(page.getByText('2 items left')).toBeVisible();
});
test('completing a todo updates the count and the Completed filter', async ({ page }) => {
// ...same two inserts...
await page.getByRole('listitem').filter({ hasText: 'Buy milk' })
.getByRole('checkbox', { name: 'Toggle Todo' }).check();
await expect(page.getByText('1 item left')).toBeVisible();
await page.getByRole('link', { name: 'Completed' }).click();
await expect(page.getByTestId('todo-title')).toHaveText(['Buy milk']);
});
test('empty input does not create a todo', async ({ page }) => {
await page.getByRole('textbox', { name: 'What needs to be done?' }).press('Enter');
await expect(page.getByTestId('todo-title')).toHaveCount(0);
});
});The result, run with npx playwright test:
ok 1 testsseed.spec.ts:2:1 › seed (1.4s)
ok 2 teststodo.spec.ts:8:3 › TodoMVC: add and complete › adds a todo and shows the remaining count (1.4s)
ok 3 teststodo.spec.ts:19:3 › ... › completing a todo updates the count and the Completed filter (717ms)
ok 4 teststodo.spec.ts:33:3 › ... › empty input does not create a todo (809ms)
4 passed (4.5s)The failure that no healer should be asked to fix
My first run did not look like that. It looked like this: 3 failed, 1 passed (1.6m). Every failing test timed out waiting for getByRole('textbox', { name: 'What needs to be done?' }), an element I had just proved existed.
The locator was fine. The bug was in my config: baseURL was https://demo.playwright.dev/todomvc and my tests called page.goto('/'). A leading slash resolves from the site root, so the tests were loading the Playwright demo homepage, not TodoMVC. The fix was a trailing slash on baseURL and goto('./') in the tests.
The lesson applies to every AI workflow. A healer given those three failures would see “locator not found” and start rewriting locators, and it might even find some other element that makes the test pass on the wrong page. Diagnose the failure before you hand it to an agent. Open the trace or error context and ask: is the page the one I expect?
The review checklist for AI-written tests
I run this against every generated test before it merges:
| Check | What to look for | Example fix |
|---|---|---|
| Does it assert an outcome? | A test that ends after a click proves nothing | Add toBeVisible, toHaveText or toHaveURL on the result |
| Is the assertion web-first? | expect(await el.isVisible()).toBe(true) checks once and can flake | await expect(el).toBeVisible() retries until timeout |
Any waitForTimeout? | Fixed sleeps are either too short or wasteful | Remove it; wait on the state you care about |
| Are locators semantic? | Long CSS or XPath chains break on redesigns | Use getByRole, getByLabel or getByTestId |
| Is it independent? | Tests that rely on another test’s data fail when run alone or in parallel | Create data in beforeEach; keep each test self-contained |
| Is there a negative case? | AI drifts toward happy paths | Add empty input, invalid input, server error |
| Does the test prove what the plan said? | Generated code can quietly drop an expected result | Check each plan bullet maps to an assertion |
| Did a heal change an expectation? | The most dangerous diff in the repo | Reject unless the product really changed |
Using the healer without letting it hide bugs
Per the docs, the healer replays the failing steps, inspects the current UI for an equivalent element, suggests a fix such as a locator update, a wait adjustment or a data fix, and reruns. If it decides the functionality itself is broken, it can leave the test skipped. That’s the right behaviour, but it means a “green” or skipped suite can hide a real regression.
- Healer fixes belong to a narrow class. Renamed button, moved element, changed accessible name, a timing issue. Anything that changes an expected value is a product decision.
- Read the diff, not the summary. The healer’s explanation tells you what it believes. The diff tells you what it did.
- Track skips. Fail the build on skipped or
fixmetests you didn’t approve, or the suite quietly erodes.
Keep the model out of CI
Generation and healing are authoring-time activities. In CI, run ordinary Playwright:
// playwright.config.ts
export default defineConfig({
retries: process.env.CI ? 2 : 0,
use: { trace: 'on-first-retry' },
});Why: a suite that calls a model on every run is slow, costs money per commit and can decide differently each time. A suite of committed test files is deterministic, reviewable and free to run. When a test fails in CI, the trace shows what happened. Then you decide whether to invoke the healer on your machine.
Playwright AI tools at a glance
| Tool | Use it to | Needs a model? |
|---|---|---|
npx playwright codegen | Record clicks into a script as you use the app | No |
Playwright MCP (--caps=testing) | Let an agent explore and produce locators and assertions | Yes |
| Playwright CLI with skills | Give a shell-enabled coding agent browser commands | Yes |
Test Agents (init-agents) | Plan, generate and heal tests in a managed loop | Yes |
npx playwright test | Run the committed suite | No |
Not sure whether MCP or the CLI suits your agent? The comparison guide covers it.
Where AI test writing still breaks
- Authentication. Put login in the seed test or a setup project and reuse saved storage state. Don’t let the model log in through the UI in every test.
- Dynamic data. Timestamps, IDs and randomized content make exact-text assertions flaky. Assert on patterns, or control the data.
- Visual and canvas UI. The accessibility snapshot can’t see a chart. Use screenshot comparison or the vision capability for those.
- Third-party widgets and iframes. Payment frames and captchas need purpose-built handling or mocks.
- Business rules. The AI knows what the page does, not what it should do. Your plan review is the only place that gets checked.
Get Playwright MCP running, then try the workflow on one flow
Set up the server, explore one real flow, review the plan, then generate and run the tests. A single well-reviewed flow teaches you more about your app’s testability than a hundred generated specs.
Set up Playwright MCP → Related: Playwright CLI vs MCP vs Chrome DevTools MCPQuestions about writing Playwright tests with AI
Can AI write reliable Playwright tests?
It can write good first drafts when it works against the live app and you review the result. It writes unreliable tests when it guesses at selectors and behaviour. The difference is process: explore, plan, generate against the real page, review, then run without a model.
Do I need Playwright MCP for the Test Agents?
Not the general @playwright/mcp server. init-agents registers its own playwright-test MCP server, started with npx playwright run-test-mcp-server. You can run both side by side.
Which AI tools does init-agents support?
The docs show VS Code (v1.105 or later for the agentic experience), Claude Code and opencode, and list Codex as a loop option. Pick one with --loop.
Will the healer fix a real bug?
No. It repairs tests, not products. It can fix a changed locator or a timing issue, and it may skip a test when the feature itself looks broken. Deciding whether the app or the test is wrong stays with you.
How do I stop AI-generated tests from being flaky?
Ban fixed waits, prefer role and label locators, use web-first assertions that retry, keep each test independent, and review every heal. Retries plus trace: 'on-first-retry' in CI help you diagnose the flakiness that remains.
Should the model run in my CI pipeline?
No. Use it to author and repair tests on a developer machine. CI should run committed test files with plain npx playwright test.

1 comment
Comments are closed.