Distilled from GH#96 — a CHEAP verification tool (< 5s per run, no LLM) and the ONLY way to interact
with localhost/file:// when claude-in-chrome is blocked for these 2 schemes on the machine.
WHAT
Purpose and context
- Purpose: verify code quickly, one-off, using Playwright run via a standalone
.mjs+node(no test runner): capture screenshotslocalhost/file://, catch console/pageerror/http/requestfailed errors, measure real elements, auth bypass; for multi-step work use a persistent sessionconnectOverCDP. - Trigger (when to use):
claude-in-chromereportsFrame with ID 0 is showing error pageorCan't interact with browser-internal or unparseable URLsonlocalhost:*/file://.- Need to diagnose a UI element that is "not visible" / "not clickable" — a screenshot is not enough, real measurements are needed (
getBoundingClientRect,getComputedStyle). - Need to verify 1 code hypothesis and then throw it away (not worth creating
*.spec.ts+ test runner). - Need to test via dev-login/fake session instead of real SSO login every time.
- User says 'verify with playwright', 'screenshot the page', 'quickly test UI with a script', 'verify multiple steps', 'keep the session between runs', 'cannot click / cannot see the element', or
/playwright-verify.
- User says 'verify with playwright', 'screenshot the page', 'quickly test UI with a script', 'verify multiple steps', 'keep the session between runs', 'cannot click / cannot see the element', or
- Non-goals: does not create
*.spec.ts+ test runner (npx playwright test), is not a UAT test case suite (→ uat-nonit-testcase), does not replace claude-in-chrome for normal external websites, does not commit scripts into the target repo.
Mental model
giả thuyết cần verify → (đã cài?) → script .mjs một-phát: launch → gắn 4 kênh lỗi → goto (load + wait) → screenshot/đo → close → đọc log + ảnh → vứt. Multi-step branch: một Chrome CDP riêng (profile tạm) ← mỗi lượt step-N.mjs bám vào, state sống giữa các lượt → pkill khi xong.
Input and output contract
| Field | Required? | Meaning | |
|---|---|---|---|
| In | URL (localhost:* / file:// / other) |
yes | the page to verify |
| In | project has @playwright/test in node_modules |
yes | the script must be run FROM that directory (or NODE_PATH) |
| In | cookie/localStorage dev-login | no | for auth bypass |
| Out | screenshot (e.g. /tmp/check.png) + log [console]/[pageerror]/[http]/[reqfail] |
yes | as evidence; a clean screenshot alone is NOT enough |
| Out | measurements of getBoundingClientRect/getComputedStyle |
when diagnosing an element | real numbers instead of guessing from the screenshot |
| Out | "done" | — | hypothesis confirmed/refuted by log + screenshot; script cleaned up; Chrome CDP (if any) has pkill |
Rules and capabilities
- RULE-01 (MUST):
import { chromium } from "@playwright/test"must be run FROM WITHIN the project directory that has@playwright/testinnode_modules— ESM resolves bycwd, not by the script file location. A script in another directory (e.g./tmp) →cpinto the project before running, or pointNODE_PATH. - RULE-02 (MUST):
waitUntil: "networkidle"HANGS INDEFINITELY on Next.js dev server — HMR/webpack dev keeps a long-poll/websocket connection alive continuously so the network never "idle". UsewaitUntil: "load"+page.waitForTimeout(N)fixed. - RULE-03 (MUST): Auth bypass for dev-login:
context.addCookies([{url, name, value}])(preferurlinstead ofdomain+pathindividually — avoid cookies not matching) +context.addInitScript(...)setlocalStorage— set state BEFORE callingnewPage(), not after. - RULE-04 (MUST): 1
contextfor each independent route if the app has logic "encounter 401 at secondary API → automatically clear global session" — reusing 1 context for multiple navigations causes route N's error to contaminate route N+1. - RULE-05 (MUST): Diagnose "not seen/not clickable": don't just look at screenshots — call
page.evaluate(() => el.getBoundingClientRect())+getComputedStyle(el)to get real measurements;locator.click()reports clear errors itself ("element is not visible") with retry step logs, read that log before guessing the cause. - RULE-06 (MUST): A clean screenshot proves nothing — always extract 4 error channels, not just 2:
console(typeerror) ·pageerror·responsehasstatus() >= 400·requestfailed. Analytics crash, API 5xx, promise rejection show up in no pixel. Attach topageBEFOREgoto:page.on("response", r => { if (r.status() >= 400) console.log("[http]", r.status(), r.url()); }); page.on("requestfailed", r => console.log("[reqfail]", r.failure()?.errorText, r.url())); - RULE-07 (MUST): Session persistence mode: a separate Chrome, a separate profile (
--user-data-dirtemp). Never--remote-debugging-portthe user's personal Chrome — macOS raises a permission dialog and the script will step on the user's active tabs. - RULE-08 (MUST): Standalone script files do not go into git — run from scratchpad, do not commit into the target repo.
- RULE-09 (MUST): Touch only what the task requires — no opportunistic changes.
- Capabilities: run a local headless browser + read/write temp files; external network only when installing a binary (CDN), with timeout.
Failure boundaries
npx playwright installis blocked by network/hangs → blocked, report a clear error (with timeout), do not let the command hang indefinitely.- ESM does not resolve
@playwright/test(running from wrong cwd) → cp the script into the project orNODE_PATH, then rerun. networkidlehangs on Next.js dev → switch toload+waitForTimeout(not an app bug).- Element not seen/not clickable → measure real numbers + read log
locator.click()before concluding; do not guess from screenshots.
HOW
Main workflow
| Step | Type | Inputs | Action | Outputs/exit | Failure/next |
|---|---|---|---|---|---|
| W01 | deterministic | machine | Check if installed (ls ~/Library/Caches/ms-playwright); if not, install |
binary ready | CDN blocked → blocked |
| W02 | judgment | hypothesis + URL | Write 1 file .mjs run directly with node, attach 4 error channels before goto |
script | — |
| W03 | effect | script | Run from the correct directory (node check.mjs), read log + images; clean up script |
log + images | resolve error → cp/NODE_PATH, repeat W03 |
| W04 | effect | multi-step work | Persistent session mode connectOverCDP (B01) |
state kept between turns | done → pkill |
Details of each step (source of truth for W01–W04):
- Check whether it's already installed before reinstalling:
ls ~/Library/Caches/ms-playwright(macOS) — the binary cache is shared across all projects, no need to reinstall per-repo.- Not present:
pnpm add -D @playwright/test(skip if the project already has it indevDependencies) thennpx playwright install chromium. npx playwright installdownloads the binary from an external CDN (cdn.playwright.dev) — a sandbox/CI with a network allowlist will hang or fail. Set a timeout when calling automatically; if blocked, report a clear error instead of letting the command hang indefinitely.
- Not present:
- Write 1 file
.mjsrun directly withnode(not vianpx playwright test) — heavier for verify-then-discard:import { chromium } from "@playwright/test"; // export chromium dùng được như package "playwright" const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1400, height: 900 } }); page.on("console", m => console.log("[console]", m.type(), m.text())); page.on("pageerror", e => console.log("[pageerror]", e.message)); await page.goto("http://localhost:3000/some-page", { waitUntil: "load" }); // KHÔNG "networkidle" — xem Rules #2 await page.waitForTimeout(1500); await page.screenshot({ path: "/tmp/check.png", fullPage: true }); await browser.close(); - Run from the correct directory (see Rules #1),
node check.mjs, read console/pageerror log + screenshots. Clean up file.mjsafterward if running in 1 specific project (no need to delete anything from git — the script isn't in the repo if run from a scratch dir). - MULTI-STEP work with conditions → persistent session mode (learned from browser-use/browser-harness,
09/2026). One-shot scripts force the agent to blindly write the whole scenario; one wrong step means rerunning from
login. Instead: set up ONE Chrome with a CDP port, each turn is a small script attaching to it —
tab, cookie,
localStorage, scroll are preserved between turns, the agent looks before deciding the next step.# một lần — Chrome riêng, profile riêng; KHÔNG bám vào Chrome cá nhân của user CH="$HOME/Library/Caches/ms-playwright/chromium-1243/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing" "$CH" --remote-debugging-port=9333 --user-data-dir=/tmp/pw-prof --headless=new --no-first-run about:blank &
When done:// mỗi lượt — step-N.mjs import { chromium } from "@playwright/test"; const browser = await chromium.connectOverCDP("http://localhost:9333"); const ctx = browser.contexts()[0]; const page = ctx.pages()[0] ?? await ctx.newPage(); // bám tab cũ, KHÔNG launch mới // ... một hành động + quan sát; state sống sang lượt sau ... await browser.close(); // chỉ ngắt kết nối, Chrome vẫn chạypkill -f "remote-debugging-port=9333". Measured 09/09/2026 (python-playwright, same pagefile://): fresh launch 1.1–1.9s/turn, loses state;connectOverCDP0.65–0.80s/turn,window.__markersurvives across a new process (3/3). The.mjsversion above hasn't been run with node in the measurement session — the API is identical to the python version that was run.
Branches
| ID | Kind | Guard | Behavior | Skip / failure | Rejoin |
|---|---|---|---|---|---|
| B01 | conditional_required | MULTI-STEP work with conditions (need to look before deciding the next step) | a separate Chrome --remote-debugging-port=9333 + temp profile; each turn step-N.mjs connectOverCDP |
one-shot → skip; never stick to personal Chrome | ends with pkill -f "remote-debugging-port=9333" |
| B02 | user_optional | page needs dev-login/fake session | context.addCookies + context.addInitScript BEFORE newPage(); 1 context/route if the app self-clears session on 401 |
— | W03 |
| B03 | recovery | element "not visible/not clickable" | page.evaluate(() => el.getBoundingClientRect()) + getComputedStyle(el), read the retry log of locator.click() |
— | W03 |
Validation and stopping
Conclude based on 4 error channels + screenshots + measurements, not just screenshots. Each run < 5s, no LLM. Stop when the hypothesis has evidence; clean up scripts + pkill Chrome CDP.
Examples
- Positive: claude-in-chrome reports
Frame with ID 0 is showing error pageonhttp://localhost:3000/some-page→ cpcheck.mjsinto project →node check.mjs→/tmp/check.png+ clean log for both[console]/[pageerror]/[http]/[reqfail]→ confirm the page renders, delete script. - Boundary/failure: script uses
waitUntil: "networkidle"on Next.js dev → hangs indefinitely; fix towaitUntil: "load"+page.waitForTimeout(1500)→ run completes. Screenshot clean but log has[http] 500 /api/x→ do NOT report pass.