Playwright Healer Found the Bugs, Then Skipped the Tests

Table of Contents
4 passed, 2 skipped, exit code 0. That’s what the Playwright healer agent left behind in three of my five demo runs, each time after it correctly diagnosed two real regressions, down to the offending line of app code, and then marked both tests test.fixme(). The healer wasn’t wrong about anything. It did exactly what its prompt tells it to do, and the build went green with two broken features in it.
On October 30, 2026, Playwright maintainer Dmitry Gozman told a user proposing a triage agent: “nowadays you are probably better off asking your agent to fix the tests instead of using the healer at all” (microsoft/playwright#43675). I took that literally. I ran the healer, a plain “fix the tests” request, and a coding agent with a triage prompt against the same five failures, five runs each. Gozman is right, with one condition the issue thread doesn’t spell out: a coding agent only beats the healer if you tell it that red is an acceptable answer.
Why this isn’t another “the healer heals wrong” post
I’ve already covered the healer’s failure modes when it guesses wrong: binding to a similar-but-wrong element, papering over race conditions in parallel suites, and measuring heal precision in shadow mode before you trust it. All three assume the problem is a bad diagnosis.
This post is about the case where the diagnosis is perfect. When the healer correctly concludes that the app is broken, its designed output is a skipped test. That’s in the official Test Agents docs: the healer’s output is “a passing test, or a skipped test if the healer believes that functionality is broken.” A skipped test doesn’t fail your pipeline. So the better the healer gets at spotting real bugs, the more coverage it quietly removes.
What did the Playwright team actually say about the healer?
They declined to build a smarter healer and told users to ask a general coding agent instead. The proposal in #43675 was sensible: run a triage agent before the healer that returns one of four verdicts (Regression, Stale test, Stale spec, Environment or flaky) and only let the healer touch stale tests.
Gozman’s full reply: “We discussed this one, and it’s unlikely we can provide anything useful on top of modern models. Just ask your agent to classify things directly. In fact, nowadays you are probably better off asking your agent to fix the tests instead of using the healer at all.” The issue was closed the same day.
That matches the commit history. The healer template was last touched in April 2026, and Playwright 1.65 still ships it with the fixme instruction intact. Meanwhile releases 1.61 through 1.65 put their AI work into the layers underneath: the bundled CLI and MCP server, agent skills, and aria snapshots.
Why does the Playwright healer mark tests test.fixme()?
Because its prompt tells it to, and it’s told not to ask you first. Here are the two lines that matter, straight from the template that npx playwright init-agents --loop=claude writes into your repo on 1.65.0:
- If the error persists and you have high level of confidence that the test is correct, mark this test as test.fixme() so that it is skipped during the execution. Add a comment before the failing step explaining what is happening instead of the expected behavior.- Do not ask user questions, you are not interactive tool, do the most reasonable thing possible to pass the test.“I’m confident the test is correct” is the exact situation where the app has a regression. The instruction for that situation is to skip the test, non-interactively, with a code comment as the only record. And a fixme’d test is invisible to the one signal most teams act on:
npx playwright test# 2 skipped# 4 passed (1.9s)echo $?# 0The JSON reporter records it as "status": "skipped" with a fixme annotation, so the information exists. Nothing reads it unless you make something read it.
How I set up the healer vs coding agent demo
A tiny helpdesk app with six Playwright tests, all green on v1. Then a v2 “release” broke five of them for three different reasons, with a CHANGELOG.md describing only the intentional changes:
| Test | Why it fails on v2 | Correct outcome |
|---|---|---|
| creates a ticket | Button renamed “Submit” → “Create ticket” (in changelog) | Update locator |
| shows the open ticket count | Text changed to “Open: 2” (in changelog) | Update expected text |
| assigning a ticket shows the assignee | Save is now async; test does a one-shot textContent() read | Web-first assertion |
| urgent tickets get the urgent badge | Bug: handler reads f.get('Priority'), field is priority | Stay red, report |
| closing a ticket moves it to the closed list | Bug: a “performance” continue stops closed tickets rendering | Stay red, report |
Three arms, five fresh copies each, all on Claude Code 2.1.319 with --model sonnet (the healer’s own frontmatter also pins sonnet), Playwright 1.65.0:
- Healer:
npx playwright init-agents --loop=claude, then “Use the playwright-test-healer agent to fix the failing Playwright tests in this repo.” - Plain coding agent: no healer installed. “Some Playwright tests in this repo are failing (run
npx playwright test). Fix them.” - Triage coding agent: no healer. The prompt below.
One asymmetry: only the healer arm had Playwright’s MCP browser tools (that’s how the healer is packaged); the other two arms could only read files and run npx playwright test. After each run I re-ran the suite myself, counted skips, and diffed both tests/ and app/.
What happened when the healer and a coding agent fixed the same tests?
The healer either skipped both regressions or patched the app, the plain agent patched the app every time, and only the triage prompt left the two broken features red. Every arm got the easy part right: in all 15 runs, the locator rename, the counter text, and the async assertion were fixed correctly, and no run deleted or loosened the two assertions guarding the real bugs. Where they split was what each agent did once it understood the app was broken.
| Arm (5 runs each) | Fixme’d both bugs | Patched app code | Left 2 tests red | Build exit code | Wall time |
|---|---|---|---|---|---|
| Healer | 3 | 2 | 0 | 0 in every run | ~65 s |
| Plain “fix them” | 0 | 5 | 0 | 0 in every run | ~55 s |
| Triage prompt | 0 | 0 | 5 | 1 in every run | ~103 s |
5/5
Diagnosed both bugs to the line
6
Regression tests skipped in total
0
Runs that ended with a red build
An earlier pilot of three runs per arm had the same shape: the healer fixme’d both bugs twice and left them red once, the plain agent patched the app three times, and the triage prompt left them red three times.
The healer’s diff is a perfect bug report, filed as a skip
This is from healer run 1, trimmed to the two regression tests. The comments are accurate root-cause analysis. The .fixme underneath them is what turns that analysis into a green build.
test('urgent tickets get the urgent badge', async ({ page }) => {// App bug: the submit handler reads f.get('Priority') but the select is named// 'priority', so the chosen priority is dropped and every new ticket becomes 'Normal'.test.fixme('urgent tickets get the urgent badge', async ({ page }) => { await page.getByLabel('Title').fill('Server room is on fire'); await page.getByLabel('Priority').selectOption('Urgent'); await page.getByRole('button', { name: 'Submit' }).click(); await page.getByRole('button', { name: 'Create ticket' }).click();
test('closing a ticket moves it to the closed list', async ({ page }) => {// App bug: render() does `if (!t.open) continue;`, so closed tickets are never// drawn into #closed; the ticket just disappears.test.fixme('closing a ticket moves it to the closed list', async ({ page }) => {In a PR titled “fix failing tests”, a reviewer skims those comments, sees a green check, and merges. Now the bug has a code comment instead of a ticket.
The healer and the plain agent both edited product code
The other two healer runs didn’t skip anything. They edited app/index.html and fixed both bugs, which is surprising for a subagent named test healer. Its tool list includes Edit and Write with no path restriction, so nothing stops it. The plain coding agent did the same thing in all five runs.
To be fair, the two-line app fixes were correct. In a toy app with an obvious typo, that’s arguably the best outcome. In a real repo it means a “fix the tests” session shipped product changes that nobody reviewed as product changes. Both arms treated green as the definition of done. The healer just has two ways to get there.
How should you ask a coding agent to fix failing Playwright tests?
Give it the classification step Gozman mentioned, plus an explicit statement that red is an acceptable result. This is the exact prompt the triage arm ran. It took nearly twice as long as the plain prompt, and it was the only arm that produced a red build with a correct diagnosis every time:
Run `npx playwright test`. Classify every failing test before you touch it:- STALE TEST: the app changed on purpose (CHANGELOG.md says so). Update the test.- TIMING: the test reads state before the app settles. Switch to a web-first assertion.- REGRESSION: the app no longer does what the test expects and CHANGELOG.md does not explain it. Do not edit the test. Leave it red.- ENV: network, credentials, infrastructure. Do not edit the test.Rules: never add test.skip, test.fixme or test.fail. Never delete or loosen anassertion to get to green. Never edit application code.Finish with a table: test | verdict | evidence | change made.Two details carry most of the weight. First, the prompt names a source of intent. “The app changed on purpose” is undecidable from a page snapshot; my changelog made it decidable, and in your repo it might be the ticket, the PR description, or a spec file. Second, the prompt bans the escape hatches by name. “Don’t hide bugs” is too vague; “never add test.fixme” is checkable.
This is the same line I drew in letting AI triage your Playwright failures without deciding them: the agent gets to classify and propose, and a REGRESSION verdict ends with a human, not a skip.
How do you stop test.fixme() from silently shrinking coverage?
Fail the build when the skip count goes over an agreed budget. Playwright has --fail-on-flaky-tests but nothing equivalent for skips, so read the JSON reporter yourself. This is the gate I ran against every demo run:
import { readFileSync } from 'node:fs';
const report = JSON.parse(readFileSync('results.json', 'utf8'));const budget = Number(process.env.SKIP_BUDGET ?? 0);const skipped = [];
const walk = (suite) => { for (const spec of suite.specs ?? []) for (const t of spec.tests) if (t.status === 'skipped') skipped.push(`${spec.file}:${spec.line} ${spec.title} [${t.annotations.map(a => a.type).join(',')}]`); (suite.suites ?? []).forEach(walk);};report.suites.forEach(walk);
console.log(`skipped: ${skipped.length} (budget ${budget})`);skipped.forEach((s) => console.log(` - ${s}`));if (skipped.length > budget) process.exit(1);It needs reporter: [['list'], ['json', { outputFile: 'results.json' }]] in your config. Against healer run 1 it turns the green build red and names both tests:
node skip-gate.mjs# skipped: 2 (budget 0)# - tickets.spec.ts:19 urgent tickets get the urgent badge [fixme]# - tickets.spec.ts:29 closing a ticket moves it to the closed list [fixme]echo $?# 1Set SKIP_BUDGET to today’s count, not zero, if your suite already carries legitimate skips; the point is that the number can’t grow without someone changing it in a reviewed commit. For the app-code problem, the cheapest check is a path filter: a branch whose purpose is test repair shouldn’t touch src/ (or your equivalent), and git diff --name-only will tell you if it did.
Is there still a case for the Playwright healer?
A narrow one. If your team lives in VS Code with Copilot and nobody is going to write a prompt file, the healer is a packaged loop that gets locator drift right; in my Claude Code runs it fixed the stale locator every time. But you’d still need the skip gate, because the healer’s skip behaviour is documented and intentional. Once you’re writing guardrails around it anyway, a ten-line prompt for the coding agent you already use is less machinery.
My recommendation is to skip the healer. Keep the classification, make red a legal answer, and gate on skips.
Your next step
Add the JSON reporter and skip-gate.mjs to your CI today, with SKIP_BUDGET set to your current skip count. Then grep your suite for test.fixme( and check how many of them carry a comment that reads like a bug report. Each of those is a bug you’ve already found and haven’t filed yet. If you want to know whether the tests that do run would catch a real change, mutation testing your AI-generated tests is the next check.
FAQ
Does the Playwright healer agent skip failing tests?
Yes, by design. The Test Agents docs say the healer outputs “a passing test, or a skipped test if the healer believes that functionality is broken.” Its prompt tells it to mark the test test.fixme() when it’s confident the test is correct and the error persists.
Does test.fixme() fail the Playwright run?
No. A fixme’d test is reported as skipped, and npx playwright test exits 0 if nothing else failed. The JSON reporter records "status": "skipped" with a fixme annotation, so you can gate on it yourself with a script like skip-gate.mjs above.
What did the Playwright team say about the healer?
On October 30, 2026, maintainer Dmitry Gozman wrote in microsoft/playwright#43675 that “you are probably better off asking your agent to fix the tests instead of using the healer at all,” and declined to build a separate triage agent.
Is a coding agent better than the healer at fixing Playwright tests?
Only with the right prompt. In my demo, a plain “fix them” request patched application code in all five runs to reach green. A prompt that required classification and allowed tests to stay red produced the correct red build, with diagnoses, in all five runs.
How do I fail CI when tests are skipped in Playwright?
Playwright has --fail-on-flaky-tests but no equivalent for skips. Add the JSON reporter, count tests with status: "skipped", and exit non-zero when the count exceeds a budget you keep in version control.
