@gowthaman-ravi/pw-recorder
v0.2.0
Published
Playwright-based manual-action recorder with right-click expectations, exposed via CLI and MCP for LLM-driven test generation.
Maintainers
Readme
pw-recorder
Record manual browser actions and right-click expectations, clean them into a per-session artifact, and generate a runnable @playwright/test spec — then replay it with the Playwright runner.
A QA drives a real browser. The tool captures what they do (clicks, fills, selects, hovers, drags, uploads, navigations…) and what they assert (right-click → "Expect visible / text =…"), plus per-action before/after HTML snapshots and per-request network JSON. A post-processing pass removes noise, and codegen turns the cleaned timeline into a real Playwright test.
Exposed two ways: a CLI and an MCP server your coding agent can call.
Install
npm install -g @gowthaman-ravi/pw-recorder # or: npm i -D @gowthaman-ravi/pw-recorder
npx playwright install chromium # one-time browser downloadThis provides two commands: pw-recorder (CLI) and pw-recorder-mcp (MCP server).
The published package already ships the built dist/, so there's no build step.
(If you didn't install globally, prefix with npx, e.g. npx pw-recorder …, or run
npx @gowthaman-ravi/pw-recorder … without installing.)
To hack on the tool instead of just using it, see Development.
Quick start
# 1. Record — opens a real browser; drive it, right-click to add expectations,
# click Stop (or close the window) when done.
pw-recorder launch https://example.com --name checkout
# 2. See the cleaned timeline
pw-recorder show <sessionId>
# 3. Generate a Playwright spec → sessions/<id>/<id>.spec.ts
pw-recorder codegen <sessionId>
# 4. Run it (generates if missing; runs your edits as-is otherwise)
pw-recorder rerun <sessionId>While recording:
- Drive the page normally — clicks, typing, selects, hovers, drag-drop, file uploads, scrolling, and clipboard paste are captured. Navigations are recorded too.
- Right-click any element to add an expectation (visible, text =, contains text, value =, attribute, count).
- Shift + right-click falls through to Chrome's native context menu.
- A small red REC bar (top-right) has the Stop button.
CLI
pw-recorder launch [url] [options] # record a session
pw-recorder list # list sessions
pw-recorder show <id> [--raw] [--json] # print the timeline (cleaned by default)
pw-recorder export <id> [--snapshots] # write actions.normalized.json (+ lean snapshots)
pw-recorder codegen <id> [--validate] [--config <f>] [--print]
pw-recorder rerun <id> [--regen] [--validate] [--headed] [--config <f>]launch options: --name, --out <dir>, --headless, --viewport 1440x900,
--storage-state <file>, --network xhr|graphql|all (default xhr).
Notes:
showprints the normalized (deduped) timeline;--rawshows the raw stream.codegenalways regenerates<id>.spec.ts(overwrites hand edits).rerunruns your existing (possibly hand-edited) spec as-is; pass--regento rebuild it from the recording first.--validateimplies--regen.--validatere-counts semantic locators with Playwright's real engine against the recorded snapshots and drops any it can't resolve (needs browsers).
MCP server
PW_RECORDER_OUT=./sessions pw-recorder-mcp| Tool | Purpose |
|---|---|
| browser_launch | Open a browser, start recording, returns sessionId (networkFilter: xhr/graphql/all) |
| browser_status | Live counts for a running session |
| session_stop | Close browser, finalize artifacts (writes actions.normalized.json) |
| session_list | All sessions on disk |
| session_get | Metadata + actions (raw) and normalizedActions (cleaned) + network index |
| session_export | Write actions.normalized.json (and, with snapshots, lean snapshots) |
| session_codegen | Generate <id>.spec.ts and return it (validate, config) |
| session_rerun | Run the spec (regen to rebuild, validate, headed, config) |
Wiring into an agent
{
"mcpServers": {
"pw-recorder": {
"command": "npx",
"args": ["-y", "@gowthaman-ravi/pw-recorder", "pw-recorder-mcp"],
"env": { "PW_RECORDER_OUT": "/abs/path/to/your-repo/recordings" }
}
}
}(If installed globally you can use "command": "pw-recorder-mcp" with no args.)
Then: "Use pw-recorder to launch a browser on staging so I can record a test."
The agent calls browser_launch, you drive + add expectations, it calls
session_stop, then session_codegen / session_rerun.
Normalization
The raw recording is noisy, so codegen consumes a cleaned, causally-ordered
view (normalizedActions), derived non-destructively — the raw actions.json is
never modified. session_stop also persists it as actions.normalized.json.
Passes: sort by sequence; drop scrolls (Playwright auto-scrolls); drop a clipboard paste a later fill reproduces; drop focus-clicks (a click whose element is then filled); collapse redirect chains and drop gesture-induced navigations (a navigation a click/press triggered — even with a hover/expect in between — since Playwright waits for it automatically); drop a hover that's just the mouse moving to a click/expect target.
Codegen
codegen / rerun produce a real @playwright/test spec:
- Locators, best candidate wins. Each element records candidates
(
getByTestId→getByLabel/getByPlaceholder/getByRole→getByText→#id/[name]→ css → xpath) each with a match count. Codegen picks the highest-priority candidate that's unique, and for form controls prefers the attribute-reliable label/placeholder over an ARIA-computed role name. - Exact matching. Text locators emit
{ exact: true }sogetByText('Location')doesn't also match'Location Launcher'. - Scoped locators. When a semantic locator is ambiguous but unique inside a
stable container, codegen scopes it — e.g.
page.locator('.chakra-modal__footer').getByRole('button', { name: 'Save', exact: true })instead of a brittle nth-of-type css chain. - Hoisted variables. Every locator becomes a
const locatorNNN = …at the top of the test, deduped and reused; scoped chains are split so the container is its own reusable const. - Value consts. Fill/paste values become named consts (
const password = …) and are reused everywhere they appear, including in assertions. - Loader waits. Where a step's snapshot shows a configured loading indicator,
codegen inserts
await locator.first().waitFor({ state: 'detached', timeout })so the test waits for spinners/skeletons to clear. --validateverifies locators against Playwright's real engine.
Configuration
Loader selectors and their timeout come from an optional config file, discovered
via --config <path>, the PW_RECORDER_CONFIG env var, or
pw-recorder.config.(yml|yaml|json) found by walking up from the working dir.
See pw-recorder.config.example.yml.
# User loaders EXTEND the built-in defaults (they don't replace them):
# .chakra-spinner .chakra-skeleton .ant-spin .ant-spin-spinning
# .ant-skeleton [data-loading] [aria-busy="true"]
loaders:
- .my-app-spinner
- '[data-busy]'
loaderTimeout: 10000 # ms to wait for a loader to leave the DOM (default 10000)Network capture
By default XHR + fetch requests are persisted (--network xhr), which
includes GraphQL; documents, scripts, styles, images and fonts are ignored.
Choose graphql for GraphQL-only or all for everything. GraphQL requests are
detected (URL, JSON query body incl. batches, application/graphql, or a GET
query param) and their operationName / operationType are stored and used in
the filename (0001-POST-host-Login.json).
Session layout
sessions/<sessionId>/
meta.json # url, viewport, ua, counts, timestamps
actions.json # raw ordered timeline (actions + expectations)
actions.normalized.json # cleaned, causally-ordered timeline (codegen input)
<sessionId>.spec.ts # generated @playwright/test spec
snapshots/ # <id>.before.html / <id>.after.html per action
network/ # one JSON per request: 0001-POST-host-path.jsonactions.json entries are typed; type:"expect" entries carry an expect block
(kind, expected, actual). Selectors carry primary plus best-first
candidates (each with a matchCount) for robust generation.
Architecture
The browser is a real Playwright persistent context; the QA interacts with
it directly. The recorder is injected via addInitScript. Match counts for semantic
locators are computed in-page at capture; --validate refines them at codegen
time with Playwright's real selector engine against the stored snapshots.
Development
Working on the tool itself (from a clone of this repo):
npm install
npm run setup # playwright install chromium
npm run build # tsc → dist/
# run the CLI from source (equivalent to the pw-recorder bin):
npm run cli -- launch https://example.com --name checkout
npm run codegen -- <sessionId>
npm run rerun -- <sessionId>
npm run verify:offline # browser-free unit checks (candidates, normalize, codegen, config)
npm run verify # full headless end-to-end pipeline check (needs browsers)