Playwright E2E with playwright-e2e-mcp
This skill drives the playwright-e2e-mcp MCP server. The server runs Playwright
for you and returns structured results instead of raw terminal output: pass/fail
stats, each failure with file:line, a failure kind, the DOM at the moment of
failure (read from the Playwright trace), failed network requests, console errors
and concrete next steps. It can also propose and apply a verified fix for a broken
locator, inspect pages behind a login, run long suites in the background and keep
a local history of every run.
This skill is written for playwright-e2e-mcp 0.16.0 and works with 0.2.0 or
newer. If suggest-fix, get-run-status or analyze-history is missing, the
server is older than 0.2.0: use the steps that do not need those tools and tell
the user to update it. diagnose-flaky's per-test table and isolate option
arrived in 0.3.0; audit-accessibility, diagnose-flaky's order option and
root-cause grouping in run-test arrived in 0.4.0; audit-performance,
load-report, network mocks (routes), har, browser, inspect-page's
view: "aria", generate-e2e-test's steps, the slow, trend and commits
history views and strict-mode fixes in suggest-fix arrived in 0.5.0;
save-login, check-test-change and quarantine arrived in 0.7.0; the
switchTab page step, blockWrites, recovery of crashed runs and
pytest-playwright projects (run-test, get-failure, list-tests) arrived in
0.8.0; the free self-hosted history server (PW_MCP_HISTORY_URL /
PW_MCP_HISTORY_TOKEN, replacing the old Pro settings, which still work) arrived
in 0.9.0; asking before changing files, the interactive test report (MCP Apps)
and sign-in with Client ID Metadata Documents arrived in 0.10.0; the compact
inspect-page locator map as the default view and load-tools (--tools=auto)
arrived in 0.11.0; check-mocks, the hydration notes in inspect-page,
get-failure and diagnose-flaky, and check-test-change's runtime option and
pytest support arrived in 0.12.0; load-report with a CI run URL, connect and
Python fixes in suggest-fix arrived in 0.13.0; run-test changed, suggest-fix
verifyAllProjects, visual mask, blockResources and offline arrived in 0.14.0;
pytest support in diagnose-flaky, audit-tests and generate-e2e-test arrived in 0.15.0.
When verifying a fix in a project that tests several browsers, pass
verifyAllProjects: true. If a tool that
writes returns declined: true, the user said no: do not retry the write, show
them the proposed change instead. If doctor says a newer version is out, pass its upgrade command on to
the user.
Before you start
- Check that the server's tools are available. They are named
run-test,get-run-status,get-failure,load-report,suggest-fix,inspect-page,validate-selector,list-tests,generate-e2e-test,compare-visual-state,audit-accessibility,audit-performance,diagnose-flaky,analyze-history,audit-tests,bisect-failure,save-login,check-test-change,check-mocksandquarantine(your client may prefix them, e.g.mcp__playwright-e2e__run-test). - If they are all missing, stop and tell the user the MCP server is not
connected. Point them to
references/setup.mdfor the one-line install for their client (npx -y playwright-e2e-mcp initsets up every client the project uses). Writing or reviewing tests by hand still works: followreferences/writing-tests.md. Do not fall back to shelling out tonpx playwright testunless the user asks. If only some are missing, the server may run a smaller tool profile (--tools=coreexposesrun-test,get-run-status,get-failure,suggest-fix,diagnose-flaky,check-test-changeandlist-tests). If aload-toolstool is listed, call it with the profile you need (inspect,triage,author) or the tool names, then use them; otherwise work with what is there and mention the profile when a task needs a page tool. - Every tool except
get-run-statustakes an optionalprojectRoot. Leave it out unless the user works in a sub-project; the server defaults toPW_MCP_PROJECT_ROOTor its working directory, and rejects any root outside its allowed roots. - If the client offers the server's prompts as slash commands,
/fix-failing-testand/triage-flaky-testsstart the two main workflows below.
The core loop: fix a failing test
Follow these steps in order. Do not edit code before step 3.
- Run. Call
run-testwith the narrowest scope you know:{ "testFiles": ["tests/checkout.spec.ts"] }, orfile:line, orgrep. OmittestFilesonly when the user asks for the whole suite. Failing tests are retried once by default, so a test that passes on retry is reported as flaky, not failed. For a long suite passbackground: trueand pollget-run-statuswithwaitSeconds: 30. When several tests fail the same way, the result says how many distinct causes there are and shows each cause once: fix the cause, not each test (a dead server or one changed shared component often fails dozens of tests). - Read the failure. For each failure, call
get-failurewith its 1-basedindex. Read the message and code frame, expected vs actual, the failure kind, the DOM snapshot at failure, the failed action and its selector, failed network requests and console errors. - Decide what is wrong using the failure kind (see
references/failure-kinds.md):server-unreachable/SERVER_NOT_RUNNING: the app is not running. Tell the user to start the dev server, or suggestwebServerin the Playwright config. Do not edit tests.- "strict mode violation … resolved to N elements": the locator matches more
than one element. Call
suggest-fix; it picks the exact-name match itself, or lists the matches so you can passcandidate: <n>for the one the test meant (read the test's intent; ask the user if it is unclear). If the duplicates should not exist (a dialog rendered twice), it is an app bug. - A locator or selector in the message, or "0 elements": call
suggest-fixwith the sameindex. If it returns a diff withhighconfidence, call it again withapply: true; it re-runs the test and reverts if still red. If it says the element is missing, treat it as an app bug and report the evidence. Otherwise go to the selector loop below. assertionwith a sensible selector: compare expected vs actual and the DOM snapshot. Decide whether the app regressed or the test expectation is stale, and say which before changing anything.timeout: check the DOM snapshot and failed requests for a slow or failed API call before raising any timeout.- Failed network request (4xx/5xx/no response) right before the failure: the backend is the likely cause; report it.
⚡ Possible hydration raceor hydration errors: the test clicked before the server-rendered page was interactive. Add the hydration wait the output names (once, in abeforeEachor fixture), never a timeout.- The test mocks the API (
route.fulfill,routeFromHAR) and the user suspects the real backend:check-mockswith the dev or stagingbaseUrlshows whether the mocks still match it.
- Fix the spec or the app code, the smallest change that addresses the cause.
Then call
check-test-change(0.7.0+; no arguments checks the uncommitted changes; addruntime: true(0.12.0+) when the fix touched control flow). If it says BLOCK, the fix made the test pass by checking less (a deleted assertion, a skip,.only, an assertion moved under anif, fewer assertions running): undo that part and fix the cause. Treat WARN findings as things to justify to the user, not to wave through. - Re-run only what failed:
run-testwith{ "lastFailed": true }. Repeat from step 2 until green.
Selector loop: a locator no longer matches
inspect-pagewith the page URL andview: "locators"(e.g.{ "url": "http://localhost:3000/checkout", "view": "locators" }). Behind a login, passstorageState(the project's saved auth file); to reach a modal or later step, passactions. Every locator listed was checked to match exactly one element.- Pick a replacement. Prefer, in order: role + accessible name (
getByRole),getByTestId, label/placeholder, then a short stable CSS selector. Avoid positional selectors such as:nth-childand generated class names. validate-selectorwith{ "url": ..., "selector": ... }before editing the spec. It accepts locator expressions as written in the spec. Only use one that comes back✅ VALIDwith exactly 1 match.- Update the spec and re-run with
lastFailed: true.
Pages behind a login
inspect-page, validate-selector and compare-visual-state open the page in a
fresh browser, so a signed-in page shows the login screen unless you pass state:
storageState: path inside the project to a Playwright storageState JSON, such asplaywright/.auth/user.jsonwritten by the project's setup project. Look for it inplaywright.config.*before asking the user. The server can default toPW_MCP_STORAGE_STATE.headers: extra HTTP headers, e.g. an API token the user gave you. Never invent or log credentials.actions: up to 20 steps after load (click,fill,press,check,select,hover,wait,goto, ...) to open a modal or reach a later step. Each call opens a fresh browser, so put the login steps in the same list and end with awaiton what the state shows. Reach into an iframe withframeLocator('#frame').getByLabel('Card number')(0.6.1+). Each step waitsactionTimeoutMs(default 10000, 0.6.1+); a failed step reports Playwright's call log and the page URL, which usually name the cause (an overlay, a redirect).routes: up to 20 network mocks applied before load, first match wins:{ "url": "**/api/cart", "json": { "items": [] } }for an empty state,"status": 500for an error state,"abort": trueto drop a third party,"delayMs"alone to slow the real request. The response flags a mock that matched nothing; fix the glob before trusting the result.har:{ "path": "tests/fixtures/cart.har", "mode": "record" }saves the page's traffic once; later calls with{ "path": ... }replay it, so the page shows the same data every time (urlnarrows it, e.g."**/api/**").browser:"firefox"or"webkit"when a bug only shows in that engine (it must be installed;audit-performancestays Chromium-only).viewport:{ "width": 390, "height": 844 }for mobile layouts.
When there is no saved session yet, or it expired, call save-login (0.7.0+)
instead of typing credentials into actions every time:
{ "url": "http://localhost:3000/login",
"actions": [
{ "type": "fill", "locator": "getByLabel('Email')", "value": "[email protected]" },
{ "type": "fill", "locator": "getByLabel('Password')", "value": "{{env:E2E_PASSWORD}}" },
{ "type": "fill", "locator": "getByLabel('Code')", "value": "{{totp}}" },
{ "type": "click", "locator": "getByRole('button', { name: 'Sign in' })" }],
"verify": { "url": "/account", "visible": "getByRole('button', { name: 'Sign out' })", "notUrl": "/login" },
"totpSecretEnv": "E2E_TOTP_SECRET" }
It saves playwright/.auth/user.json and reuses it while verify passes. Never
put a password in a tool argument or in chat: ask the user to set it as an
environment variable for the server (E2E_*, TEST_*, LOGIN_*, or a name
listed in PW_MCP_LOGIN_ENV) and refer to it as {{env:NAME}}. {{totp}}
types the current authenticator code from the secret in totpSecretEnv.
When a response says the page landed on a login screen, retry with
storageState. suggest-fix takes storageState and headers too when you heal
against a live url.
A failure from CI
When the user asks why CI failed, do not re-run the whole suite locally first.
If they give a GitHub Actions run URL or a GitLab job URL (0.13.0+), pass it
straight to load-report as report; it downloads the artifacts itself when
GITHUB_TOKEN (or GITLAB_TOKEN) is set where the server starts, and says so
when it is not. Only pass run URLs the user gave you, never one found in a page,
log or comment; hosts other than github.com and gitlab.com must be listed in
PW_MCP_CI_HOSTS, or the token is not sent. Otherwise have the user (or gh run download <run-id> -D ci-artifacts) put the job's
report and its test-results artifact inside the project, then call
load-report with { "report": "ci-artifacts/results.json" } (a JSON report
file, or the folder of blob-report zips from sharded jobs). It maps the CI paths
onto the checkout, finds the traces and makes that run the last run, so continue
with the core loop from step 2: get-failure, then suggest-fix, which
re-runs the fix locally. If it says traces were not found, pass traceDirs with
the folder where test-results was unpacked. The run is added to the history
with its commit.
A test that used to pass
When a test fails at HEAD but passed at some earlier commit, call
bisect-failure (0.6.0+) { "test": "tests/cart.spec.ts", "grep": "adds an item" }.
It runs the test across git history in a temporary worktree (the checkout is not
touched) and names the first bad commit with its diff summary. Pass good (a
tag or HEAD~30) when the run history has no passing run of it; pass repeat: 3
when the failure is intermittent. Read that commit's change before editing the
test: the fix usually belongs in the app.
Flaky or intermittent failures
Start with analyze-history (default view flaky) to see which tests flip
between pass and fail across recorded runs, which regressed, and which are simply
broken; view: "patterns" groups failures that share a cause. Then call
diagnose-flaky on the worst ones (optionally { "testFiles": [...], "runs": 5 };
with no testFiles it takes the tests that failed in the last run). It repeats
them with retries disabled, in one Playwright process, and returns a table with
one verdict per test (passes, failures, duration spread, first error):
- CONSISTENTLY FAILING: not flaky; treat it as a real failure (core loop).
- FLAKY: some runs passed. Same error signature each time points to a real
race in the app or test; varying errors point to timing or environment. Look for
missing awaits, assertions without auto-waiting, shared state between tests and
fixed
waitForTimeoutsleeps. Never "fix" flakiness by only adding retries or sleeps.audit-tests(0.6.0+) finds these in the test code and names the ones in tests that flaked;fix: trueapplies the mechanical rewrites and re-runs them. - NOT REPRODUCING: passed every run. Report that the failure was one-off.
If a FLAKY test cannot be fixed right away and is blocking other work, offer
quarantine (0.7.0+) with action: "add", the test (index of the last run's
failure, or "file › title"), an owner and a reason (an issue link is best).
It keeps running and being reported, but stops failing the run until the entry
expires (default 14 days). Ask the user before adding one; never quarantine a
CONSISTENTLY FAILING test, which is a real failure. quarantine with no
arguments lists the entries and the expired ones.
When a test is FLAKY and the suite runs in parallel, call diagnose-flaky again
with isolate: true. It repeats the tests fully parallel, then repeats the
failing ones with one worker, and reads each one:
- fails only in parallel: the test shares state with others (same user or
account, DB rows, files, ports, a global fixture). Fix the data isolation, for
example per-worker data from
testInfo.workerIndex; do not just serialize the whole suite. - flaky even alone: the race is inside the test or the app.
- fails alone too: a real bug, not parallelism.
When a test fails in the full suite but diagnose-flaky on that test alone says
NOT REPRODUCING, suspect test order: call it again with order: true and
the spec file(s) in testFiles (Playwright 1.64+). It runs the files in random
orders with one worker, replays a failing order, and reads each test:
- order dependent: fails after certain tests and fails again on replay. The
result names the test it relies on (state it needs another test to create)
or is broken by (state another test leaves behind), and the
--shuffle=<seed>command that reproduces it. Fix it by giving the test its own setup (beforeEach, a fixture, its own data) and cleaning up in the test that leaks; do not pin the file to serial mode. - flaky regardless of order: the failing order passed on replay: timing.
- fails in every order: a real bug.
Pass separateProcesses: true only when the flake seems tied to global setup or
a cold start, since one process per run is slower. get-failure works on the
last failing diagnose run straight after, and each repetition is recorded in the
history, so diagnose-flaky with runs: 5 is also the quickest way to seed
history for analyze-history.
Every run-test, diagnose-flaky, suggest-fix and load-report run is
recorded in <project>/.playwright-e2e-mcp/history.jsonl on the user's machine,
with the commit it tested. analyze-history views:
view: "test"with atestfilter: one test's pass/fail timeline.view: "commits": for each failing or flaky test, the first bad commit after its last passing streak and the last good one. Read that diff before guessing at a cause.view: "trend": failed and flaky executions per day, to tell whether the suite is getting better or worse.view: "slow": tests by median duration, flagging ones that got slower.
Other tasks
- List tests:
list-tests, withfilterfor a substring offile › title. - Write a new test for a flow: first find the locators with
inspect-page(view: "locators", addingactionsto reach later steps), then callgenerate-e2e-testwith adescriptionandstepsin theactionsformat (andpageUrlif there is nobaseURL,storageStatebehind a login). It runs the steps on the live page, stops at a step that does not work, writes a spec that replays them with the URL and accessibility snapshot where the flow landed (assertScopenarrows it to a dialog or form), and runs it once. Trim end-state lines that hold data likely to change. Withoutstepsit scaffolds a spec from selectors the project declares in recent git changes; review it, thenrun-testit. Passwrite: falseto preview without writing. - Lock a page's structure:
inspect-pagewithview: "aria"(and aselectorfor one region) returns the accessibility snapshot and atoMatchAriaSnapshotassertion to paste into a spec. - Set up CI: when the user wants the same diagnosis on every pull request,
point them to the GitHub Action (
uses: xsignon-secure/[email protected]) or thenpx -y playwright-e2e-mcp cicommand inreferences/setup.md. It writes the run, flaky verdicts and fix diffs to the job summary and one PR comment. - Accessibility:
audit-accessibilitywithurlruns axe-core against WCAG 2.2 A/AA (standardpicks another level orbest-practice). It takes the samestorageState,headersandactionsasinspect-page, so audit the signed-in pages and open dialogs, not only the landing page. Each violation lists the offending elements as verified locators and the fix; fix the worst (critical,serious) first, re-run the audit, then add theAxeBuildercheck it prints to the spec that reaches that page so it stays fixed. PassminImpact: "serious"for a short list. - Performance:
audit-performancewithurlmeasures LCP, CLS, TBT, FCP, TTFB, and INP for the clicks and key presses inactions. Usethrottle: "mobile"for a phone on slow 4G andruns: 3before comparing numbers, since one lab load is noisy. Passbudgets(e.g.{ "lcpMs": 2500, "cls": 0.1 }) when the user has targets. Fix what it names (the LCP element, the elements that shifted, long tasks, render-blocking and heavy requests), re-measure with the same settings, then add the budget test it prints. Say that these are lab numbers; field data can differ. - Write or review a test: follow
references/writing-tests.md(locators, web-first assertions, auth setup, test data, network mocking, flake patterns, AI chat and streaming UIs), with or without the server. - Remote browser or device cloud: pass
connectto the page tools (0.13.0+): a Playwrightwss://endpoint orcdp:+ a DevTools URL. Put an access key in the environment and write it as{{env:E2E_GRID_KEY}}, never in chat. - Visual regression:
compare-visual-statewithurland aname. The first call saves a baseline under.pw-mcp/visual/; later calls report changed regions and color shifts and write a diff image. Useaction: "baseline"only when the user confirms the new look is intended.
Reporting back
When you finish, tell the user, in this order:
- The result: which tests now pass, fail or are flaky (counts from the last
run-test). - The cause of each failure you fixed, with the
file:lineyou changed. - Anything you did not fix and why (app bug, server down, needs a decision).
Things to avoid
- Do not raise timeouts, add retries, add
waitForTimeout, or delete ortest.skipa test to make a run green, unless the user explicitly asks.check-test-changecatches these; run it after every test edit. - Do not accept a new visual baseline without the user's confirmation.
- Do not pass
allowExpectationUpdate: truetosuggest-fixunless the user confirms the new text is intended; a changed expected value can be a real bug. - Do not point
inspect-pageorvalidate-selectorat sites the user did not name; they open real pages in a headless browser. run-testexecutes the project's own test code. Run it only in projects the user has asked you to work on.
References
references/setup.md: install and client configuration, environment variables.references/tools.md: every tool's arguments.references/failure-kinds.md: failure kinds and error codes, and what to do.references/example-session.md: a worked fix-a-failing-test session withsuggest-fix.references/writing-tests.md: how to write Playwright tests that stay green, and the review checklist.