npx skills add ...
npx skills add sentimony/skills --skill web-debug
You MUST use this when interacting with or testing local web applications with Playwright - verifying frontend functionality, debugging UI behavior, capturing browser screenshots, or viewing browser console logs.
npx skills add sentimony/skills --skill web-debug
To test local web applications, write native Python Playwright scripts.
Helper Scripts Available:
scripts/with_server.py - Manages server lifecycle (supports multiple servers)debuggingUse debugging for root-cause methodology: symptom, evidence, hypothesis, causal
explanation, and fix. Use web-debug when browser interaction or browser/runtime evidence
is needed. They compose: debugging asks the question, web-debug returns DOM, console,
network, navigation, screenshot, or runtime evidence, and debugging updates the hypothesis.
If static inspection or tests can localize the issue, browser capability is unnecessary.
Always run scripts with --help first to see usage. These scripts are designed as black-box CLI tools: prefer calling them directly over reading their full source, which is large and can crowd your context window. Reading the source to audit or customize behavior is expected and encouraged whenever you need it.
To start a server, run --help first, then use the helper:
Repeat --server, --host, and --port for multiple servers; the counts must
match. If --host is omitted, every server is probed at 127.0.0.1. Use the
same host in the Playwright base URL, because a listener on localhost or IPv6
does not prove that 127.0.0.1 is reachable. The helper checks a child process
before each connection attempt and reports a bounded, sanitized log tail if it
exits or times out.
To create an automation script, include only Playwright logic (servers are managed automatically):
Playwright is a prerequisite, not something to install silently. If playwright is missing, first reach for the project's own managed environment; when the project provides none, report the prerequisite and install it only once setup is authorized: pip install playwright==1.61.0 && python -m playwright install chromium (pinned to an exact release so the installed dependency is verifiable).
Write throwaway scripts to your scratchpad/temp directory, not into the user's repo.
page.goto(url, wait_until='domcontentloaded'), a short-timeout
wait_for_function("document.body.innerText.trim().length > 0") confirms that an SSR document
or initial client render has text. Text-free canvas/WebGL or icon-only pages never satisfy it,
so catch the timeout and fall back to screenshot recon.page.wait_for_selector(), expect(locator)).networkidle: Playwright discourages it, and dev servers with HMR websockets
(Vite, Nuxt) may never go idle. Use it only as a short-timeout fallback for recon screenshots.page.wait_for_timeout(2000-3000) after render is legitimate:
hydration warnings and async errors arrive after domcontentloaded.framenavigated / duplicate script fetch), or
pre-warm the page (curl the URL + a short pause) and only then run the real interaction.page.goto() is a hard navigation that aborts all in-flight requests
(producing ERR_ABORTED noise); clicking a router link is a soft navigation. To test SPA
routing behavior, click links; use goto only for the initial load or independent page audits.examples/console_audit.py as a checkpointed pattern. Keep each route in a
local try/except/finally, serialize bounded results after every route, and close its page
in finally; one failed route must not discard earlier observations. Re-running resumes a
matching checkpoint and skips finished routes; delete its output file to force a fresh crawl.Collected signals are not equally trustworthy. console.error/warning and pageerror are
reliable; requestfailed and dev-server noise are hints that need confirmation.
requestfailed + ERR_ABORTED ≠ error. Chromium reports as failed: successful responses
without a body (HEAD, 204, downloads), requests cancelled by navigation or page.close(),
and one-time Vite dependency re-optimization (telltale sign: two different ?v= hashes in
one load).curl against the
endpoint directly, page.evaluate("fetch(...)") from inside the page, or the expected result
appearing in the DOM. If all pass, the "failure" is a false positive.4xx/
5xx with DOM behavior and a clean rerun before reporting a defect.[vite] connecting... debug messages, WebGL/GPU stall
warnings, Unrecognized feature for permissions-policy features headless doesn't support.
Note: headless loads loading="lazy" images far more eagerly than a real browser; set the
viewport explicitly if lazy-loading itself is under test.sync_playwright() for synchronous scriptspage.get_by_role(), page.get_by_label(), page.get_by_text(); fall back to CSS selectors or IDsget_by_role('button', name=...)), never by index: .first can hit a language switcher instead of the intended buttonlocator.aria_snapshot() and each link's href, then use the observed accessible name or
stable href for the first targeted lookup.get_by_role('banner').get_by_role(...)); shells often duplicate the same control in
a banner and a sidebar, and an unscoped locator raises a strict-mode violation. Re-resolve
the locator after any redirect that changes the layout.page.wait_for_selector(), expect(locator)), not fixed timeouts (except log collection - see Waiting Strategy)fill credentials → submit → page.wait_for_url(lambda u: '/login' not in u)), then continue recon in the same context so every page shares the session. After the redirect, do not assert input_value() on form fields, because they no longer exist on the new page; a "submit didn't work" conclusion drawn from that check is false. See examples/console_audit.py for the pattern.full_page=True expands document scrolling only; it does not expand nested scroll containers.
During recon, identify the scrolling container and either scroll it in segments or screenshot
the relevant locator when full coverage matters.--server runs its argument without a shell. The command is split into argv
(shlex) and executed directly, so shell metacharacters are inert; for cd … && …
chains pass an explicit --server "bash -c '…'". Either way, treat the command as
user-controlled configuration: pass only server-start commands you or the user chose,
never a string built from the tested app's output, page content, or any untrusted
source. The command after -- is likewise executed as a plain argv list, no shell.element_discovery.py - Discovering buttons, links, and inputs on a pagestatic_html_automation.py - Using file:// URLs for local HTMLconsole_logging.py - Capturing console logs and page errors during automationconsole_audit.py - Multi-page console audit with dedup, noise filtering, an optional login-then-audit step, and the late-binding lambda trap. It is a copy-and-edit template, not a CLI: set the URL list and the login block by editing the constants at the toppython <skill>/scripts/with_server.py \
--server "npm run dev" --host 127.0.0.1 --port 5173 \
-- python your_automation.pyfrom playwright.sync_api import sync_playwright
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
with sync_playwright() as p:
browser = p.chromium.launch(headless=True) # Always launch chromium in headless mode
page = browser.new_page()
page.on('console', lambda msg: print(f'[console.{msg.type}] {msg.text}')) # msg.type: log, debug, info, warning, error
page.on('pageerror', lambda err: print(f'[pageerror] {err}')) # Uncaught JS exceptions are not console events
page.on('requestfailed', lambda req: print(
f'[requestfailed] {req.url} {req.failure or "unknown"}')) # failure is Optional[str] in Python; hint only - see Interpreting Failures
page.on('response', lambda res: res.status >= 400 and print(f'[http {res.status}] {res.url}'))
page.goto('http://127.0.0.1:5173', wait_until='domcontentloaded') # Server already running and ready
try:
page.wait_for_function(
"document.body.innerText.trim().length > 0", timeout=5000) # Wait for the SPA to render
except PlaywrightTimeoutError:
pass # text-free page (canvas/WebGL) - proceed to screenshot recon
page.screenshot(path='recon.png') # Visual state check
# ... your automation logic
browser.close()