{"id":"2d53eb88-00e1-4c91-be28-4619695b5c30","entityType":"agent","slug":"clawhub-ivangdavila-playwright-aywright-2","name":"Playwright (Automation + MCP + Scraper)","canonicalUrl":"https://www.xpersona.co/agent/clawhub-ivangdavila-playwright-aywright-2","canonicalPath":"/agent/clawhub-ivangdavila-playwright-aywright-2","generatedAt":"2026-11-14T06:52:51.581Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-11-11T17:16:35.852Z","emptyReason":null},"description":"Automates, tests, and debugs browsers with Playwright: locators, auto-waiting, traces, CI runs, and MCP browser control. Use when a test is flaky, times out, or fails only in CI or headless; when a locator matches multiple elements or the wrong one (strict mode violation); when clicks need force, waits become sleeps, or networkidle never settles; for storageState and login setup, request mocking and HAR replay, uploads and downloads, iframes and shadow DOM, popups and dialogs, screenshot diffs that change per machine, trace and report artifacts, sharding a slow suite, device and permission emulation, accessibility checks, driving a real browser through Playwright MCP, extracting data from JS-rendered pages, or porting a Cypress, Puppeteer, or Selenium suite to Playwright. Not for maintaining an existing Cypress or Puppeteer suite (cypress, puppeteer) or for work a plain HTTP request answers (http). Skill: Playwright (Automation + MCP + Scraper) Owner: ivangdavila Summary: Automates, tests, and debugs browsers with Playwright: locators, auto-waiting, traces, CI runs, and MCP browser control. Use when a test is flaky, times out, or fails only in CI or headless; when a locator matches multiple elements or the wrong one (strict mode violation); when clicks need force, waits become sleeps, or networkidle never settl","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 43.8K downloads reported by the source. Last updated 11/11/2026.","installCommand":"clawhub skill install s178jdk12x4qj3gs2se3etxf3h83h7ft:playwright","sourceUrl":"https://clawhub.ai/ivangdavila/playwright","homepage":"https://clawhub.ai/ivangdavila/skills/playwright","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/ivangdavila/playwright","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/ivangdavila/skills/playwright","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":90,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Automates, tests, and debugs browsers with Playwright: locators, auto-waiting, traces, CI runs, and MCP browser control. Use when a test is flaky, times out, or"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-11-11T17:16:35.852Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-11-11T17:16:35.852Z","emptyReason":null},"stars":null,"forks":null,"downloads":43751,"likes":null,"task":null,"library":null,"packageName":null,"latestVersion":"1.0.4","tractionLabel":"43.8K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-11-11T17:16:35.795Z","emptyReason":null},"lastUpdatedAt":"2026-11-11T17:16:35.852Z","lastCrawledAt":"2026-11-11T17:16:35.795Z","lastIndexedAt":null,"nextCrawlAt":"2026-11-12T17:16:35.795Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.4","createdAt":"2026-07-26T11:43:46.329Z","changelog":"Full coverage pass: deeper guides, situation-named files, and per-user configuration","fileCount":24,"zipByteSize":68292},{"version":"1.0.3","createdAt":"2026-03-11T17:44:52.810Z","changelog":"Clarified the MCP-first browser automation flow and improved quick-start guidance for forms, screenshots, and extraction.","fileCount":8,"zipByteSize":14891},{"version":"1.0.2","createdAt":"2026-02-26T12:07:52.145Z","changelog":"Rewritten with MCP integration and scraping focus. Now covers standalone use and MCP server setup.","fileCount":7,"zipByteSize":10164},{"version":"1.0.1","createdAt":"2026-02-26T12:06:49.007Z","changelog":"Rewritten with MCP integration and scraping focus. Now covers standalone use and MCP server setup.","fileCount":7,"zipByteSize":10162},{"version":"1.0.0","createdAt":"2026-02-13T11:29:44.555Z","changelog":"Initial release","fileCount":7,"zipByteSize":8864}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s178jdk12x4qj3gs2se3etxf3h83h7ft:playwright","setupComplexity":"low","setupSteps":["Setup complexity is LOW. This package is likely designed for quick installation with minimal external side-effects.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-11-14T06:52:51.579Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-aywright-2/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-11-11T17:16:35.852Z","emptyReason":null},"readme":"Skill: Playwright (Automation + MCP + Scraper)\n\nOwner: ivangdavila\n\nSummary: Automates, tests, and debugs browsers with Playwright: locators, auto-waiting, traces, CI runs, and MCP browser control. Use when a test is flaky, times out, or fails only in CI or headless; when a locator matches multiple elements or the wrong one (strict mode violation); when clicks need force, waits become sleeps, or networkidle never settles; for storageState and login setup, request mocking and HAR replay, uploads and downloads, iframes and shadow DOM, popups and dialogs, screenshot diffs that change per machine, trace and report artifacts, sharding a slow suite, device and permission emulation, accessibility checks, driving a real browser through Playwright MCP, extracting data from JS-rendered pages, or porting a Cypress, Puppeteer, or Selenium suite to Playwright. Not for maintaining an existing Cypress or Puppeteer suite (cypress, puppeteer) or for work a plain HTTP request answers (http).\n\nTags: latest:1.0.4\n\nVersion history:\n\nv1.0.4 | 2026-07-26T11:43:46.329Z | user\n\nFull coverage pass: deeper guides, situation-named files, and per-user configuration\n\nv1.0.3 | 2026-03-11T17:44:52.810Z | user\n\nClarified the MCP-first browser automation flow and improved quick-start guidance for forms, screenshots, and extraction.\n\nv1.0.2 | 2026-02-26T12:07:52.145Z | user\n\nRewritten with MCP integration and scraping focus. Now covers standalone use and MCP server setup.\n\nv1.0.1 | 2026-02-26T12:06:49.007Z | user\n\nRewritten with MCP integration and scraping focus. Now covers standalone use and MCP server setup.\n\nv1.0.0 | 2026-02-13T11:29:44.555Z | user\n\nInitial release\n\nArchive index:\n\nArchive v1.0.4: 24 files, 68292 bytes\n\nFiles: _meta.json (425b), accessibility.md (5304b), auth.md (6234b), browsers.md (5374b), ci-cd.md (6961b), commands.md (3592b), config.md (4878b), contexts.md (6140b), debugging.md (6999b), files.md (4948b), fixtures.md (3296b), flake.md (5570b), mcp.md (5118b), migration.md (4731b), network.md (6854b), performance.md (4815b), scraping.md (5790b), selectors.md (6683b), setup.md (2843b), skill-card.md (2643b), SKILL.md (21439b), testing.md (5170b), visual.md (5184b), waiting.md (6886b)\n\nFile v1.0.4:SKILL.md\n\n---\nname: playwright\nslug: playwright\nversion: 1.0.4\ndescription: >-\n  Automates, tests, and debugs browsers with Playwright: locators, auto-waiting, traces, CI runs, and MCP browser control.\n  Use when a test is flaky, times out, or fails only in CI or headless; when a locator matches multiple elements or the\n  wrong one (strict mode violation); when clicks need force, waits become sleeps, or networkidle never settles; for\n  storageState and login setup, request mocking and HAR replay, uploads and downloads, iframes and shadow DOM, popups and\n  dialogs, screenshot diffs that change per machine, trace and report artifacts, sharding a slow suite, device and\n  permission emulation, accessibility checks, driving a real browser through Playwright MCP, extracting data from\n  JS-rendered pages, or porting a Cypress, Puppeteer, or Selenium suite to Playwright. Not for maintaining an existing\n  Cypress or Puppeteer suite (cypress, puppeteer) or for work a plain HTTP request answers (http).\nhomepage: https://clawic.com/skills/playwright\nchangelog: \"Full coverage pass: deeper guides, situation-named files, and per-user configuration\"\nmetadata:\n  clawdbot:\n    emoji: P\n    requires:\n      bins:\n      - node\n      - npx\n    os:\n    - linux\n    - darwin\n    - win32\n    install:\n    - id: npm-playwright\n      kind: npm\n      package: playwright\n      bins:\n      - playwright\n      label: Install Playwright\n    - id: npm-playwright-mcp\n      kind: npm\n      package: '@playwright/mcp'\n      bins:\n      - playwright-mcp\n      label: Install Playwright MCP (optional)\n    displayName: Playwright (Automation + MCP + Scraper)\n    configPaths:\n    - ~/Clawic/data/playwright/\n---\n\nUser preferences and memory live in `~/Clawic/data/playwright/` (see `setup.md` on first use). If you have data at an old location (`~/playwright/` or `~/clawic/playwright/`), move it to `~/Clawic/data/playwright/`. Everything a run produces — specs, traces, reports, snapshots, `storageState` files — stays inside the repository or its temp dir, never in the preferences folder: auth state is a credential and belongs in a gitignored path.\n\n## When To Use\n\n- Writing or repairing Playwright specs, fixtures, `playwright.config.ts`, or one-off automation scripts\n- A run fails: timeout, strict mode violation, flaky click, passes locally and fails in CI or headless\n- Login, session reuse, uploads, downloads, iframes, popups, dialogs, or device emulation are in the way\n- Visual snapshots, accessibility checks, trace capture, sharding, or cutting suite wall time\n- Driving a live browser through Playwright MCP, or extracting data a plain fetch cannot render\n- Porting a Cypress, Puppeteer, or Selenium suite to Playwright, including deciding which tests not to port\n- Not for maintaining an existing Cypress or Puppeteer suite (→ `cypress`, `puppeteer`) or for anything an HTTP request already answers (→ `http`)\n\n## Quick Reference\n\n| Situation | Play |\n|---|---|\n| `strict mode violation: resolved to N elements` | Narrow with `.filter({ hasText })` or a parent chain — never `.nth()` unless position is the behavior under test → `selectors.md` |\n| `Timeout 30000ms exceeded waiting for locator` | The locator never matched. Trace it, do not raise the timeout → `debugging.md` |\n| `element is not visible` / `intercepts pointer events` | An overlay, animation, or disabled state — `force: true` hides the bug → `waiting.md` |\n| Test needs a `waitForTimeout` to pass | Replace with a web-first assertion on the state you were sleeping for → `waiting.md` |\n| Green locally, red in CI (or only headless) | Compare viewport, workers, timezone, animations, and browser build → `flake.md`, `ci-cd.md` |\n| Passes alone, fails in the suite | Shared account, shared server data, or leaked storage — isolate per worker → `auth.md` |\n| Same test red 1 run in 20 | Measure before fixing: `--repeat-each`, then classify by cause → `flake.md` |\n| Login repeated in every test | Sign in once in a setup project, reuse `storageState` → `auth.md` |\n| A test passes while the feature is broken, or nobody knows what deserves an E2E | What earns a test, outcome assertions, skip and fixme annotations → `testing.md` |\n| Writing `playwright.config.ts`, adding a project, a setting that will not apply | Config anatomy, precedence, projects, `webServer`, tags → `config.md` |\n| Setup repeated in every test, page objects, or where test data comes from | Test- and worker-scoped fixtures, page-object rules, data strategy → `fixtures.md` |\n| Third party, payment sandbox, or slow API in the path | Intercept with `page.route`, or record and replay a HAR → `network.md` |\n| Upload, download, PDF, or drag-and-drop | Event-first patterns (`waitForEvent('download')`, `setInputFiles`) → `files.md` |\n| New tab, popup, `window.alert`, geolocation, dark mode, offline | Context-level control; dialogs auto-dismiss unless handled → `contexts.md` |\n| Screenshot diff passes locally, fails on the runner | Snapshots are per-platform; generate them where CI runs → `visual.md` |\n| WCAG or keyboard-navigation coverage | axe scan plus real keyboard assertions → `accessibility.md` |\n| Suite takes too long | Measure the slowest tests first, then parallelize or seed via API → `performance.md` |\n| Browser missing, wrong channel, WebKit-only failure | Version-locked browsers and engine differences → `browsers.md` |\n| Agent should click through a live site now | Snapshot-driven MCP browser control → `mcp.md` |\n| Pull data from a JS-rendered page | Bounded extraction with throttling → `scraping.md` |\n| Porting from Cypress, Puppeteer, or Selenium | Translation table and the habits to drop → `migration.md` |\n| Anything else | Reproduce headed with a trace (`npx playwright test <file> --headed --trace on`), open the trace, and read the DOM snapshot at the failing step |\n\nCLI toolkit: `commands.md`. First-use preference loading: `setup.md`.\n\n## Core Rules\n\n1. **Locate by what the user perceives.** Order: role/label/text → test ID → CSS/XPath. `getByRole('button', { name: 'Submit' })` survives a class rename and a CSS-in-JS rebuild; `.css-1a2b3c` does not. Test IDs are a contract you own — fine, but they assert nothing about accessibility.\n2. **Never sleep; assert.** `page.waitForTimeout(500)` is simultaneously too slow on a fast run and too short on a loaded runner. Web-first assertions re-check until the expect timeout (5 s default) and stop the moment the condition holds — strictly faster and strictly more reliable.\n3. **Budget the timeouts, do not inflate them.** Defaults: test 30 000 ms, expect 5 000 ms, action and navigation 0 (unbounded, so the test timeout is the real ceiling), `webServer` 60 000 ms. Sizing rule: sum the slowest legitimate path, measured not guessed, and keep the total inside the 30 000 ms default — login 4 s + dashboard render 8 s + one 5 s assertion = 17 s, comfortably inside 30 s. When one step is genuinely slow, raise that assertion's `timeout`, not the global test timeout: a global bump turns every real hang into a 2-minute wait.\n4. **One test, one fresh context, one account it owns.** Playwright gives each test a new browser context; that resets cookies and storage but not your database. With N workers, allocate N accounts and index them by `parallelIndex` — sharing one mutable user makes failures order-dependent, and order-dependent failures are unfixable by reading the diff.\n5. **Capture before you rewrite.** For any failure you did not cause in the last edit, get a trace first (`trace: 'on-first-retry'`, or `--trace on` for one run). Rewriting a locator against a symptom you never observed is how a two-line bug becomes a rewritten suite.\n6. **Retries buy green builds and hide the bill.** A per-test failure probability p across N tests makes a full run red with probability 1 − (1 − p)^N: p = 0.005 across 300 tests → 78% of runs red. Two retries mask that, but the flake is still there and now costs 3× wall time on every occurrence. Keep retries, and track the flaky count as a number that must go down (`flake.md`).\n7. **Mock what you do not own; seed through the API, click through the UI only for what is under test.** Creating a user by API and starting the test on the target page removes minutes and removes the upstream service from your failure surface. Exception: the integration itself is the thing being verified.\n8. **Treat production and destructive flows as opt-in.** Automating a real payment, deletion, or email send needs the user to say so in this session; default to local or staging. `storageState` files are live credentials — gitignore them and never write one into a shared or synced folder.\n9. **Pin the browsers to the Playwright version.** Browser builds ship with the package, so `mcr.microsoft.com/playwright:vX.Y.Z-*` must match the installed `@playwright/test` exactly, and a CI cache key must include that version. A mismatch surfaces as `Executable doesn't exist at .../ms-playwright/...` or, worse, as a silent behavior difference.\n\n## Failure Signatures\n\nRead the first line of the error before anything else; it names the subsystem.\n\n| Message | Real cause | First move |\n|---|---|---|\n| `strict mode violation: ... resolved to 3 elements` | The locator describes a category, not an element | Filter or chain (`selectors.md`); the error lists the matches — read them |\n| `Timeout 30000ms exceeded. waiting for locator(...)` | Never matched: wrong frame, wrong name, not rendered | Trace → DOM snapshot at that step (`debugging.md`) |\n| `waiting for element to be visible, enabled and stable` | Matched but not actionable: animation, overlay, `disabled` | `waiting.md` actionability table — not `force: true` |\n| `element intercepts pointer events` | Another node covers the target (cookie banner, toast, modal backdrop) | Dismiss it in a fixture; the banner is real user-facing state |\n| `Test timeout of 30000ms exceeded while running \"beforeEach\" hook` | Setup, not the test, is slow or hanging | Move it to a setup project or fixture with its own timeout |\n| `Target page, context or browser has been closed` | Action after `close()`, or the process crashed mid-run | Check for a crash in the worker log; in Docker suspect `/dev/shm` (`ci-cd.md`) |\n| `Executable doesn't exist at .../ms-playwright/...` | Browsers not installed or version-mismatched | `npx playwright install --with-deps` at the pinned version (rule 9) |\n| `net::ERR_CONNECTION_REFUSED` | App not up, wrong port, or `webServer` exited | `webServer` block plus `reuseExistingServer: !process.env.CI` |\n| `net::ERR_CERT_AUTHORITY_INVALID` | Self-signed cert on staging or a corporate MITM proxy | `ignoreHTTPSErrors: true` scoped to that project only |\n| Screenshot comparison failed, `-linux.png` missing | Snapshots are keyed by platform and project | Generate baselines on the CI platform (`visual.md`) |\n| No error, test passes, feature is broken | Asserted on the command, not the outcome | Assert user-visible state (`testing.md`) |\n\n## Isolation Model\n\nWhat resets, and when — the mental model behind most \"it only fails in the suite\" bugs.\n\n| Layer | Cost to create | Resets | Leaks across tests |\n|---|---|---|---|\n| Browser | Expensive; one per worker, reused | Nothing between tests | Yes, by design |\n| Context | Cheap; new one per test by default | Cookies, localStorage, sessionStorage, permissions, service workers | No |\n| Page | Cheapest; many per context | Nothing (shares the context jar) | Within the test only |\n| Worker process | Restarted after a failure and on retry | Worker fixtures re-run | Module-level globals inside one worker |\n| Server, database, mailbox | Not managed by Playwright | Nothing | **Yes — this is the real leak** |\n\nConsequences: `fullyParallel: true` runs tests inside one file across workers, so file-level ordering guarantees vanish. `test.describe.serial` restores order but skips the remaining tests on the first failure and re-runs the whole group on retry. A retried test starts in a fresh worker — which is exactly why some flake \"fixes itself\" on retry and tells you the leak was in-process.\n\n## Output Gates\n\nBefore delivering a spec, script, or config:\n\n- Zero `waitForTimeout` used for synchronization and zero `networkidle` waits — each replaced by an assertion on the state being waited for (a politeness delay in extraction is the only exception, `scraping.md`)?\n- Every locator resolves to exactly one element, without `.first()`/`.nth()` standing in for a real filter?\n- Assertions check user-visible outcomes (text, URL, enabled state, download), not that a call returned?\n- No shared mutable account or shared seed data between tests that can run in parallel?\n- Trace configured (`on-first-retry` at minimum) and failure artifacts uploaded in CI?\n- No credential, token, or `storageState` path committed; production URLs absent unless the user asked for them?\n- Runs headless on a clean checkout with browsers installed at the pinned version?\n\n## Configuration\n\nUser-dependent variables. Defaults apply until the user states a preference; store them in `~/Clawic/data/playwright/config.yaml`.\n\n| Variable | Type | Default | Effect |\n|---|---|---|---|\n| language | typescript \\| javascript | typescript | Language of every emitted spec, config, and snippet; switches `playwright.config.ts` to `.js` and drops type imports |\n| runner_mode | test \\| script \\| mcp | test | Which path Quick Reference proposes first: `@playwright/test` suite, standalone Playwright script, or MCP browser control |\n| default_browsers | list (chromium, firefox, webkit) | chromium | Projects written into config and the engines assumed when diagnosing a failure (`browsers.md`) |\n| cross_browser_cadence | pr \\| nightly \\| release | nightly | Which gate the non-default engines run at: every PR, a scheduled run, or release only — sets the matrix in `browsers.md` and the jobs written in `ci-cd.md` |\n| headed_by_default | bool | false | Adds `--headed` to suggested commands and `headless: false` to launch options during exploration |\n| test_id_attribute | string | data-testid | Value written to `testIdAttribute` and the attribute `getByTestId` examples use |\n| ci_provider | github \\| gitlab \\| other | github | Which pipeline template `ci-cd.md` guidance is rendered for, including artifact and cache steps |\n| scrape_delay_ms | number (0-60000) | 1000 | Minimum gap between page loads in multi-page extraction (`scraping.md` throttling) |\n\nPreference areas — customizable dimensions; a stated preference gets recorded in `config.yaml` and applied:\n\n- **Tooling** — package manager (`npm`/`pnpm`/`yarn`/`bun`) used in every command, UI mode vs CLI, codegen vs hand-written locators\n- **Conventions** — spec layout and naming, page-object vs inline fixtures, tag vocabulary (`@smoke`, `@flaky`), assertion style\n- **Platform** — browser channels (`chrome`, `msedge`), device emulation set, CI runner OS, official Docker image vs manual browser install\n- **Safety posture** — whether non-local base URLs may be automated at all, confirmation before destructive or paid flows, how loudly to flag production targets\n- **Flake policy** — retry count in CI, quarantine-and-file vs fix-before-merge, whether a flaky pass fails the build\n- **Cadence** — what runs at which gate (PR, nightly, release), how often the `@integration` spec hits the real third party, how long a `@flaky` quarantine may last before the test is fixed or deleted, when a recorded HAR and an accessibility scan are refreshed\n- **Extraction posture** — crawl breadth, robots and terms strictness, asset blocking, stop conditions\n- **Output format** — reporter set (`list`, `html`, `blob`, `junit`), how much trace and video to retain, verbosity of failure summaries\n\n## Traps\n\n| Trap | Why it fails | Do instead |\n|---|---|---|\n| `force: true` to get past a failing click | Bypasses the actionability checks that were reporting a real overlay or disabled control — the test goes green while the user stays blocked | Find the blocker in the trace; dismiss banners in a fixture (`waiting.md`) |\n| `waitForLoadState('networkidle')` | Analytics beacons, polling, and websockets keep an SPA \"busy\" forever; the docs discourage it | Assert on the element or response you actually need |\n| `.first()` / `.nth(0)` to silence strict mode | Freezes an accidental DOM order; a new promo card at index 0 silently retargets the test | `.filter({ hasText })`, `.filter({ has })`, or scope to a parent |\n| `page.$$eval` / `locator.all()` for lists | Neither auto-waits; both snapshot whatever exists at call time, so a slow render yields an empty array and a passing test | `await expect(list).toHaveCount(n)` first, then read |\n| `locator.isVisible()` in an `if` | Returns immediately without retrying — the classic \"works on my fast machine\" conditional | `await expect(locator).toBeVisible()`, or `toBeHidden()` for the negative |\n| Storing an `ElementHandle` across a re-render | The handle points at a detached node; locators re-resolve at action time, handles do not | Keep locators in variables; handles only when you need the raw node |\n| One `storageState` for every test that mutates data | Parallel workers fight over the same server-side rows | One account per worker indexed by `parallelIndex` (`auth.md`) |\n| Building the app's own page objects before any duplication exists | An abstraction layer nobody needed makes every future failure two files away from its cause | Inline first; extract on the third repeat (`fixtures.md`) |\n| Locators reaching into an iframe | Locators pierce open shadow DOM but never an iframe boundary | `page.frameLocator(...)` / `locator.contentFrame()` (`selectors.md`) |\n| Registering a `dialog` handler and forgetting to respond | The page hangs; without any handler Playwright auto-dismisses, so adding one changes behavior | Always `accept()` or `dismiss()` inside the handler (`contexts.md`) |\n| Committing a suite that only passes headed | Headless differs in fonts, media codecs, and window size, and CI has no display | Run headless locally before handing off (`browsers.md`) |\n| Driving a full browser to read static HTML | Slower, flakier, and a browser process per request for data a fetch returns | Try the HTTP path first (`scraping.md`, `http`) |\n\n## Where Experts Disagree\n\n- **Test IDs vs semantic locators.** Role and label locators double as an accessibility smoke test and break when the UI genuinely changes for users; test IDs never break and never tell you anything. Default: semantic first, test IDs for elements with no accessible name (icon buttons, list rows, canvas widgets). The boundary is whether the string a locator uses is one a user could perceive.\n- **Retries in CI.** Two retries is the scaffolded default and keeps a large suite mergeable; the zero-retry camp argues every masked flake is a real race in the product. Workable line: retries on, flaky count reported and trending down, and no test allowed to stay flaky across two sprints.\n- **How many E2E tests.** Pyramid says a handful of journeys; the trophy argues browser tests are the only ones that check what ships. Decide by cost: an E2E test that takes 20 s and catches a class of bug unit tests structurally cannot is worth it; one that re-tests validation logic is not.\n- **Agent-driven browsing vs committed automation.** MCP is unbeatable for exploration and one-off tasks and leaves nothing reviewable behind. Anything that must run again next week gets written down as code (`mcp.md`).\n\n## Security & Privacy\n\n| Endpoint | Data sent | Purpose |\n|---|---|---|\n| Sites the user asks to automate | Navigation, form input, cookies, uploaded files, page interactions | The requested automation, testing, capture, or extraction |\n| `https://registry.npmjs.org` | Package metadata and tarballs on install | Install Playwright or Playwright MCP |\n| Playwright browser CDN (during `playwright install`) | Browser build downloads | Fetch the version-locked browser binaries |\n\nThis skill does NOT: persist sessions or credentials by default; write anything outside the repository except the preferences folder above; recommend fingerprint spoofing, CAPTCHA-solving services, or rotating exits; automate production, payment, or destructive flows without the user asking in the current session; or send page content anywhere beyond the sites involved in the task.\n\nGuardrails: auth artifacts stay in a gitignored path and are never printed into logs or reports; traces and videos can contain tokens and personal data, so treat uploaded CI artifacts as sensitive and cap retention; extraction stays inside the scope the user named.\n\n## Related Skills\nMore Clawic skills, get them at https://clawic.com/skills/playwright (install if the user confirms):\n- `cypress` — the same job in a Cypress codebase\n- `puppeteer` — Chrome-only automation without the test runner\n- `http` — methods, status codes, headers, and caching when the answer needs no browser at all\n- `scrape` — robots, rate limits, and data-handling compliance for larger crawls\n- `screenshot` — capture craft beyond test assertions\n\n## Feedback\n- If useful, star it: https://clawic.com/skills/playwright\n- Latest version: https://clawic.com/skills/playwright\n\nPart of [Clawic](https://clawic.com), the verified skill library. Get this skill: https://clawic.com/skills/playwright.\n\nFile v1.0.4:_meta.json\n\n{\n  \"owner\": \"clawic\",\n  \"slug\": \"playwright\",\n  \"displayName\": \"Playwright (Automation + MCP + Scraper)\",\n  \"latest\": {\n    \"version\": \"1.0.4\",\n    \"publishedAt\": 1785066064433\n  },\n  \"history\": [\n    {\n      \"version\": \"1.0.3\",\n      \"publishedAt\": 1773251092810\n    },\n    {\n      \"version\": \"1.0.2\",\n      \"publishedAt\": 1772107672145\n    },\n    {\n      \"version\": \"1.0.0\",\n      \"publishedAt\": 1770982184555\n    }\n  ]\n}\n\nFile v1.0.4:accessibility.md\n\n# Accessibility — Automated Scans and Keyboard Paths\n\nAutomated scanning catches a minority of real barriers — Deque's own figure for axe-core is around 57% of issues found automatically. Treat the scan as the floor and keyboard plus screen-reader semantics as the actual test.\n\n## Scanning With axe\n\n```typescript\nimport AxeBuilder from '@axe-core/playwright';\n\ntest('dashboard has no critical a11y violations', async ({ page }) => {\n  await page.goto('/dashboard');\n  const results = await new AxeBuilder({ page })\n    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])\n    .analyze();\n  expect(results.violations).toEqual([]);\n});\n```\n\nScoping and triage:\n\n```typescript\nnew AxeBuilder({ page })\n  .include('#main')                       // scan one region\n  .exclude('.third-party-widget')         // an embed you cannot fix\n  .disableRules(['color-contrast'])       // temporarily, with a linked issue\n```\n\n- Scan **states**, not just routes: open the modal, expand the menu, trigger the error, then analyze. Most violations live in states a route-level scan never renders.\n- Adopting on a legacy app: snapshot the current violations as a baseline and fail only on new ones (`expect(results.violations.length).toBeLessThanOrEqual(baseline)`), then ratchet the number down. A first run demanding zero gets the whole check deleted.\n- Attach the violation list to the report: `testInfo.attach('axe', { body: JSON.stringify(results.violations, null, 2), contentType: 'application/json' })` — a failure that names the rule and the node is actionable; a bare count is not.\n\n## What The Scan Cannot See\n\n| Barrier | How to test |\n|---|---|\n| Focus order that jumps around the page | Tab through and assert the sequence (below) |\n| Focus lost after a modal closes | Assert focus returns to the trigger |\n| A keyboard trap inside a widget | Tab N times and assert you escaped |\n| A label that is present but meaningless (\"click here\") | Human review, or a lint rule on the copy |\n| Live-region announcements | Assert `role=\"status\"`/`aria-live` content changes |\n| Contrast on a background image | Human review |\n| Whether the flow is usable at all with a screen reader | Manual pass; no tool substitutes |\n\n## ARIA Snapshots — Structure As An Assertion\n\n`toMatchAriaSnapshot` (playwright >=1.49) freezes the accessibility tree of a region as YAML: roles, accessible names, and nesting.\n\n```typescript\nawait expect(page.getByRole('navigation')).toMatchAriaSnapshot(`\n  - list:\n    - listitem:\n      - link \"Home\"\n    - listitem:\n      - link /Sign (in|out)/\n`);\n\nawait expect(page.getByRole('dialog')).toMatchAriaSnapshot({ name: 'checkout-dialog.aria.yml' });\n```\n\n- It fails on the regressions a scan and a screenshot both miss: a heading demoted to a `div`, a button that lost its accessible name, a menu item that appeared, an order that changed. It passes through a pure restyle.\n- Scope it to the smallest meaningful region — nav, dialog, menu, table, a design-system component. A whole-page tree churns on every copy change and gets `-u`'d blind, which is the same failure mode as full-page screenshots (`visual.md`).\n- Generate with `--update-snapshots` and read the YAML like code: it is the machine's transcript of what a screen reader will announce, and an unreviewed baseline freezes today's bugs.\n- Regexes match names, and omitted children are allowed — the snapshot asserts what it lists, so a partial tree is a legitimate, lower-churn assertion.\n\n## Keyboard Assertions\n\n```typescript\nawait page.keyboard.press('Tab');\nawait expect(page.getByRole('link', { name: 'Skip to content' })).toBeFocused();\n\nfor (const name of ['Email', 'Password', 'Sign in']) {\n  await page.keyboard.press('Tab');\n  await expect(page.getByRole(/button|textbox/, { name })).toBeFocused();\n}\n\nawait page.getByRole('button', { name: 'Open settings' }).click();\nawait page.keyboard.press('Escape');\nawait expect(page.getByRole('button', { name: 'Open settings' })).toBeFocused();   // focus returned\n```\n\nUse `ControlOrMeta` for shortcuts so the test survives both macOS and Linux runners. `page.keyboard.press('Shift+Tab')` covers backward order, where most focus bugs hide.\n\n## Semantics You Get For Free\n\nEvery `getByRole` locator is an accessibility assertion in disguise: if the role-and-name locator cannot find the control, a screen reader cannot announce it either. Writing the suite with role-based locators (`selectors.md`) means a regression in accessible naming breaks a test before anyone files a ticket — the strongest argument for semantic locators over test IDs.\n\n`await page.getByRole('button', { name: 'Save' })` failing after a refactor to `<div onclick>` is exactly the signal you want.\n\n## Where To Run It\n\n- One a11y spec per major surface, tagged `@a11y`, in the PR gate — the scan takes about as long as a page load.\n- Component libraries: scan each component's states in the component suite; it is cheaper and catches issues before they multiply across pages.\n- Do not scan every page in every test; duplicate violations flood the report and nobody reads it twice.\n- A violation baseline and an ARIA snapshot are both dated artifacts: put their review on a schedule (the `Cadence` preference area, gates in `ci-cd.md`), or the ratchet stops ratcheting and nobody notices.\n\nFile v1.0.4:auth.md\n\n# Auth — Sessions, Roles, and Parallel-Safe Accounts\n\nLogging in through the UI in every test is the most expensive line in most suites: sign in once, reuse the state, and give every worker an account it owns.\n\nContents: [The Default Shape](#the-default-shape) · [What storageState Actually Captures](#what-storagestate-actually-captures) · [Faster: Skip The UI Entirely](#faster-skip-the-ui-entirely) · [Multiple Roles](#multiple-roles) · [Parallel-Safe Accounts](#parallel-safe-accounts) · [SSO, 2FA, And Magic Links](#sso-2fa-and-magic-links) · [Expiry And Rotation](#expiry-and-rotation) · [Handling Credentials](#handling-credentials)\n\n## The Default Shape\n\nA setup project signs in once and writes the storage state; every other project starts already authenticated.\n\n```typescript\n// global.setup.ts\nimport { test as setup, expect } from '@playwright/test';\n\nsetup('authenticate', async ({ page }) => {\n  await page.goto('/login');\n  await page.getByLabel('Email').fill(process.env.E2E_USER!);\n  await page.getByLabel('Password').fill(process.env.E2E_PASS!);\n  await page.getByRole('button', { name: 'Sign in' }).click();\n  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();\n  await page.context().storageState({ path: '.auth/user.json' });\n});\n```\n\n```typescript\n// playwright.config.ts\nprojects: [\n  { name: 'setup', testMatch: /global\\.setup\\.ts/ },\n  { name: 'chromium', dependencies: ['setup'], use: { storageState: '.auth/user.json' } },\n]\n```\n\n`.auth/` goes in `.gitignore`. A committed storage state is a committed live session.\n\n## What storageState Actually Captures\n\n| Captured | Not captured |\n|---|---|\n| Cookies (including httpOnly) | `sessionStorage` — per tab, never serialized by default |\n| `localStorage` per origin | IndexedDB, unless the Playwright version supports opting it in |\n| | Service-worker caches |\n| | Anything the server holds against a device fingerprint |\n\nIf the app keeps its token in `sessionStorage`, reuse fails silently: the test lands on a login page and the first assertion times out. Workarounds: copy the value in through an init script, or ask the app to accept the cookie path in test builds.\n\n```typescript\nawait context.addInitScript(([k, v]) => sessionStorage.setItem(k, v), ['token', token]);\n```\n\n## Faster: Skip The UI Entirely\n\nThe login form is a product feature that deserves **one** test. Everywhere else, mint the session through the API:\n\n```typescript\nconst res = await request.post('/api/login', { data: { email, password } });\nconst { token } = await res.json();\nawait context.addInitScript(t => localStorage.setItem('token', t), token);\n// or: await context.addCookies([{ name: 'session', value: token, domain: 'localhost', path: '/' }]);\n```\n\nSaves the whole login round trip per worker and removes the login page from the failure surface of 400 unrelated tests.\n\n## Multiple Roles\n\nOne storage-state file per role, one project per role:\n\n```typescript\nprojects: [\n  { name: 'setup', testMatch: /auth\\.setup\\.ts/ },\n  { name: 'admin',  dependencies: ['setup'], use: { storageState: '.auth/admin.json' } },\n  { name: 'viewer', dependencies: ['setup'], use: { storageState: '.auth/viewer.json' } },\n]\n```\n\nPer-test override when a spec needs a different identity:\n\n```typescript\ntest.use({ storageState: '.auth/admin.json' });        // describe or file scope\nconst anon = await browser.newContext({ storageState: undefined });   // logged-out path\n```\n\nTwo users interacting (chat, approvals, presence) need two contexts in one test — see `contexts.md`.\n\n## Parallel-Safe Accounts\n\nShared mutable accounts are the top cause of \"passes alone, fails in the suite\". Allocate by worker:\n\n```typescript\nexport const test = base.extend<{}, { account: Account }>({\n  account: [async ({}, use, workerInfo) => {\n    const account = await api.createUser(`e2e-w${workerInfo.parallelIndex}@example.test`);\n    await use(account);\n    await api.deleteUser(account.id);\n  }, { scope: 'worker' }],\n});\n```\n\n- With N workers you need N accounts, not one. `parallelIndex` is stable and bounded by the worker count; `workerIndex` grows across restarts and will overrun a fixed pool.\n- Sharded CI multiplies workers by shards: 4 shards × 4 workers = 16 concurrent identities. Size the pool for that number.\n- Legacy systems with a fixed account list: index into the pool by `parallelIndex` and take a lock (a database row, a lock file) if shards run on separate machines.\n\n## SSO, 2FA, And Magic Links\n\n| Obstacle | Approach |\n|---|---|\n| OAuth / SSO redirect to an external IdP | Prefer a test IdP or a mocked provider; otherwise a dedicated setup project drives the real login once, and nothing else touches it |\n| TOTP 2FA | Generate the code from the shared secret in the setup project — never a phone; store the secret as a CI secret |\n| SMS or push 2FA | Not automatable: use a bypass flag, a test tenant with 2FA off, or a provider sandbox |\n| Magic link | Read it from a mail-catcher API (Mailhog, Mailpit, provider sandbox), never from a real inbox |\n| CAPTCHA on login | Disable in the test environment. Solving services are out of scope and usually a terms violation |\n| Corporate IdP with device trust | Test against a staging IdP; production SSO is a manual smoke check |\n\n## Expiry And Rotation\n\n- Short-lived tokens outlive a fast suite but not a slow one. If tests near the end of a 25-minute run fail with 401 and earlier ones pass, the state expired mid-run: re-mint per worker instead of once globally.\n- A stale `.auth/*.json` from yesterday's local run is a classic false failure. Regenerate in the setup project every run; never commit, never cache across CI runs.\n- CSRF-protected apps may bind the session to a token issued at page load — reusing cookies is fine, reusing an in-page token is not.\n\n## Handling Credentials\n\n- Read from `process.env`, injected by CI secrets. Never hardcoded, never in the repo, never in `~/Clawic/data/playwright/`.\n- Traces and videos capture typed values: mask password fields or set `trace: 'off'` for the auth setup project if the app echoes anything sensitive.\n- The test user is a real account with real permissions — scope it to a throwaway tenant, not to production data.\n\nFile v1.0.4:browsers.md\n\n# Browsers — Engines, Channels, and Installs\n\nPlaywright ships its own browser builds, pinned to the package version. Most \"works on my machine\" browser problems are a version or channel mismatch, not a bug.\n\n## Engines\n\n| Engine | What it really is | Test it when |\n|---|---|---|\n| `chromium` | Playwright's Chromium build | Default; matches Chrome and Edge behavior closely enough for almost everything |\n| `firefox` | Playwright's Gecko build | You support Firefox; also catches spec-correct behavior Chromium tolerates |\n| `webkit` | WebKit engine, **not Safari** | Closest available proxy for Safari and iOS; shares the engine, not the browser |\n| `channel: 'chrome'` / `'msedge'` | Your installed branded browser | Widevine DRM, proprietary codecs, Chrome-specific APIs, or a bug reported only in real Chrome |\n\nWebKit on Linux differs from Safari on macOS in fonts, codecs, and media support. A WebKit-only failure is worth investigating, but \"passes in WebKit\" is not proof it works in Safari — for release-critical Safari behavior, run a real device or a hosted Safari.\n\nEngine differences that actually bite: date and number formatting via Intl, `input[type=date]` widgets, clipboard permissions (Chromium-only), `isMobile` (unsupported in Firefox), download handling, and font fallback affecting layout.\n\nExclude the affected case, not the engine: `test.skip(browserName === 'firefox', 'isMobile unsupported')` keeps the rest of the file running on Firefox and states why in the report (annotations: `testing.md`). Dropping the whole project to dodge one case is how cross-browser coverage quietly ends.\n\n## Headless\n\nChromium has two headless implementations: the old separate headless shell (fast, small) and the browser's own headless mode, which behaves like headed and supports extensions. Playwright >=1.49 exposes the browser-native one as `channel: 'chromium'`. When a bug reproduces headed but not headless, switching to the browser-native mode is the first thing to try — before rewriting the test.\n\nHeadless differences to expect regardless: no window manager (so window-size APIs report the configured viewport), a different default UA in older builds, no GPU by default, and no extensions in the old shell.\n\n## Install And Version Locking\n\n```bash\nnpx playwright install                      # all configured browsers\nnpx playwright install chromium --with-deps # one browser plus Linux system libraries (needs root)\nnpx playwright install-deps                 # libraries only\nnpx playwright --version                    # the number the CI image tag must match\n```\n\n- Browser builds are tied to the package version: upgrading `@playwright/test` without re-running `install` gives `Executable doesn't exist at ...`.\n- The Docker image tag must match exactly: `mcr.microsoft.com/playwright:v1.XX.Y-noble` with `@playwright/test@1.XX.Y`. A mismatched image either fails to find browsers or runs different builds than developers do.\n- Cache location: `~/.cache/ms-playwright` (Linux), `~/Library/Caches/ms-playwright` (macOS), `%USERPROFILE%\\AppData\\Local\\ms-playwright` (Windows). Override with `PLAYWRIGHT_BROWSERS_PATH`; `=0` installs inside `node_modules` (the fix for read-only or per-user cache problems in containers).\n- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` when the base image already contains them — otherwise every CI job re-downloads hundreds of megabytes.\n- Old browser versions accumulate in the cache across upgrades; `npx playwright uninstall --all` reclaims the space when disk gets tight.\n\n## Launch Options Worth Knowing\n\n```typescript\nuse: {\n  launchOptions: {\n    args: ['--disable-dev-shm-usage'],     // Docker: /dev/shm defaults to 64 MB (ci-cd.md)\n    slowMo: 250,                            // debugging only, never committed\n  },\n  contextOptions: { reducedMotion: 'reduce' },\n}\n```\n\n- `--no-sandbox` is the reflex fix for container crashes and the wrong one: it disables Chromium's sandbox. Run as a non-root user in the image, or use the official image which is set up for it.\n- `slowMo` and `headless: false` are investigation tools; a committed `slowMo` silently multiplies suite time.\n- Custom `args` are Chromium-only; Firefox uses `firefoxUserPrefs`, WebKit accepts almost none.\n\n## Choosing A Matrix\n\n| Suite | Matrix |\n|---|---|\n| PR gate | Chromium only — the fastest signal, catches most regressions |\n| Nightly | Chromium + Firefox + WebKit |\n| Release | Add branded `chrome`/`msedge` channels and a mobile viewport project |\n| Public-facing consumer app | Add real-device Safari verification outside Playwright |\n\nRunning three engines on every PR triples cost for a low yield: cross-engine failures are real but rare, and finding them a few hours later on a nightly run is usually acceptable. Decide from your own analytics, not from the browser list.\n\nWhich row is the default is the `cross_browser_cadence` preference (`pr` | `nightly` | `release`, default `nightly`); the jobs that implement each gate are in `ci-cd.md`.\n\n## Electron And Android\n\nPlaywright can drive Electron apps (`_electron.launch({ args: ['.'] })`, then `electronApp.firstWindow()`) and Chrome on Android over adb. Both are supported but thinner than the web path: expect fewer conveniences, more version sensitivity, and less documentation. Verify the API shape against the installed version before building a suite on either.\n\nFile v1.0.4:ci-cd.md\n\n# CI — Pipelines, Sharding, Artifacts\n\nA Playwright job has four jobs of its own: install the right browsers, start the app, run the suite deterministically, and leave behind evidence when it fails.\n\nContents: [GitHub Actions](#github-actions) · [GitLab CI](#gitlab-ci) · [Docker](#docker) · [Caching Browsers](#caching-browsers) · [Sharding](#sharding) · [Serving The App](#serving-the-app) · [Gates And Cadence](#gates-and-cadence) · [Config Recommendations For CI](#config-recommendations-for-ci) · [Debugging A CI-Only Failure](#debugging-a-ci-only-failure)\n\n## GitHub Actions\n\n```yaml\nname: e2e\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    timeout-minutes: 30\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with: { node-version: 20, cache: npm }\n      - run: npm ci\n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps chromium\n      - run: npx playwright test\n        env:\n          BASE_URL: http://localhost:3000\n      - uses: actions/upload-artifact@v4\n        if: ${{ !cancelled() }}\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 7\n```\n\n- `if: ${{ !cancelled() }}` beats `if: failure()`: flaky-but-passed runs still upload the trace that explains the flake.\n- `timeout-minutes` on the job plus `globalTimeout` in the config: two independent brakes on a hung run.\n- Install only the browsers the projects use; installing all three roughly triples download time for nothing.\n\n## GitLab CI\n\n```yaml\ne2e:\n  image: mcr.microsoft.com/playwright:v1.XX.Y-noble   # must equal the installed @playwright/test\n  script:\n    - npm ci\n    - npx playwright test\n  artifacts:\n    when: always\n    paths: [playwright-report/, test-results/]\n    expire_in: 7 days\n```\n\n## Docker\n\n```dockerfile\nFROM mcr.microsoft.com/playwright:v1.XX.Y-noble\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nCMD [\"npx\", \"playwright\", \"test\"]\n```\n\nRun with `--ipc=host`, or `--shm-size=1gb`. Docker's default `/dev/shm` is **64 MB**, and Chromium exhausting it produces tab crashes that surface as `Target page, context or browser has been closed` — the single most common containerized Playwright failure. `--disable-dev-shm-usage` is the alternative when you cannot change the run flags.\n\nThe image tag must match the installed Playwright version exactly (`browsers.md`).\n\n## Caching Browsers\n\n```yaml\n- uses: actions/cache@v4\n  with:\n    path: ~/.cache/ms-playwright\n    key: pw-${{ runner.os }}-${{ hashFiles('package-lock.json') }}\n```\n\nThe key must change when Playwright does — hashing the lockfile achieves that. A cache keyed only on the OS serves stale browsers after every upgrade and produces the \"executable doesn't exist\" error at random. Note that a cache hit still needs `install-deps` on a bare runner for the system libraries.\n\n## Sharding\n\n```yaml\nstrategy:\n  fail-fast: false\n  matrix:\n    shard: [1, 2, 3, 4]\nsteps:\n  - run: npx playwright test --shard=${{ matrix.shard }}/4 --reporter=blob\n  - uses: actions/upload-artifact@v4\n    with: { name: blob-${{ matrix.shard }}, path: blob-report/ }\n```\n\n```yaml\nmerge:\n  needs: test\n  if: ${{ !cancelled() }}\n  steps:\n    - uses: actions/download-artifact@v4\n      with: { pattern: blob-*, path: all-blobs, merge-multiple: true }\n    - run: npx playwright merge-reports --reporter=html ./all-blobs\n```\n\n- The blob reporter plus `merge-reports` is what makes sharding usable: without it you get four disconnected reports and no single verdict.\n- Shards split the test list deterministically, not by duration, so a shard holding the slow tests finishes last — balance by moving slow specs, not by adding shards.\n- `fail-fast: false` or one red shard cancels the others and destroys the evidence.\n- Shard only a suite that is already deterministic: four parallel copies of a flaky suite quadruple the chance of a red run (`flake.md`).\n\n## Serving The App\n\n```typescript\nwebServer: {\n  command: 'npm run start',              // a production build, not the dev server\n  url: 'http://localhost:3000',\n  reuseExistingServer: !process.env.CI,\n  timeout: 60_000,\n  stdout: 'pipe',                        // server logs land in the report on failure\n}\n```\n\nDev servers recompile on demand, so the first test in each area pays a multi-second compile and times out on a cold, loaded runner. Build once, serve the build.\n\nTesting a deployed environment instead: set `BASE_URL`, drop `webServer`, and add a readiness check — a deploy that is still rolling out fails the whole suite on the first navigation.\n\n## Gates And Cadence\n\nNot everything belongs on every push. Split the suite by gate, and let the schedule carry what a PR cannot afford:\n\n```yaml\non:\n  pull_request:\n  schedule: [{ cron: '0 3 * * *' }]      # the wider matrix runs while nobody waits for it\n```\n\n| Gate | What runs | Blocking |\n|---|---|---|\n| PR | Default engine, `--grep-invert \"@flaky\\|@slow\\|@integration\"` | Yes — this is the merge signal |\n| Quarantine | `--grep @flaky` on the same commit | No — coverage kept, red ignored (`flake.md`) |\n| Nightly | Every engine in `cross_browser_cadence`, plus `@slow` | No, but a red nightly opens an issue |\n| Integration | `--grep @integration` against real sandboxes | No — third-party downtime must not block merges |\n| Release | Branded `chrome`/`msedge` channels, mobile projects, visual baselines | Yes, on the release branch |\n\nCadence is a preference, not a constant: `cross_browser_cadence` (default `nightly`) decides where Firefox and WebKit run, and a team shipping Safari-critical features moves them into the PR gate and pays the time. Two dated artifacts need their own schedule regardless: recorded HARs (`network.md`) and accessibility baselines (`accessibility.md`) both pass forever against a fossil.\n\n## Config Recommendations For CI\n\n| Setting | Value | Why |\n|---|---|---|\n| `retries` | 2 | Absorbs infrastructure noise; the flaky count stays visible (`flake.md`) |\n| `workers` | 1 on a 2-core runner, cores/2 on a bigger one | Over-subscribing a shared runner creates timeouts that look like product bugs |\n| `forbidOnly` | `!!process.env.CI` | A committed `.only` otherwise greens the gate with one test |\n| `trace` | `on-first-retry` | Evidence for every flake, no cost on green runs |\n| `reporter` | `[['blob'], ['github']]` sharded, `[['html']]` otherwise | Blob is required for merging |\n| `globalTimeout` | 20-30 min | Kills a hung run before the provider does |\n\n## Debugging A CI-Only Failure\n\n1. Download the artifact and open the trace locally — the whole run is in that file.\n2. Reproduce in the CI image: `docker run --rm -it -v $PWD:/app -w /app mcr.microsoft.com/playwright:v1.XX.Y-noble npx playwright test`.\n3. Match the environment: same viewport, `TZ`, locale, worker count.\n4. Still green locally? It is load or ordering: `--workers=<ci value> --repeat-each=5` (`flake.md`).\n\nFile v1.0.4:commands.md\n\n# Commands — The Working Toolkit\n\nCommands that shorten a debugging loop, not the ones in the README.\n\n## Run Less, Faster\n\n```bash\nnpx playwright test path/to/file.spec.ts:42     # one test, by line number\nnpx playwright test -g \"checkout\"               # by title substring\nnpx playwright test --project=chromium          # one project instead of all\nnpx playwright test --last-failed               # rerun only what failed last time (playwright >=1.44)\nnpx playwright test --repeat-each=20 -g \"flaky\" # measure a flake rate (flake.md)\nnpx playwright test --workers=1                 # serialize to test for cross-test leakage\nnpx playwright test --max-failures=1            # stop at the first red in a long suite\n```\n\n`--headed` shows the browser; `--debug` opens the Inspector **and disables timeouts** (same as `PWDEBUG=1`), so a paused test never dies under you.\n\n## See What Happened\n\n```bash\nnpx playwright test --trace on                  # force a trace for this run\nnpx playwright show-trace test-results/**/trace.zip\nnpx playwright show-report                      # open the last HTML report\nnpx playwright test --ui                        # watch mode: time-travel, pick tests, live locators\n```\n\nTrace viewer reading order: timeline → the failing action → **DOM snapshot before/after** → Network → Console. The before/after snapshots answer \"was it there and covered?\" faster than any rerun.\n\n## Discover Locators And Flows\n\n```bash\nnpx playwright codegen https://example.com          # record clicks into a spec\nnpx playwright codegen --device=\"iPhone 15\" <url>   # record under emulation\nnpx playwright codegen --save-storage=auth.json <url>   # record a login, keep the session\nnpx playwright codegen --load-storage=auth.json <url>   # start recorded flows already signed in\nnpx playwright open <url>                           # a Playwright-controlled browser, no recording\n```\n\nCodegen output is a draft: it emits whatever locator it found, including brittle CSS. Keep the flow, rewrite the locators (`selectors.md`).\n\n## Browsers And Environment\n\n```bash\nnpx playwright install --with-deps chromium     # browsers + Linux system libs (needs root)\nnpx playwright install-deps                     # system libs only\nnpx playwright --version                        # must match the CI image tag exactly\n```\n\n- Binaries live in `~/.cache/ms-playwright` (Linux), `~/Library/Caches/ms-playwright` (macOS), `%USERPROFILE%\\AppData\\Local\\ms-playwright` (Windows).\n- `PLAYWRIGHT_BROWSERS_PATH=0` installs into `node_modules` instead — the fix when a shared cache is unwritable in a container.\n- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1` at install time when the base image already ships them.\n\n## Snapshots And Reports\n\n```bash\nnpx playwright test --update-snapshots          # rewrite baselines — review the diff, never blind-accept\nnpx playwright merge-reports --reporter=html ./blob-reports   # one report from sharded runs\n```\n\n## Quick Captures Without Writing Code\n\n```bash\nnpx playwright screenshot --full-page --viewport-size=1280,720 <url> out.png\nnpx playwright pdf <url> out.pdf                # Chromium headless only\n```\n\n## Environment Flags Worth Knowing\n\n| Variable | Effect |\n|---|---|\n| `PWDEBUG=1` | Inspector + all timeouts disabled |\n| `DEBUG=pw:api` | Log every Playwright call — the last line before a hang names the culprit |\n| `DEBUG=pw:browser` | Browser process stdout/stderr; where crash reasons appear |\n| `PLAYWRIGHT_HTML_REPORT` | Output dir for the HTML report |\n| `CI=1` | Enables `forbidOnly` and the CI branches of a typical config |\n\nFile v1.0.4:config.md\n\n# Config — playwright.config.ts, Projects, and Run Shape\n\nEverything that decides what runs, where, and in what order. Fixtures and page objects: `fixtures.md`. What deserves a test at all: `testing.md`.\n\n## Config Anatomy\n\n```typescript\nimport { defineConfig, devices } from '@playwright/test';\n\nexport default defineConfig({\n  testDir: './tests',\n  fullyParallel: true,                       // default is false: files run serially in one worker\n  forbidOnly: !!process.env.CI,              // a committed test.only silently skips the suite\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 1 : undefined,   // undefined = half the logical cores\n  globalTimeout: 30 * 60_000,                // a hung job dies here instead of at the provider limit\n  reporter: process.env.CI ? [['blob'], ['github']] : [['html']],\n  use: {\n    baseURL: process.env.BASE_URL ?? 'http://localhost:3000',\n    trace: 'on-first-retry',\n    screenshot: 'only-on-failure',\n    testIdAttribute: 'data-testid',\n  },\n  webServer: {\n    command: 'npm run dev',\n    url: 'http://localhost:3000',\n    reuseExistingServer: !process.env.CI,    // locally attach to a running server; in CI always start clean\n    timeout: 60_000,\n  },\n  projects: [\n    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },\n  ],\n});\n```\n\n- `baseURL` makes every `goto('/checkout')` relative — one env var repoints the whole suite at staging.\n- `forbidOnly` is the cheapest guard in the file: without it, one committed `.only` turns a 400-test gate into a 1-test gate that still shows green.\n- `testIdAttribute` is the only place `getByTestId` reads its attribute name from; changing it in one project and not another produces locators that work in half the matrix.\n\n## Option Precedence\n\nInnermost wins: `test.use()` inside a file or `describe` > the project's `use` > the top-level `use` > the Playwright default. When a setting appears not to apply, read outward from the test, not inward from the config — the override is almost always closer to the test than you expect.\n\nCommand-line flags (`--workers`, `--project`, `--grep`, `--headed`) beat all of them for one run and change nothing on disk.\n\n## Projects Are For Dimensions, Not Folders\n\n```typescript\nprojects: [\n  { name: 'setup', testMatch: /global\\.setup\\.ts/ },\n  { name: 'chromium', dependencies: ['setup'], use: { ...devices['Desktop Chrome'], storageState: '.auth/user.json' } },\n  { name: 'mobile', dependencies: ['setup'], use: { ...devices['Pixel 7'], storageState: '.auth/user.json' } },\n  { name: 'teardown', testMatch: /global\\.teardown\\.ts/ },\n]\n```\n\nUse projects for browsers, devices, auth roles, and environments. `dependencies` gives ordered phases (setup runs to completion before dependents start); `teardown` on a project runs after its dependents finish. Do not use projects to group by feature — that is what `--grep` and tags are for.\n\nA project is also the unit of cadence: which projects run on a PR versus a nightly is the `cross_browser_cadence` decision (`browsers.md` matrix), and `--project=chromium` is how the PR job spends one third of the time.\n\n## The webServer Block\n\n- `url` polls until the server answers; use `port` only when the server picks a path you cannot predict. A `url` that returns 404 still counts as up — point it at a route that exists.\n- `reuseExistingServer: !process.env.CI` is the local-development half: attach to the dev server already in your terminal, start a clean one in CI. Without it, every local run either fails on a busy port or races your own server.\n- `timeout: 60_000` covers a cold compile; a dev server that needs longer is the reason a first test times out on a loaded machine.\n- If the command exits, the run aborts with the server's own output — read that before suspecting the tests.\n- CI serves a production build instead, with readiness checks and `BASE_URL` for deployed environments (`ci-cd.md`).\n\n## Parallelism Controls\n\n```typescript\ntest.describe.configure({ mode: 'serial' });   // ordered, and a failure SKIPS the rest of the group\ntest.describe.configure({ mode: 'parallel' }); // spread across workers even without fullyParallel\ntest.describe.configure({ retries: 0 });       // per-group override\n```\n\nSerial mode is a last resort: it converts one failure into a block of skipped tests and re-runs the whole group on retry. Prefer independent tests with per-test setup.\n\n## Tags And Selective Runs\n\n```typescript\ntest('checkout @smoke', ...);\ntest('admin panel', { tag: '@slow' }, ...);\n```\n\n```bash\nnpx playwright test --grep @smoke          # PR gate\nnpx playwright test --grep-invert @slow    # everything but the long tail\n```\n\nTag vocabulary worth having from day one: `@smoke` (must pass on every PR), `@slow`, `@flaky` (quarantined, `flake.md`), `@integration` (hits a real third party, so it runs on a schedule rather than on every PR).\n\nFile v1.0.4:contexts.md\n\n# Contexts — Tabs, Popups, Dialogs, and Emulation\n\nThe context is the unit of isolation: an incognito profile with its own cookies, storage, permissions, and emulation. Pages inside it share everything; contexts share nothing.\n\nContents: [Multiple Users In One Test](#multiple-users-in-one-test) · [New Tabs And Popups](#new-tabs-and-popups) · [Dialogs](#dialogs) · [Emulation](#emulation) · [Runtime Overrides](#runtime-overrides) · [Lifecycle And Cost](#lifecycle-and-cost) · [Persistent Profiles](#persistent-profiles)\n\n## Multiple Users In One Test\n\nChat, approvals, presence, and collaborative editing need two identities simultaneously — two contexts, one browser:\n\n```typescript\ntest('reviewer sees the submitted doc', async ({ browser }) => {\n  const authorCtx = await browser.newContext({ storageState: '.auth/author.json' });\n  const reviewCtx = await browser.newContext({ storageState: '.auth/reviewer.json' });\n  const author = await authorCtx.newPage();\n  const reviewer = await reviewCtx.newPage();\n\n  await author.goto('/docs/new');\n  await author.getByRole('button', { name: 'Submit' }).click();\n\n  await reviewer.goto('/queue');\n  await expect(reviewer.getByText('Untitled doc')).toBeVisible();\n\n  await authorCtx.close();\n  await reviewCtx.close();\n});\n```\n\nTwo pages in the **same** context share a session — that models two tabs of one user, not two users. Getting this wrong produces tests that pass because both \"users\" are the same person.\n\n## New Tabs And Popups\n\n```typescript\nconst popupPromise = page.waitForEvent('popup');\nawait page.getByRole('link', { name: 'Open docs' }).click();   // target=\"_blank\"\nconst popup = await popupPromise;\nawait popup.waitForLoadState();\nawait expect(popup).toHaveURL(/docs/);\n```\n\n- Context-level for tabs you did not trigger directly: `context.on('page', p => ...)`.\n- `context.pages()` lists open pages; the last one is usually the new tab, but ordering is not a contract — match by URL.\n- OAuth popups are a page like any other: fill the IdP form in the popup, then continue on the opener after it closes.\n- The opener keeps running: assertions on `page` still work while `popup` is open.\n\n## Dialogs\n\n**Playwright auto-dismisses `alert`, `confirm`, `prompt`, and `beforeunload` when no handler is registered.** Two consequences:\n\n1. A test that \"clicks Delete and nothing happens\" is often a `confirm()` being dismissed for you.\n2. The moment you register a handler, auto-dismissal stops — **you must respond, or the page hangs forever**.\n\n```typescript\npage.once('dialog', async dialog => {\n  expect(dialog.message()).toContain('Delete 3 items?');\n  await dialog.accept();            // or dialog.dismiss()\n});\nawait page.getByRole('button', { name: 'Delete' }).click();\n\npage.on('dialog', d => d.accept('typed answer'));   // prompt value\n```\n\nUse `once` when only one dialog is expected — a lingering `on` handler silently accepts a later dialog the test never meant to confirm.\n\n## Emulation\n\n```typescript\nconst context = await browser.newContext({\n  ...devices['iPhone 15'],          // viewport, UA, deviceScaleFactor, isMobile, hasTouch\n  locale: 'de-DE',\n  timezoneId: 'Europe/Berlin',\n  colorScheme: 'dark',\n  reducedMotion: 'reduce',\n  geolocation: { latitude: 52.52, longitude: 13.405 },\n  permissions: ['geolocation'],\n  viewport: { width: 1280, height: 720 },\n});\n```\n\n| Knob | Notes |\n|---|---|\n| `devices[...]` | Chromium emulation: correct viewport, touch, and UA — **not** a real iOS engine. WebKit + a device descriptor is closer to Safari; only a real device proves iOS behavior |\n| `isMobile` | Not supported in Firefox — a mobile project must run on Chromium or WebKit |\n| `locale` / `timezoneId` | Set them explicitly in config; the CI runner is UTC and your laptop is not (`flake.md`) |\n| `colorScheme` | `'dark'`, `'light'`, `'no-preference'`; a project per scheme is cheaper than in-test toggling |\n| `reducedMotion: 'reduce'` | Also a free way to calm animations in visual tests |\n| `geolocation` | Requires the `geolocation` permission or the app sees a denial |\n| `offline` | `context.setOffline(true)` mid-test to exercise the offline UI |\n| `viewport: null` | Uses the real window size — only meaningful headed |\n\nPermissions are context-scoped: `context.grantPermissions(['notifications', 'clipboard-write'], { origin })`, `context.clearPermissions()`. Names and support vary by engine; Chromium has the widest set.\n\n## Runtime Overrides\n\n```typescript\nawait page.setViewportSize({ width: 375, height: 812 });   // responsive breakpoints mid-test\nawait page.emulateMedia({ media: 'print', colorScheme: 'dark' });\nawait page.clock.setFixedTime(new Date('2026-01-01T00:00:00Z'));   // deterministic dates (playwright >=1.45)\nawait context.addInitScript(() => { window.__E2E__ = true; });     // runs before any page script\n```\n\n`addInitScript` is the correct place to stub `window` APIs (analytics, feature flags, `crypto.randomUUID`) because it runs before the app's own code on every page and every navigation — a `page.evaluate` after `goto` is already too late.\n\n## Lifecycle And Cost\n\n| Action | When |\n|---|---|\n| `browser.newContext()` | Per test (the runner does it for you), or per identity |\n| `context.newPage()` | Per tab |\n| `browser.close()` | Per worker, at the end — never per test |\n| Manual contexts | Always close them in a `finally` or fixture teardown; leaked contexts leak browser memory across a long run |\n\nLaunching a browser is the expensive step; a context is comparatively cheap, which is what makes per-test isolation affordable. In a plain script (no test runner), reuse one browser and create a context per logical job.\n\n## Persistent Profiles\n\n```typescript\nconst context = await chromium.launchPersistentContext('/tmp/pw-profile', { headless: false });\n```\n\nKeeps cookies, localStorage, and extensions across runs, and returns a context with no `browser` to close. Legitimate for exploratory work, extension testing, and MCP sessions (`mcp.md`). Wrong for a test suite: persistent state is exactly what makes results irreproducible, and a stale profile is undebuggable a week later.\n\nFile v1.0.4:debugging.md\n\n# Debugging — Symptom to Cause\n\nWork symptom-first. Each chain is ordered by probability and every step is a check, not a guess.\n\nContents: [The Universal First Three](#the-universal-first-three) · [Locator Never Matched](#locator-never-matched-timeout--waiting-for-locator) · [Element Found But The Action Fails](#element-found-but-the-action-fails) · [Passes Locally, Fails In CI](#passes-locally-fails-in-ci) · [Only Fails Headless](#only-fails-headless) · [The Suite Hangs](#the-suite-hangs) · [Network-Level Investigation](#network-level-investigation) · [Debug Interactively](#debug-interactively) · [Failure Artifacts Worth Keeping](#failure-artifacts-worth-keeping) · [When You Are Truly Stuck](#when-you-are-truly-stuck)\n\n## The Universal First Three\n\n1. **Read the first line of the error.** Playwright names the failed condition (`strict mode violation`, `waiting for element to be visible`, `waiting for locator`). The rest of the stack rarely adds anything.\n2. **Open the trace.** `npx playwright show-trace test-results/**/trace.zip` → click the failing action → the **before** DOM snapshot answers \"was it rendered?\" and the **after** answers \"did anything happen?\". Network and Console tabs are in the same file.\n3. **Reproduce narrow.** `npx playwright test file.spec.ts:42 --headed --trace on`. If it passes alone but fails in the suite, the bug is isolation, not the test (→ `flake.md`).\n\nIf no trace exists, that is the first fix: `use: { trace: 'on-first-retry' }`.\n\n## Locator Never Matched (`Timeout ... waiting for locator`)\n\n1. Trace → before-snapshot at that step. Element visible in the snapshot? Then the locator is wrong, not the timing.\n2. Not in the snapshot at all → it renders later (assert on a preceding state), or it lives in an **iframe** (`selectors.md`), or behind a closed shadow root.\n3. In the snapshot but not matched → inspect the accessible name; icon buttons often have an `aria-label` different from the visible text, and `getByText` matches substrings while `getByRole` name matching is whole-string.\n4. Matched in headed, not in headless → viewport. A 1280×720 headless window hides a desktop-only element behind a responsive breakpoint (`browsers.md`).\n5. Only in CI → different data. The row you locate by text does not exist in the seeded environment.\n\n## Element Found But The Action Fails\n\n1. `not visible` → zero-size box, `display: none`, or an ancestor collapsed. Check the parent chain in the snapshot.\n2. `intercepts pointer events` → the snapshot shows the covering node; it is usually a consent banner, a toast, or a sticky header. Dismiss it in a fixture — that is real user-facing state, not test noise.\n3. `element is not stable` → an animation still running. Disable animations for the run, or assert on the settled state first (`waiting.md`).\n4. `element is not enabled` → the app enables the button after async validation; assert `toBeEnabled()` before clicking.\n5. Click \"works\" but nothing happens → a handler attached after hydration. Wait for a hydration signal (a `data-` attribute, a network response) instead of the element's presence.\n\n## Passes Locally, Fails In CI\n\nEach check is under a minute:\n\n| Difference | Check |\n|---|---|\n| Viewport | CI default is 1280×720; local headed windows are larger |\n| Workers | Local half-cores vs CI 1 (or vice versa) changes ordering and load |\n| Timezone / locale | `TZ` and `--lang` differ; date assertions break silently |\n| Animations and fonts | Missing fonts change layout and text wrapping |\n| Machine speed | A shared runner is 2-5× slower; races surface only there |\n| Data | Seeded fixtures vs a lived-in local database |\n| Browser build | CI image version vs local install (rule 9) |\n| Base URL | Staging behind auth, a redirect, or a cold start |\n\nFastest reproduction of a slow runner locally: `npx playwright test --workers=4 --repeat-each=3` while a build runs, or throttle CPU through CDP (`performance.md`).\n\n## Only Fails Headless\n\n1. Viewport and window size (above).\n2. Media: no H.264 or DRM in some headless builds — video and audio tests need a branded channel (`browsers.md`).\n3. Downloads and PDFs behave differently; `page.pdf()` is Chromium-headless only (`files.md`).\n4. Extensions and profile state that exist only in your headed browser.\n5. Confirm with `--headed` **and** the CI browser build before concluding it is headless-specific.\n\n## The Suite Hangs\n\n- `DEBUG=pw:api npx playwright test` — the last logged call before the silence names the stuck operation.\n- Unhandled `dialog`: with a handler registered but never answered, the page blocks forever (`contexts.md`).\n- `waitForEvent` registered after the action that fired it — the promise never settles (`waiting.md`).\n- `webServer` never became ready: its own 60 s timeout fires first; check the command actually serves on the configured port.\n- No `globalTimeout` set, so a hung job runs until the CI provider kills it.\n\n## Network-Level Investigation\n\n```typescript\npage.on('request', r => console.log('>>', r.method(), r.url()));\npage.on('response', r => console.log('<<', r.status(), r.url()));\npage.on('requestfailed', r => console.log('XX', r.url(), r.failure()?.errorText));\npage.on('console', m => console.log('PAGE', m.type(), m.text()));\npage.on('pageerror', e => console.log('PAGEERROR', e.message));\n```\n\nAttach these in a fixture during an investigation, not permanently — the noise buries the signal. The trace already records all of it; use the listeners only when you need output during a live run. Deeper interception, mocking, and HAR replay: `network.md`.\n\n## Debug Interactively\n\n```bash\nnpx playwright test file.spec.ts --debug   # Inspector, timeouts disabled\nnpx playwright test --ui                   # watch mode, time travel, live locator picker\n```\n\n```typescript\nawait page.pause();   // drop into the Inspector at an exact point\n```\n\nUI mode is the better default while iterating: it keeps a trace for every run, lets you edit and re-run a single test, and its locator picker generates a locator from a click — faster than guessing and verifying in a rerun loop.\n\n## Failure Artifacts Worth Keeping\n\n```typescript\n// playwright.config.ts\nuse: {\n  trace: 'on-first-retry',        // 'on' bloats storage and slows every run\n  screenshot: 'only-on-failure',\n  video: 'retain-on-failure',\n}\n```\n\nCustom evidence for a stubborn failure:\n\n```typescript\ntest.afterEach(async ({ page }, testInfo) => {\n  if (testInfo.status !== testInfo.expectedStatus) {\n    await testInfo.attach('dom', { body: await page.content(), contentType: 'text/html' });\n  }\n});\n```\n\nAttachments land in the HTML report next to the failure — better than a console dump nobody scrolls to.\n\n## When You Are Truly Stuck\n\nRebuild the test forward from `page.goto`, one action at a time in `--debug`, asserting after each. The first step whose assertion needs an explanation is the bug — and it is usually two steps before the one that was failing.\n\nFile v1.0.4:files.md\n\n# Files — Uploads, Downloads, PDFs, and Clipboard\n\nFile flows fail for one reason: the browser event is asynchronous and the test acts after it fired. Register the waiter first, always.\n\n## Uploads\n\n```typescript\nawait page.getByLabel('Avatar').setInputFiles('fixtures/avatar.png');\nawait page.getByLabel('Attachments').setInputFiles(['a.pdf', 'b.pdf']);   // multiple\nawait page.getByLabel('Avatar').setInputFiles([]);                        // clear the input\n\n// In-memory file, no fixture on disk\nawait page.getByLabel('CSV').setInputFiles({\n  name: 'data.csv', mimeType: 'text/csv', buffer: Buffer.from('id,name\\n1,Alice'),\n});\n```\n\n- `setInputFiles` targets the `<input type=\"file\">` itself, and it works even when the input is **hidden** behind a styled button — the usual mistake is locating the pretty button instead of the input.\n- No visible input at all (drag-drop zones, custom pickers): intercept the chooser.\n\n```typescript\nconst chooserPromise = page.waitForEvent('filechooser');\nawait page.getByRole('button', { name: 'Upload' }).click();\nconst chooser = await chooserPromise;\nawait chooser.setFiles('fixtures/report.pdf');\n```\n\n- Assert the **result**, not the call: `await expect(page.getByText('avatar.png')).toBeVisible()`. `setInputFiles` resolving proves nothing about the upload.\n- Large files: the upload request is a real request — wait for its response, not a fixed delay.\n- Fixture paths are resolved relative to the current working directory; use `path.join(__dirname, 'fixtures', ...)` so the test survives being run from the repo root or its own folder.\n\n## Drag And Drop Of Files\n\nHTML5 drop zones need a synthetic `DataTransfer`:\n\n```typescript\nconst dataTransfer = await page.evaluateHandle(async () => {\n  const dt = new DataTransfer();\n  dt.items.add(new File(['id,name\\n1,Alice'], 'data.csv', { type: 'text/csv' }));\n  return dt;\n});\nawait page.getByTestId('dropzone').dispatchEvent('drop', { dataTransfer });\n```\n\nElement-to-element dragging is different and simpler: `await source.dragTo(target)`. When a drag library needs intermediate movement, drive it manually — `mouse.down()`, two or three `mouse.move()` steps, `mouse.up()` — because most libraries ignore a single jump.\n\n## Downloads\n\n```typescript\nconst downloadPromise = page.waitForEvent('download');\nawait page.getByRole('button', { name: 'Export CSV' }).click();\nconst download = await downloadPromise;\n\nexpect(download.suggestedFilename()).toBe('report.csv');\nconst stream = await download.createReadStream();          // assert content without touching disk\nawait download.saveAs(test.info().outputPath('report.csv'));\n```\n\n- Downloads live in a temp dir and are **deleted when the context closes** unless saved. `saveAs` into `testInfo.outputPath` puts them next to the test's other artifacts and cleans up with them.\n- `acceptDownloads` is on by default; turning it off makes downloads fail rather than pause.\n- `download.path()` waits for the transfer to finish — use it (or the stream) before asserting size or content.\n- Some apps download by navigating to a URL that responds with `Content-Disposition`. If nothing fires, check whether the click opened a new tab: listen on the context, not the page.\n- Faster and more precise for content assertions: get the URL from the button, then `page.request.get(url)` and assert on the response body. The browser only needs to prove the link is right.\n\n## PDFs\n\n```typescript\nawait page.pdf({ path: 'out.pdf', format: 'A4', printBackground: true });\n```\n\n- **Chromium headless only.** Firefox, WebKit, and headed Chromium throw.\n- `page.pdf` renders the print stylesheet, not the screen: `await page.emulateMedia({ media: 'print' })` first if you want to see what you are generating.\n- Asserting a generated PDF: check byte length and page count with a parser, or render to an image and compare visually (`visual.md`). Comparing raw PDF bytes fails on embedded timestamps.\n\n## Clipboard\n\n```typescript\nawait context.grantPermissions(['clipboard-read', 'clipboard-write']);   // Chromium\nawait page.getByRole('button', { name: 'Copy link' }).click();\nconst text = await page.evaluate(() => navigator.clipboard.readText());\nexpect(text).toContain('/share/');\n```\n\nClipboard permissions are Chromium-specific; in Firefox and WebKit, assert the visible confirmation (\"Copied!\") and cover the copied value in a unit test instead. Keyboard paste (`Meta+V` / `Control+V`) also depends on the platform modifier — read it from `process.platform` or use `ControlOrMeta`.\n\n## File System Boundaries\n\n- Write only into `test.info().outputPath(...)` or the system temp dir. Never into the user's home, never into `~/Clawic/data/playwright/`.\n- Generated fixtures belong in `.gitignore`; committed fixtures stay small and are checked for real content (a 0-byte PNG passes an upload and fails an assertion later).\n- Clean up in fixture teardown, after `use()`, so it runs even when the test fails.\n\nFile v1.0.4:fixtures.md\n\n# Fixtures — Setup, Page Objects, and Test Data\n\nWhere a test gets its world from. Config and projects: `config.md`. Accounts and sessions: `auth.md`.\n\n## Fixtures Over Helpers\n\n```typescript\nimport { test as base } from '@playwright/test';\n\ntype Fixtures = { cart: CartPage };\ntype WorkerFixtures = { seededOrg: Org };\n\nexport const test = base.extend<Fixtures, WorkerFixtures>({\n  cart: async ({ page }, use) => {\n    await page.goto('/cart');\n    await use(new CartPage(page));\n  },\n  seededOrg: [async ({}, use, workerInfo) => {\n    const org = await api.createOrg(`w${workerInfo.parallelIndex}`);\n    await use(org);\n    await api.deleteOrg(org.id);      // teardown after the LAST test in this worker\n  }, { scope: 'worker' }],\n});\n```\n\n- Test-scoped fixtures run per test; worker-scoped run once per worker process and are the right home for expensive setup (a seeded tenant, a running service).\n- Everything after `use()` is teardown and runs even when the test fails — the only reliable cleanup point.\n- `auto: true` fixtures run without being requested: use for global listeners or per-test tracing, sparingly.\n- Fixtures beat `beforeEach` helpers because a failure inside a fixture is reported as setup, not as a mysterious assertion failure inside the test.\n- Overriding a built-in fixture (`page`, `context`, `baseURL`) is legal and is the clean way to apply a route or an init script to every test: `page: async ({ page }, use) => { await page.route(...); await use(page); }`.\n\n## Page Objects: Late, Not Early\n\nExtract on the third repetition. Rules that keep them useful:\n\n```typescript\nexport class CheckoutPage {\n  constructor(private page: Page) {}\n  readonly total = this.page.getByTestId('total-price');\n  readonly pay = this.page.getByRole('button', { name: 'Pay' });\n\n  async removeItem(name: string) {\n    await this.page.getByRole('listitem').filter({ hasText: name })\n      .getByRole('button', { name: 'Remove' }).click();\n  }\n}\n```\n\n- Expose **locators**, not `getTotal(): Promise<string>` accessors — the caller keeps retrying assertions.\n- No assertions inside page objects except an explicit `expectX()` method; a page object that asserts hides which check failed.\n- No conditional branching on page state inside the object; that is the test's decision.\n- Hand the object out through a fixture rather than constructing it in every test: one place changes when the constructor grows a dependency.\n\n## Data Strategy\n\n| Approach | Use when | Cost |\n|---|---|---|\n| Create via API in a fixture | Default | Fast, isolated, needs API access |\n| Seed script before the run | Read-only reference data | Shared state risk if tests mutate it |\n| Create through the UI | The creation flow is what is under test | Slowest, couples every test to that flow |\n| Fixed test accounts | Legacy systems with no provisioning API | Needs per-worker allocation (`auth.md`) |\n\nUnique-value rule: derive from `workerInfo.parallelIndex` plus a timestamp, never a bare `Date.now()` — two workers starting in the same millisecond collide on a unique constraint, and that failure looks exactly like flake.\n\nCleanup belongs to the fixture that created the record, in the teardown half after `use()`. A test that deletes data it did not create is the polluter in someone else's failure.\n\nArchive v1.0.3: 8 files, 14891 bytes\n\nFiles: ci-cd.md (3501b), debugging.md (3624b), scraping.md (3968b), selectors.md (2384b), skill-card.md (3050b), SKILL.md (10970b), testing.md (4512b), _meta.json (129b)\n\nFile v1.0.3:SKILL.md\n\n---\nname: Playwright (Automation + MCP + Scraper)\nslug: playwright\nversion: 1.0.3\nhomepage: https://clawic.com/skills/playwright\ndescription: \"Browser automation via Playwright MCP. Navigate websites, click elements, fill forms, take screenshots, extract data, and debug real browser workflows. Use when (1) you need a real browser, not static fetch; (2) the task involves Playwright MCP, browser tools, Playwright tests, scripts, or JS-rendered pages; (3) the user wants navigation, forms, screenshots, PDFs, downloads, or browser-driven extraction turned into a reliable outcome.\"\nchangelog: Clarified the MCP-first browser automation flow and improved quick-start guidance for forms, screenshots, and extraction.\nmetadata: {\"clawdbot\":{\"emoji\":\"P\",\"requires\":{\"bins\":[\"node\",\"npx\"]},\"os\":[\"linux\",\"darwin\",\"win32\"],\"install\":[{\"id\":\"npm-playwright\",\"kind\":\"npm\",\"package\":\"playwright\",\"bins\":[\"playwright\"],\"label\":\"Install Playwright\"},{\"id\":\"npm-playwright-mcp\",\"kind\":\"npm\",\"package\":\"@playwright/mcp\",\"bins\":[\"playwright-mcp\"],\"label\":\"Install Playwright MCP (optional)\"}]}}\n---\n\n## When to Use\n\nUse this skill for real browser tasks: JS-rendered pages, multi-step forms, screenshots or PDFs, UI debugging, Playwright test authoring, MCP-driven browser control, and structured extraction from rendered pages.\n\nPrefer it when static fetch is insufficient or when the task depends on browser events, visible DOM state, authentication context, uploads or downloads, or user-facing rendering.\n\nIf the user mainly wants the agent to drive a browser with simple actions like navigate, click, fill, screenshot, download, or extract, treat MCP as a first-class path.\n\nUse direct Playwright for scripts and tests. Use MCP when browser tools are already in the loop, the user explicitly wants MCP, or the fastest path is browser actions rather than writing new automation code.\n\nPrimary fit is repo-owned browser work: tests, debugging, repros, screenshots, and deterministic automation. Treat rendered-page extraction as a secondary use case, not the default identity.\n\n## Architecture\n\nThis skill is instruction-only. It does not create local memory, setup folders, or persistent profiles by default.\n\nLoad only the smallest reference file needed for the task. Keep auth state temporary unless the repository already standardizes it and the user explicitly wants browser-session reuse.\n\n## Quick Start\n\n### MCP browser path\n```bash\nnpx @playwright/mcp --headless\n```\n\nUse this path when the agent already has browser tools available or the user wants browser automation without writing new Playwright code.\n\n### Common MCP actions\n\nTypical Playwright MCP tool actions include:\n- `browser_navigate` for opening a page\n- `browser_click` and `browser_press` for interaction\n- `browser_type` and `browser_select_option` for forms\n- `browser_snapshot` and `browser_evaluate` for inspection and extraction\n- `browser_choose_file` for uploads\n- screenshot, PDF, trace, and download capture through the active browser workflow\n\n### Common browser outcomes\n\n| Goal | Typical MCP-style action |\n|------|--------------------------|\n| Open and inspect a site | navigate, wait, inspect, screenshot |\n| Complete a form | navigate, click, fill, select, submit |\n| Capture evidence | screenshot, PDF, download, trace |\n| Pull structured page data | navigate, wait for rendered state, extract |\n| Reproduce a UI bug | headed run, trace, console or network inspection |\n\n### Existing test suite\n```bash\nnpx playwright test\nnpx playwright test --headed\nnpx playwright test --trace on\n```\n\n### Bootstrap selectors and flows\n```bash\nnpx playwright codegen https://example.com\n```\n\n### Direct script path\n```javascript\nconst { chromium } = require('playwright');\n\n(async () => {\n  const browser = await chromium.launch({ headless: true });\n  const page = await browser.newPage();\n  await page.goto('https://example.com');\n  await page.screenshot({ path: 'page.png', fullPage: true });\n  await browser.close();\n})();\n```\n\n## Quick Reference\n\n| Topic | File |\n|------|------|\n| Selector strategy and frame handling | `selectors.md` |\n| Failure analysis, traces, logs, and headed runs | `debugging.md` |\n| Test architecture, mocks, auth, and assertions | `testing.md` |\n| CI defaults, retries, workers, and failure artifacts | `ci-cd.md` |\n| Rendered-page extraction, pagination, and respectful throttling | `scraping.md` |\n\n## Approach Selection\n\n| Situation | Best path | Why |\n|----------|-----------|-----|\n| Static HTML or a simple HTTP response is enough | Use a cheaper fetch path first | Faster, cheaper, less brittle |\n| You need a reliable first draft of selectors or flows | Start with `codegen` or a headed exploratory run | Faster than guessing selectors from source or stale DOM |\n| Local app, staging app, or repo-owned E2E suite | Use `@playwright/test` | Best fit for repeatable tests and assertions |\n| One-off browser automation, screenshots, downloads, or rendered extraction | Use direct Playwright API | Simple, explicit, and easy to debug in code |\n| Agent/browser-tool workflow already depends on `browser_*` tools or the user wants no-code browser control | Use Playwright MCP | Fastest path for navigate-click-fill-screenshot workflows |\n| CI failures, flake, or environment drift | Start with `debugging.md` and `ci-cd.md` | Traces and artifacts matter more than new code |\n\n## Core Rules\n\n### 1. Test user-visible behavior and the real browser boundary\n- Do not spend Playwright on implementation details that unit or API tests can cover more cheaply.\n- Use Playwright when success depends on rendered UI, actionability, auth, uploads or downloads, navigation, or browser-only behavior.\n\n### 2. Make runs isolated before making them clever\n- Keep tests and scripts independent so retries, parallelism, and reruns do not inherit hidden state.\n- Extend the repository's existing Playwright harness, config, and fixtures before inventing a parallel testing shape from scratch.\n- Do not share mutable accounts, browser state, or server-side data across parallel runs unless the suite was explicitly designed for it.\n\n### 3. Reconnaissance before action\n- Open, wait, and inspect the rendered state before locking selectors or assertions.\n- Use `codegen`, headed mode, or traces to discover stable locators instead of guessing from source or stale DOM.\n- For flaky or CI-only failures, capture a trace before rewriting selectors or waits.\n\n### 4. Prefer resilient locators and web-first assertions\n- Use role, label, text, alt text, title, or test ID before CSS or XPath.\n- Assert the user-visible outcome with Playwright assertions instead of checking only that a click or fill command executed.\n- If a locator is ambiguous, disambiguate it. Do not silence strictness with `first()`, `last()`, or `nth()` unless position is the actual behavior under test.\n\n### 5. Wait on actionability and app state, not arbitrary time\n- Let Playwright's actionability checks work for you before reaching for sleeps or forced actions.\n- Prefer `expect`, URL waits, response waits, and explicit app-ready signals over generic timing guesses.\n\n### 6. Control what you do not own\n- Mock or isolate third-party services, flaky upstream APIs, analytics noise, and cross-origin dependencies whenever the goal is to verify your app.\n- For rendered extraction, prefer documented APIs or plain HTTP paths before driving a full browser.\n- Do not make live third-party widgets or upstream integrations the reason your suite flakes unless that exact integration is what the user asked to validate.\n\n### 7. Keep auth, production access, and persistence explicit\n- Do not persist saved browser state by default.\n- Reuse auth state only when the repository already standardizes it or the user explicitly asks for session reuse.\n- For destructive, financial, medical, production, or otherwise high-stakes flows, prefer staging or local environments and require explicit user confirmation before continuing.\n\n## Playwright Traps\n\n- Guessing selectors from source or using `first()`, `last()`, or `nth()` to silence ambiguity -> the automation works once and then flakes.\n- Starting a new Playwright structure when the repo already has config, fixtures, auth setup, or conventions -> the new flow fights the existing harness and wastes time.\n- Testing internal implementation details instead of visible outcomes -> the suite passes while the user path is still broken.\n- Sharing one authenticated state across parallel tests that mutate server-side data -> failures become order-dependent and hard to trust.\n- Reaching for `force: true` before understanding overlays, disabled state, or actionability -> the test hides a real bug.\n- Waiting on `networkidle` for chatty SPAs -> analytics, polling, or sockets keep the page \"busy\" even when the UI is ready.\n- Driving a full browser when HTTP or an API would answer the question -> more cost, more flake, less signal.\n- Treating third-party widgets and live upstream services as if they were stable parts of your own product -> failures stop being actionable.\n\n## External Endpoints\n\n| Endpoint | Data Sent | Purpose |\n|----------|-----------|---------|\n| User-requested web origins | Browser requests, form input, cookies, uploads, and page interactions required by the task | Automation, testing, screenshots, PDFs, and rendered extraction |\n| `https://registry.npmjs.org` | Package metadata and tarballs during optional installation | Install Playwright or Playwright MCP |\n\nNo other data is sent externally.\n\n## Security & Privacy\n\nData that leaves your machine:\n- Requests sent to the websites the user asked to automate.\n- Optional package-install traffic to npm when installing Playwright tooling.\n\nData that stays local:\n- Source code, traces, screenshots, videos, PDFs, and temporary browser state kept in the workspace or system temp directory.\n\nThis skill does NOT:\n- Create hidden memory files or local folder systems.\n- Recommend browser-fingerprint hacks, challenge-solving services, or rotating exits.\n- Persist sessions or credentials by default.\n- Make undeclared network requests beyond the target sites involved in the task and optional tool installation.\n- Treat high-stakes production flows as safe to automate without explicit user direction.\n\n## Trust\n\nBy using this skill, browser requests go to the websites you automate and optional package downloads go through npm.\nOnly install if you trust those services and the sites involved in your workflow.\n\n## Related Skills\nInstall with `clawhub install <slug>` if user confirms:\n- `web` - HTTP-first investigation before escalating to a real browser.\n- `scrape` - Broader extraction workflows when browser automation is not the main challenge.\n- `screenshots` - Capture and polish visual artifacts after browser work.\n- `multi-engine-web-search` - Find and shortlist target pages before automating them.\n\n## Feedback\n- If useful: `clawhub star playwright`\n- Stay updated: `clawhub sync`\n\nFile v1.0.3:_meta.json\n\n{\n  \"ownerId\": \"kn73vp5rarc3b14rc7wjcw8f8580t5d1\",\n  \"slug\": \"playwright\",\n  \"version\": \"1.0.3\",\n  \"publishedAt\": 1773251092810\n}\n\nFile v1.0.3:ci-cd.md\n\n# CI Success Defaults\n\n## GitHub Actions\n\n```yaml\nname: Playwright Tests\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      \n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n      \n      - name: Install dependencies\n        run: npm ci\n      \n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps\n      \n      - name: Run tests\n        run: npx playwright test\n      \n      - uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 7\n```\n\nUse the official Playwright image or install browsers explicitly. Always keep traces and failure artifacts.\n\n## GitLab CI\n\n```yaml\nplaywright:\n  image: mcr.microsoft.com/playwright:v1.40.0-jammy\n  stage: test\n  script:\n    - npm ci\n    - npx playwright test\n  artifacts:\n    when: on_failure\n    paths:\n      - playwright-report/\n    expire_in: 7 days\n```\n\n## Docker Setup\n\n```dockerfile\nFROM mcr.microsoft.com/playwright:v1.40.0-jammy\n\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\n\nCMD [\"npx\", \"playwright\", \"test\"]\n```\n\n## When to Add Sharding\n\n```yaml\n# GitHub Actions matrix\njobs:\n  test:\n    strategy:\n      matrix:\n        shard: [1, 2, 3, 4]\n    steps:\n      - name: Run tests\n        run: npx playwright test --shard=${{ matrix.shard }}/4\n```\n\nDo not shard a small or unstable suite just to look faster. Add sharding only after the suite is already deterministic.\n\n## playwright.config.ts for CI\n\n```typescript\nimport { defineConfig, devices } from '@playwright/test';\n\nexport default defineConfig({\n  testDir: './tests',\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 4 : undefined,\n  reporter: process.env.CI \n    ? [['html'], ['github']] \n    : [['html']],\n  \n  use: {\n    trace: 'on-first-retry',\n    screenshot: 'only-on-failure',\n    video: 'on-first-retry',\n  },\n\n  projects: [\n    {\n      name: 'chromium',\n      use: { ...devices['Desktop Chrome'] },\n    },\n    {\n      name: 'firefox',\n      use: { ...devices['Desktop Firefox'] },\n    },\n    {\n      name: 'webkit',\n      use: { ...devices['Desktop Safari'] },\n    },\n  ],\n});\n```\n\n## Caching Browsers\n\n```yaml\n# GitHub Actions\n- name: Cache Playwright browsers\n  uses: actions/cache@v4\n  with:\n    path: ~/.cache/ms-playwright\n    key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}\n```\n\n## Environment Variables\n\n```yaml\nenv:\n  BASE_URL: https://staging.example.com\n  CI: true\n```\n\n```typescript\n// playwright.config.ts\nuse: {\n  baseURL: process.env.BASE_URL || 'http://localhost:3000',\n}\n```\n\n## Flaky Test Management\n\n```typescript\n// Mark known flaky test\ntest('sometimes fails', {\n  annotation: { type: 'flaky', description: 'Network timing issue' },\n}, async ({ page }) => {\n  // test code\n});\n\n// Retry configuration\nexport default defineConfig({\n  retries: 2,\n  expect: {\n    timeout: 10000,\n  },\n});\n```\n\n## Common CI Issues\n\n| Issue | Fix |\n|-------|-----|\n| Browsers not found | Use official Playwright Docker image |\n| Display errors | Headless mode or `xvfb-run` |\n| Out of memory | Reduce workers, close contexts |\n| Timeouts | Increase `actionTimeout`, add retries |\n| Inconsistent screenshots | Set fixed viewport, disable animations |\n| Order-dependent failures | Remove shared auth or shared mutable test data |\n\nFile v1.0.3:debugging.md\n\n# Debugging Guide\n\n## First Moves\n\n1. Reproduce in headed mode.\n2. Capture a trace before rewriting selectors or waits.\n3. Check whether the failure is selector drift, actionability, environment drift, shared state, or a real product regression.\n\n## Inspector and Headed Runs\n\n```bash\nnpx playwright test --debug\nnpx playwright test my-test.spec.ts --debug\nnpx playwright test --headed\n```\n\n```typescript\nawait page.pause();\n```\n\n## Trace Viewer\n\n```bash\nnpx playwright test --trace on\nnpx playwright show-trace trace.zip\n```\n\n```typescript\nuse: {\n  trace: 'retain-on-failure',\n}\n```\n\nStart with traces for CI and flaky failures. Use screenshots and videos as supporting evidence, not as the primary debugging tool.\n\n## Common Errors\n\n### Element Not Found\n\nUse explicit waits and confirm the right frame or shadow boundary before rewriting selectors. If the locator is ambiguous, improve the locator instead of clicking the first match.\n\n```typescript\nawait page.waitForSelector('.element');\nconst frame = page.frameLocator('iframe');\nawait frame.locator('.element').click();\nawait page.click('.element', { timeout: 60000 });\n```\n\n### Flaky Click\n\nCheck visibility, scrolling, overlays, and disabled state before forcing the click.\n\n```typescript\nawait page.locator('.btn').waitFor({ state: 'visible' });\nawait page.locator('.btn').scrollIntoViewIfNeeded();\nawait page.locator('.btn').click();\n```\n\nUse `force: true` only after confirming that the overlay or disabled state is not the real bug.\n\nIf the click target keeps changing, inspect actionability conditions first: visible, stable, enabled, and actually receiving pointer events.\n\n### Timeout in CI\n\nSlow environments usually need better waits, traces, or fewer workers before they need bigger timeouts.\n\n```typescript\nexport default defineConfig({\n  timeout: 60000,\n  expect: { timeout: 10000 },\n});\n\nawait expect.poll(async () => {\n  return await page.locator('.items').count();\n}, { timeout: 30000 }).toBeGreaterThan(5);\n```\n\n### Network Issues\n\n```typescript\npage.on('request', request => {\n  console.log('>>', request.method(), request.url());\n});\n\npage.on('response', response => {\n  console.log('<<', response.status(), response.url());\n});\n\nconst responsePromise = page.waitForResponse('**/api/data');\nawait page.click('.load-data');\nconst response = await responsePromise;\n```\n\n## Failure Artifacts\n\n```typescript\ntest.afterEach(async ({ page }, testInfo) => {\n  if (testInfo.status !== 'passed') {\n    await page.screenshot({\n      path: `screenshots/${testInfo.title}.png`,\n      fullPage: true,\n    });\n  }\n});\n```\n\n## Console and Runtime Errors\n\n```typescript\npage.on('console', msg => {\n  console.log('PAGE LOG:', msg.text());\n});\n\npage.on('pageerror', error => {\n  console.log('PAGE ERROR:', error.message);\n});\n```\n\n## Compare Local vs CI\n\n| Check | Command |\n|-------|---------|\n| Viewport | `await page.viewportSize()` |\n| User agent | `await page.evaluate(() => navigator.userAgent)` |\n| Timezone | `await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone)` |\n| Network | `page.on('request', ...)` |\n| Shared auth/data | verify whether tests mutate the same account or fixtures |\n\n## Debugging Checklist\n\n1. Run with `--debug` or `--headed`.\n2. Add `await page.pause()` before the failure point.\n3. Capture trace, screenshot, and console output before changing selectors.\n4. Check for iframes, shadow DOM, overlays, loading states, and shared auth or data collisions.\n5. Compare viewport, network behavior, workers, and environment flags between local and CI.\n6. Only then rewrite selectors, waits, fixtures, or test structure.\n\nFile v1.0.3:scraping.md\n\n# Rendered-Page Extraction Patterns\n\nUse browser extraction only when the data is hidden behind rendering, client-side interactions, pagination, or downloads. If a plain HTTP fetch or documented API can answer the question, use that first.\n\n## Basic Extraction\n\n```typescript\nconst browser = await chromium.launch();\nconst page = await browser.newPage();\nawait page.goto('https://example.com/products');\nawait page.waitForSelector('.product-card');\n\nconst products = await page.$$eval('.product-card', cards =>\n  cards.map(card => ({\n    name: card.querySelector('.name')?.textContent?.trim(),\n    price: card.querySelector('.price')?.textContent?.trim(),\n    url: card.querySelector('a')?.href,\n  }))\n);\n\nawait browser.close();\n```\n\n## Wait Strategies for Dynamic Apps\n\n```typescript\nawait page.waitForSelector('[data-loaded=\"true\"]');\nawait page.waitForSelector('.loading-spinner', { state: 'hidden' });\n\nawait expect.poll(async () => {\n  return await page.locator('.product').count();\n}).toBeGreaterThan(0);\n```\n\nUse `networkidle` only when the app genuinely becomes quiet. Polling, analytics, and sockets often make it the wrong condition.\n\n## Infinite Scroll\n\n```typescript\nasync function scrollToBottom(page: Page) {\n  let previousHeight = 0;\n  let previousCount = 0;\n\n  while (true) {\n    const currentHeight = await page.evaluate(() => document.body.scrollHeight);\n    if (currentHeight === previousHeight) break;\n\n    previousHeight = currentHeight;\n    previousCount = await page.locator('.product-card').count();\n    await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));\n    await expect.poll(async () => {\n      return await page.locator('.product-card').count();\n    }).toBeGreaterThan(previousCount);\n  }\n}\n```\n\n## Pagination\n\n```typescript\nasync function scrapeAllPages(page: Page) {\n  const allData = [];\n\n  while (true) {\n    allData.push(...await extractData(page));\n\n    const nextButton = page.getByRole('button', { name: 'Next' });\n    if (!(await nextButton.isVisible()) || await nextButton.isDisabled()) break;\n\n    await nextButton.click();\n    await page.locator('.results').waitFor();\n  }\n\n  return allData;\n}\n```\n\n## Retries and Error Handling\n\n```typescript\nasync function scrapeWithRetry(url: string, retries = 3) {\n  for (let attempt = 1; attempt <= retries; attempt++) {\n    const page = await context.newPage();\n    try {\n      await page.goto(url, { timeout: 30000 });\n      return await extractData(page);\n    } catch (error) {\n      if (attempt === retries) throw error;\n      await new Promise(resolve => setTimeout(resolve, 1000 * attempt));\n    } finally {\n      await page.close();\n    }\n  }\n}\n```\n\n## Respectful Throttling\n\n```typescript\nclass RateLimiter {\n  private lastRequest = 0;\n\n  constructor(private minDelay: number) {}\n\n  async wait() {\n    const elapsed = Date.now() - this.lastRequest;\n    if (elapsed < this.minDelay) {\n      await new Promise(resolve => setTimeout(resolve, this.minDelay - elapsed));\n    }\n    this.lastRequest = Date.now();\n  }\n}\n```\n\nThrottle multi-page work, respect robots and terms where applicable, and keep scope aligned with the user's request.\n\n## Structured Data Extraction\n\n```typescript\nconst jsonLd = await page.$eval(\n  'script[type=\"application/ld+json\"]',\n  el => JSON.parse(el.textContent || '{}')\n);\n\nconst tableData = await page.$$eval('table tbody tr', rows =>\n  rows.map(row =>\n    Array.from(row.querySelectorAll('td')).map(td => td.textContent?.trim())\n  )\n);\n```\n\nWhen extracting repeated items, prefer locating the correct collection first and only then evaluating over that collection. Do not scrape the whole page if the user only needs one bounded region.\n\n## Avoid by Default\n\n- Browser-fingerprint hacks, rotating exits, or challenge-solving services.\n- Blind session persistence across tasks.\n- Extraction plans that ignore cheaper API or HTTP paths.\n- Wide crawls when the user only asked for one page, one result set, or one bounded workflow.\n\nFile v1.0.3:selectors.md\n\n# Selector Strategies\n\n## Hierarchy (Most to Least Resilient)\n\n### 1. Role-Based (Best)\n```typescript\npage.getByRole('button', { name: 'Submit' })\npage.getByRole('link', { name: /sign up/i })\npage.getByRole('heading', { level: 1 })\npage.getByRole('textbox', { name: 'Email' })\n```\n\n### 2. Test IDs (Explicit)\n```typescript\npage.getByTestId('checkout-button')\npage.getByTestId('product-card')\n```\nConfigure in `playwright.config.ts`:\n```typescript\nuse: { testIdAttribute: 'data-testid' }\n```\n\n### 3. Label/Placeholder (Forms)\n```typescript\npage.getByLabel('Email address')\npage.getByPlaceholder('Enter your email')\n```\n\n### 4. Text Content (Visible)\n```typescript\npage.getByText('Add to Cart', { exact: true })\npage.getByText(/welcome/i)  // regex for flexibility\n```\n\n### 5. CSS (Last Resort)\n```typescript\n// Avoid these patterns:\npage.locator('.css-1a2b3c')  // generated class\npage.locator('div > span:nth-child(2)')  // positional\npage.locator('#root > div > div > button')  // deep nesting\n\n// Acceptable:\npage.locator('[data-product-id=\"123\"]')  // semantic attribute\npage.locator('form.login-form')  // stable class\n```\n\n## Chaining and Filtering\n\n```typescript\npage.getByRole('listitem').filter({ hasText: 'Product A' })\n\npage.getByTestId('cart').getByRole('button', { name: 'Remove' })\n```\n\nPrefer filtering or parent-child chaining over `first()`, `last()`, or `nth()`. Use positional locators only when order is the thing being tested or there is genuinely no stable identity.\n\nIf a locator matches multiple elements, do not silence strictness with position by default. Disambiguate the locator until it represents the intended target.\n\n## Frame Handling\n\n```typescript\n// Named frame\nconst frame = page.frameLocator('iframe[name=\"checkout\"]')\nframe.getByRole('button', { name: 'Pay' }).click()\n\n// Frame by URL\npage.frameLocator('iframe[src*=\"stripe\"]')\n```\n\n## Shadow DOM\n\n```typescript\n// Playwright pierces shadow DOM by default\npage.locator('my-component').getByRole('button')\n```\n\n## Common Mistakes\n\n| Mistake | Better |\n|---------|--------|\n| `page.locator('button').click()` | `page.getByRole('button', { name: 'Submit' }).click()` |\n| `page.getByTestId('product-card').first()` | filter or chain until only the intended card matches |\n| `nth-child(3)` | Filter by text, role, test ID, or parent context |\n| `//div[@class=\"xyz\"]/span[2]` | Role-based or test ID |\n\nFile v1.0.3:skill-card.md\n\n## Description: <br>\nBrowser automation guidance via Playwright MCP for navigating sites, clicking elements, filling forms, taking screenshots, extracting data, debugging workflows, and authoring tests or scripts. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[ivangdavila](https://clawhub.ai/user/ivangdavila) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and engineers use this skill for real-browser workflows that require rendered page state, user interaction, screenshots, downloads, Playwright tests, MCP-driven browser control, or structured extraction when static HTTP is insufficient. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: Browser automation can send form inputs, cookies, uploads, and page interactions to user-requested web origins. <br>\nMitigation: Use the skill only on sites and apps the user is authorized to test, and prefer local or staging environments for sensitive workflows. <br>\nRisk: Screenshots, videos, PDFs, traces, reports, and temporary browser state may capture credentials or private page data. <br>\nMitigation: Restrict access and retention for generated artifacts, and avoid persistent browser sessions unless the repository already standardizes that workflow or the user explicitly requests it. <br>\nRisk: Optional installation of Playwright tooling downloads package metadata and tarballs from npm. <br>\nMitigation: Install only when the user trusts npm package retrieval and the local environment is allowed to fetch Playwright or Playwright MCP dependencies. <br>\n\n\n## Reference(s): <br>\n- [ClawHub Skill Page](https://clawhub.ai/ivangdavila/playwright) <br>\n- [Skill Homepage](https://clawic.com/skills/playwright) <br>\n- [Playwright npm Package](https://registry.npmjs.org/playwright) <br>\n- [Playwright MCP npm Package](https://registry.npmjs.org/@playwright/mcp) <br>\n- [Selector Strategy and Frame Handling](artifact/selectors.md) <br>\n- [Debugging Guide](artifact/debugging.md) <br>\n- [Testing Patterns](artifact/testing.md) <br>\n- [CI Success Defaults](artifact/ci-cd.md) <br>\n- [Rendered-Page Extraction Patterns](artifact/scraping.md) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Guidance, Markdown, Code, Shell commands, Configuration] <br>\n**Output Format:** [Markdown with inline bash, JavaScript, TypeScript, YAML, and Dockerfile examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [May recommend browser actions, test commands, Playwright configuration, CI snippets, screenshots, PDFs, traces, reports, or extraction scripts depending on the user task.] <br>\n\n## Skill Version(s): <br>\n1.0.3 (source: frontmatter and server release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nFile v1.0.3:testing.md\n\n# Testing Patterns\n\n## What to Test First\n\nPrioritize critical user journeys, auth boundaries, payments, file upload or download flows, and state transitions that are expensive to break.\n\nDo not spend E2E budget on trivial presentational details that cheaper unit or component tests can cover.\n\nPrefer assertions on what the user can observe: visible state, text, URL, enabled or disabled controls, downloads, navigation, and persisted app state.\n\n## Test Structure\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Checkout Flow', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('/products');\n  });\n\n  test('completes purchase with valid card', async ({ page }) => {\n    await page.getByTestId('product-card')\n      .filter({ hasText: 'Product A' })\n      .click();\n    await page.getByRole('button', { name: 'Add to Cart' }).click();\n    await page.getByRole('link', { name: 'Checkout' }).click();\n    await expect(page.getByRole('heading', { name: 'Order Summary' })).toBeVisible();\n  });\n});\n```\n\n## Page Object Model\n\nUse page objects when a flow is reused across many tests. Do not build a giant abstraction layer before duplication is real.\n\n```typescript\nexport class CheckoutPage {\n  constructor(private page: Page) {}\n\n  readonly cartItems = this.page.getByTestId('cart-item');\n  readonly checkoutButton = this.page.getByRole('button', { name: 'Checkout' });\n  readonly totalPrice = this.page.getByTestId('total-price');\n\n  async removeItem(name: string) {\n    await this.cartItems\n      .filter({ hasText: name })\n      .getByRole('button', { name: 'Remove' })\n      .click();\n  }\n\n  async expectTotal(amount: string) {\n    await expect(this.totalPrice).toHaveText(amount);\n  }\n}\n```\n\n## Fixtures\n\n```typescript\nimport { test as base } from '@playwright/test';\nimport { CheckoutPage } from './pages/checkout.page';\n\ntype Fixtures = {\n  checkoutPage: CheckoutPage;\n};\n\nexport const test = base.extend<Fixtures>({\n  checkoutPage: async ({ page }, use) => {\n    await page.goto('/checkout');\n    await use(new CheckoutPage(page));\n  },\n});\n```\n\n## Isolation Rules\n\n- Keep tests independent so they can run alone, in parallel, or after retries without hidden dependencies.\n- If tests mutate shared backend state, use dedicated accounts, seeded data, or per-worker isolation instead of reusing one mutable user everywhere.\n- When auth is shared, prefer the Playwright setup-project pattern or one account per worker over ad hoc state reuse.\n\n## Mock What You Do Not Need to Re-Test\n\n```typescript\ntest('shows error on API failure', async ({ page }) => {\n  await page.route('**/api/checkout', route => {\n    route.fulfill({\n      status: 500,\n      body: JSON.stringify({ error: 'Payment failed' }),\n    });\n  });\n\n  await page.goto('/checkout');\n  await page.getByRole('button', { name: 'Pay' }).click();\n  await expect(page.getByText('Payment failed')).toBeVisible();\n});\n```\n\nAvoid testing third-party widgets, analytics, payment processors, or upstream APIs end to end unless the point of the test is that exact integration.\n\n## Visual Regression\n\nUse visual assertions for layout or rendering regressions that humans would otherwise miss. Keep viewport, fonts, and animations deterministic.\n\n```typescript\ntest('matches snapshot', async ({ page }) => {\n  await page.goto('/dashboard');\n  await expect(page).toHaveScreenshot('dashboard.png', {\n    maxDiffPixels: 100,\n  });\n});\n```\n\n## Parallelization\n\n```typescript\nexport default defineConfig({\n  workers: process.env.CI ? 4 : undefined,\n  fullyParallel: true,\n});\n\ntest.describe.configure({ mode: 'parallel' });\ntest.describe.configure({ mode: 'serial' });\n```\n\n## Authentication State\n\nPersist auth only when the suite already standardizes that pattern and the stored state is safe to reuse.\n\n```typescript\nconst authFile = 'playwright/.auth/user.json';\n// Reuse a saved auth file only in suites that intentionally standardize it.\n```\n\nFor one-off debugging, privileged accounts, or stateful flows that mutate backend data, prefer logging in inside the test or using isolated worker accounts instead of carrying one shared session everywhere.\n\n## Assertions\n\n```typescript\nawait expect(locator).toBeVisible();\nawait expect(locator).toHaveText('Expected');\nawait expect(locator).toBeEnabled();\nawait expect(locator).toHaveAttribute('href', '/path');\nawait expect(page).toHaveURL(/dashboard/);\n\nawait expect.poll(async () => {\n  return await page.evaluate(() => window.dataLoaded);\n}).toBe(true);\n```\n\nArchive v1.0.2: 7 files, 10164 bytes\n\nFiles: ci-cd.md (3438b), debugging.md (3865b), scraping.md (3454b), selectors.md (2130b), SKILL.md (5346b), testing.md (3538b), _meta.json (129b)\n\nFile v1.0.2:SKILL.md\n\n---\nname: Playwright (Automation + MCP + Scraper)\nslug: playwright\nversion: 1.0.2\nhomepage: https://playwright.dev\ndescription: Browser automation and web scraping with Playwright. Forms, screenshots, data extraction. Works standalone or via MCP. Testing included.\nchangelog: Rewritten with MCP integration and scraping focus. Now covers standalone use and MCP server setup.\n---\n\n## When to Use\n\nUse this skill when you need to:\n- **Scrape a website** (static or JavaScript-rendered)\n- **Automate form filling** (login, checkout, data entry)\n- **Test a web application** (E2E tests, visual regression)\n- **Take screenshots or PDFs** of web pages\n- **Extract data** from tables, lists, or dynamic content\n\n## Decision Matrix\n\n| Scenario | Method | Speed |\n|----------|--------|-------|\n| Static HTML | `web_fetch` tool | ⚡ Fastest |\n| JavaScript-rendered | Playwright direct | 🚀 Fast |\n| AI agent automation | MCP server | 🤖 Integrated |\n| E2E testing | @playwright/test | ✅ Full framework |\n\n## Quick Reference\n\n| Task | File |\n|------|------|\n| E2E testing patterns | `testing.md` |\n| CI/CD integration | `ci-cd.md` |\n| Debugging failures | `debugging.md` |\n| Web scraping patterns | `scraping.md` |\n| Selector strategies | `selectors.md` |\n\n## Core Rules\n\n1. **Never use `waitForTimeout()`** - always wait for specific conditions (element, URL, network)\n2. **Always close the browser** - call `browser.close()` to prevent memory leaks\n3. **Prefer role selectors** - `getByRole()` survives UI changes better than CSS\n4. **Handle dynamic content** - use `waitFor()` before interacting with elements\n5. **Persist auth state** - use `storageState` to save and reuse login sessions\n\n## Quick Start - Common Tasks\n\n### Scrape a Page\n```javascript\nconst { chromium } = require('playwright');\nconst browser = await chromium.launch();\nconst page = await browser.newPage();\nawait page.goto('https://example.com');\nconst text = await page.locator('body').textContent();\nawait browser.close();\n```\n\n### Fill a Form and Submit\n```javascript\nawait page.goto('https://example.com/login');\nawait page.getByLabel('Email').fill('user@example.com');\nawait page.getByLabel('Password').fill('secret');\nawait page.getByRole('button', { name: 'Sign in' }).click();\nawait page.waitForURL('**/dashboard');\n```\n\n### Take a Screenshot\n```javascript\nawait page.goto('https://example.com');\nawait page.screenshot({ path: 'screenshot.png', fullPage: true });\n```\n\n### Extract Table Data\n```javascript\nconst rows = await page.locator('table tr').all();\nconst data = [];\nfor (const row of rows) {\n  const cells = await row.locator('td').allTextContents();\n  data.push(cells);\n}\n```\n\n## Selector Priority\n\n| Priority | Method | Example |\n|----------|--------|---------|\n| 1 | `getByRole()` | `getByRole('button', { name: 'Submit' })` |\n| 2 | `getByLabel()` | `getByLabel('Email')` |\n| 3 | `getByPlaceholder()` | `getByPlaceholder('Search...')` |\n| 4 | `getByTestId()` | `getByTestId('submit-btn')` |\n| 5 | `locator()` | `locator('.class')` - last resort |\n\n## Common Traps\n\n| Trap | Fix |\n|------|-----|\n| Element not found | Add `await locator.waitFor()` before interacting |\n| Flaky clicks | Use `click({ force: true })` or wait for `state: 'visible'` |\n| Timeout in CI | Increase timeout, check viewport size matches local |\n| Auth lost between tests | Use `storageState` to persist cookies |\n| SPA never reaches networkidle | Wait for specific DOM element instead |\n| 403 Forbidden | Check if site blocks headless; try `headless: false` |\n| Blank page after load | Increase wait time or use `waitUntil: 'networkidle'` |\n\n## Handling Sessions\n\n```javascript\n// Save session after login\nawait page.context().storageState({ path: 'auth.json' });\n\n// Reuse session in new context\nconst context = await browser.newContext({ storageState: 'auth.json' });\n```\n\n## MCP Integration\n\nFor AI agents using Model Context Protocol:\n\n```bash\nnpm install -g @playwright/mcp\nnpx @playwright/mcp --headless\n```\n\n### MCP Tools Reference\n\n| Tool | Description |\n|------|-------------|\n| `browser_navigate` | Navigate to URL |\n| `browser_click` | Click element by selector |\n| `browser_type` | Type text into input |\n| `browser_select_option` | Select dropdown option |\n| `browser_get_text` | Get text content |\n| `browser_evaluate` | Execute JavaScript |\n| `browser_snapshot` | Get accessible page snapshot |\n| `browser_close` | Close browser context |\n| `browser_choose_file` | Upload file |\n| `browser_press` | Press keyboard key |\n\n### MCP Server Options\n\n```bash\n--headless              # Run without UI\n--browser chromium      # chromium|firefox|webkit\n--viewport-size 1920x1080\n--timeout-action 10000  # Action timeout (ms)\n--timeout-navigation 30000\n--allowed-hosts example.com,api.example.com\n--save-trace            # Save trace for debugging\n--save-video 1280x720   # Record video\n```\n\n## Installation\n\n```bash\nnpm init playwright@latest\n# Or add to existing project\nnpm install -D @playwright/test\nnpx playwright install chromium\n```\n\n## Related Skills\nInstall with `clawhub install <slug>` if user confirms:\n- `puppeteer` - Alternative browser automation (Chrome-focused)\n- `scrape` - General web scraping patterns and strategies\n- `web` - Web development fundamentals and HTTP handling\n\n## Feedback\n\n- If useful: `clawhub star playwright`\n- Stay updated: `clawhub sync`\n\nFile v1.0.2:_meta.json\n\n{\n  \"ownerId\": \"kn73vp5rarc3b14rc7wjcw8f8580t5d1\",\n  \"slug\": \"playwright\",\n  \"version\": \"1.0.2\",\n  \"publishedAt\": 1772107672145\n}\n\nFile v1.0.2:ci-cd.md\n\n# CI/CD Integration\n\n## GitHub Actions\n\n```yaml\nname: Playwright Tests\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      \n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n      \n      - name: Install dependencies\n        run: npm ci\n      \n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps\n      \n      - name: Run tests\n        run: npx playwright test\n      \n      - uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 7\n```\n\n## GitLab CI\n\n```yaml\nplaywright:\n  image: mcr.microsoft.com/playwright:v1.40.0-jammy\n  stage: test\n  script:\n    - npm ci\n    - npx playwright test\n  artifacts:\n    when: on_failure\n    paths:\n      - playwright-report/\n    expire_in: 7 days\n```\n\n## Docker Setup\n\n```dockerfile\nFROM mcr.microsoft.com/playwright:v1.40.0-jammy\n\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\n\nCMD [\"npx\", \"playwright\", \"test\"]\n```\n\n## Test Sharding\n\n```yaml\n# GitHub Actions matrix\njobs:\n  test:\n    strategy:\n      matrix:\n        shard: [1, 2, 3, 4]\n    steps:\n      - name: Run tests\n        run: npx playwright test --shard=${{ matrix.shard }}/4\n```\n\n## playwright.config.ts for CI\n\n```typescript\nimport { defineConfig, devices } from '@playwright/test';\n\nexport default defineConfig({\n  testDir: './tests',\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 4 : undefined,\n  reporter: process.env.CI \n    ? [['html'], ['github']] \n    : [['html']],\n  \n  use: {\n    trace: 'on-first-retry',\n    screenshot: 'only-on-failure',\n    video: 'on-first-retry',\n  },\n\n  projects: [\n    {\n      name: 'chromium',\n      use: { ...devices['Desktop Chrome'] },\n    },\n    {\n      name: 'firefox',\n      use: { ...devices['Desktop Firefox'] },\n    },\n    {\n      name: 'webkit',\n      use: { ...devices['Desktop Safari'] },\n    },\n  ],\n});\n```\n\n## Caching Browsers\n\n```yaml\n# GitHub Actions\n- name: Cache Playwright browsers\n  uses: actions/cache@v4\n  with:\n    path: ~/.cache/ms-playwright\n    key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}\n```\n\n## Environment Variables\n\n```yaml\nenv:\n  BASE_URL: https://staging.example.com\n  CI: true\n```\n\n```typescript\n// playwright.config.ts\nuse: {\n  baseURL: process.env.BASE_URL || 'http://localhost:3000',\n}\n```\n\n## Flaky Test Management\n\n```typescript\n// Mark known flaky test\ntest('sometimes fails', {\n  annotation: { type: 'flaky', description: 'Network timing issue' },\n}, async ({ page }) => {\n  // test code\n});\n\n// Retry configuration\nexport default defineConfig({\n  retries: 2,\n  expect: {\n    timeout: 10000,  // Increase assertion timeout\n  },\n});\n```\n\n## Report Hosting\n\n```yaml\n# Deploy to GitHub Pages\n- name: Deploy report\n  if: always()\n  uses: peaceiris/actions-gh-pages@v3\n  with:\n    github_token: ${{ secrets.GITHUB_TOKEN }}\n    publish_dir: ./playwright-report\n```\n\n## Common CI Issues\n\n| Issue | Fix |\n|-------|-----|\n| Browsers not found | Use official Playwright Docker image |\n| Display errors | Headless mode or `xvfb-run` |\n| Out of memory | Reduce workers, close contexts |\n| Timeouts | Increase `actionTimeout`, add retries |\n| Inconsistent screenshots | Set fixed viewport, disable animations |\n\nFile v1.0.2:debugging.md\n\n# Debugging Guide\n\n## Playwright Inspector\n\n```bash\n# Run in debug mode\nnpx playwright test --debug\n\n# Debug specific test\nnpx playwright test my-test.spec.ts --debug\n\n# Headed mode (see browser)\nnpx playwright test --headed\n```\n\n```typescript\n// Pause in test\nawait page.pause();\n```\n\n## Trace Viewer\n\n```bash\n# Record trace\nnpx playwright test --trace on\n\n# View trace file\nnpx playwright show-trace trace.zip\n```\n\n```typescript\n// Config for traces\nuse: {\n  trace: 'on-first-retry',  // Only on failures\n  trace: 'retain-on-failure',  // Keep failed traces\n}\n```\n\n## Common Errors\n\n### Element Not Found\n\n```\nError: Timeout 30000ms exceeded waiting for selector\n```\n\n**Causes:**\n- Element doesn't exist in DOM\n- Element is inside iframe\n- Element is in shadow DOM\n- Page hasn't loaded\n\n**Fixes:**\n```typescript\n// Wait for element\nawait page.waitForSelector('.element');\n\n// Check frame context\nconst frame = page.frameLocator('iframe');\nawait frame.locator('.element').click();\n\n// Increase timeout\nawait page.click('.element', { timeout: 60000 });\n```\n\n### Flaky Click\n\n```\nError: Element is not visible\nError: Element is outside viewport\n```\n\n**Fixes:**\n```typescript\n// Ensure visible\nawait page.locator('.btn').waitFor({ state: 'visible' });\nawait page.locator('.btn').click();\n\n// Scroll into view\nawait page.locator('.btn').scrollIntoViewIfNeeded();\n\n// Force click (bypass checks)\nawait page.locator('.btn').click({ force: true });\n```\n\n### Timeout in CI\n\n**Causes:**\n- Slower CI environment\n- Network latency\n- Resource constraints\n\n**Fixes:**\n```typescript\n// Increase global timeout\nexport default defineConfig({\n  timeout: 60000,\n  expect: { timeout: 10000 },\n});\n\n// Use polling assertions\nawait expect.poll(async () => {\n  return await page.locator('.items').count();\n}, { timeout: 30000 }).toBeGreaterThan(5);\n```\n\n### Stale Element\n\n```\nError: Element is no longer attached to DOM\n```\n\n**Fix:**\n```typescript\n// Don't store element references\nconst button = page.locator('.submit');  // This is fine (locator)\n\n// Re-query when needed\nawait button.click();  // Playwright re-queries automatically\n```\n\n### Network Issues\n\n```typescript\n// Log all requests\npage.on('request', request => {\n  console.log('>>', request.method(), request.url());\n});\n\npage.on('response', response => {\n  console.log('<<', response.status(), response.url());\n});\n\n// Wait for specific request\nconst responsePromise = page.waitForResponse('**/api/data');\nawait page.click('.load-data');\nconst response = await responsePromise;\n```\n\n## Screenshot Debugging\n\n```typescript\n// Take screenshot on failure\ntest.afterEach(async ({ page }, testInfo) => {\n  if (testInfo.status !== 'passed') {\n    await page.screenshot({\n      path: `screenshots/${testInfo.title}.png`,\n      fullPage: true,\n    });\n  }\n});\n```\n\n## Console Logs\n\n```typescript\n// Capture console messages\npage.on('console', msg => {\n  console.log('PAGE LOG:', msg.text());\n});\n\npage.on('pageerror', error => {\n  console.log('PAGE ERROR:', error.message);\n});\n```\n\n## Slow Motion\n\n```typescript\n// playwright.config.ts\nuse: {\n  launchOptions: {\n    slowMo: 500,  // 500ms delay between actions\n  },\n}\n```\n\n## Compare Local vs CI\n\n| Check | Command |\n|-------|---------|\n| Viewport | `await page.viewportSize()` |\n| User agent | `await page.evaluate(() => navigator.userAgent)` |\n| Timezone | `await page.evaluate(() => Intl.DateTimeFormat().resolvedOptions().timeZone)` |\n| Network | `page.on('request', ...)` to log all requests |\n\n## Debugging Checklist\n\n1. [ ] Run with `--debug` or `--headed`\n2. [ ] Add `await page.pause()` before failure point\n3. [ ] Check for iframes/shadow DOM\n4. [ ] Verify element exists with `page.locator().count()`\n5. [ ] Review trace file in Trace Viewer\n6. [ ] Compare screenshots between local and CI\n7. [ ] Check console for JS errors\n8. [ ] Verify network requests completed\n\nFile v1.0.2:scraping.md\n\n# Scraping Patterns\n\n## Basic Extraction\n\n```typescript\nconst browser = await chromium.launch();\nconst page = await browser.newPage();\nawait page.goto('https://example.com/products');\n\nconst products = await page.$$eval('.product-card', cards =>\n  cards.map(card => ({\n    name: card.querySelector('.name')?.textContent?.trim(),\n    price: card.querySelector('.price')?.textContent?.trim(),\n    url: card.querySelector('a')?.href,\n  }))\n);\n\nawait browser.close();\n```\n\n## Wait Strategies for SPAs\n\n```typescript\n// Wait for specific element\nawait page.waitForSelector('[data-loaded=\"true\"]');\n\n// Wait for network idle (careful with SPAs)\nawait page.goto(url, { waitUntil: 'networkidle' });\n\n// Wait for loading indicator to disappear\nawait page.waitForSelector('.loading-spinner', { state: 'hidden' });\n\n// Custom condition with polling\nawait expect.poll(async () => {\n  return await page.locator('.product').count();\n}).toBeGreaterThan(0);\n```\n\n## Infinite Scroll\n\n```typescript\nasync function scrollToBottom(page: Page) {\n  let previousHeight = 0;\n  \n  while (true) {\n    const currentHeight = await page.evaluate(() => document.body.scrollHeight);\n    if (currentHeight === previousHeight) break;\n    \n    previousHeight = currentHeight;\n    await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));\n    await page.waitForLoadState('domcontentloaded');\n  }\n}\n```\n\n## Pagination\n\n```typescript\n// Click-based pagination\nasync function scrapeAllPages(page: Page) {\n  const allData = [];\n  \n  while (true) {\n    const pageData = await extractData(page);\n    allData.push(...pageData);\n    \n    const nextButton = page.getByRole('button', { name: 'Next' });\n    if (await nextButton.isDisabled()) break;\n    \n    await nextButton.click();\n    await page.waitForLoadState('networkidle');\n  }\n  \n  return allData;\n}\n```\n\n## Session Persistence\n\n```typescript\n// Save cookies and localStorage for later reuse\nawait context.storageState({ path: 'session.json' });\n\n// Restore session in new context\nconst context = await browser.newContext({\n  storageState: 'session.json',\n});\n```\n\n## Error Handling with Retries\n\n```typescript\nasync function scrapeWithRetry(url: string, retries = 3) {\n  for (let i = 0; i < retries; i++) {\n    try {\n      const page = await context.newPage();\n      await page.goto(url, { timeout: 30000 });\n      return await extractData(page);\n    } catch (error) {\n      if (i === retries - 1) throw error;\n      await new Promise(r => setTimeout(r, 2000 * (i + 1)));\n    } finally {\n      await page.close();\n    }\n  }\n}\n```\n\n## Rate Limiting (Be Respectful)\n\n```typescript\nclass RateLimiter {\n  private lastRequest = 0;\n  \n  constructor(private minDelay: number) {}\n  \n  async wait() {\n    const elapsed = Date.now() - this.lastRequest;\n    if (elapsed < this.minDelay) {\n      await new Promise(r => setTimeout(r, this.minDelay - elapsed));\n    }\n    this.lastRequest = Date.now();\n  }\n}\n\nconst limiter = new RateLimiter(2000);  // 2s between requests\n\nfor (const url of urls) {\n  await limiter.wait();\n  await scrape(url);\n}\n```\n\n## Structured Data Extraction\n\n```typescript\n// Extract with JSON-LD\nconst jsonLd = await page.$eval(\n  'script[type=\"application/ld+json\"]',\n  el => JSON.parse(el.textContent || '{}')\n);\n\n// Extract table to array\nconst tableData = await page.$$eval('table tbody tr', rows =>\n  rows.map(row => \n    Array.from(row.querySelectorAll('td')).map(td => td.textContent?.trim())\n  )\n);\n```\n\nFile v1.0.2:selectors.md\n\n# Selector Strategies\n\n## Hierarchy (Most to Least Resilient)\n\n### 1. Role-Based (Best)\n```typescript\npage.getByRole('button', { name: 'Submit' })\npage.getByRole('link', { name: /sign up/i })\npage.getByRole('heading', { level: 1 })\npage.getByRole('textbox', { name: 'Email' })\n```\n\n### 2. Test IDs (Explicit)\n```typescript\npage.getByTestId('checkout-button')\npage.getByTestId('product-card').first()\n```\nConfigure in `playwright.config.ts`:\n```typescript\nuse: { testIdAttribute: 'data-testid' }\n```\n\n### 3. Label/Placeholder (Forms)\n```typescript\npage.getByLabel('Email address')\npage.getByPlaceholder('Enter your email')\n```\n\n### 4. Text Content (Visible)\n```typescript\npage.getByText('Add to Cart', { exact: true })\npage.getByText(/welcome/i)  // regex for flexibility\n```\n\n### 5. CSS (Last Resort)\n```typescript\n// Avoid these patterns:\npage.locator('.css-1a2b3c')  // generated class\npage.locator('div > span:nth-child(2)')  // positional\npage.locator('#root > div > div > button')  // deep nesting\n\n// Acceptable:\npage.locator('[data-product-id=\"123\"]')  // semantic attribute\npage.locator('form.login-form')  // stable class\n```\n\n## Chaining and Filtering\n\n```typescript\n// Filter within results\npage.getByRole('listitem').filter({ hasText: 'Product A' })\n\n// Chain locators\npage.getByTestId('cart').getByRole('button', { name: 'Remove' })\n\n// Get nth item\npage.getByRole('listitem').nth(2)\npage.getByRole('listitem').first()\npage.getByRole('listitem').last()\n```\n\n## Frame Handling\n\n```typescript\n// Named frame\nconst frame = page.frameLocator('iframe[name=\"checkout\"]')\nframe.getByRole('button', { name: 'Pay' }).click()\n\n// Frame by URL\npage.frameLocator('iframe[src*=\"stripe\"]')\n```\n\n## Shadow DOM\n\n```typescript\n// Playwright pierces shadow DOM by default\npage.locator('my-component').getByRole('button')\n```\n\n## Common Mistakes\n\n| Mistake | Better |\n|---------|--------|\n| `page.locator('button').click()` | `page.getByRole('button', { name: 'Submit' }).click()` |\n| Storing locator result | Re-query each time |\n| `nth-child(3)` | Filter by text or test ID |\n| `//div[@class=\"xyz\"]/span[2]` | Role-based or test ID |\n\nFile v1.0.2:testing.md\n\n# Testing Patterns\n\n## Test Structure\n\n```typescript\nimport { test, expect } from '@playwright/test';\n\ntest.describe('Checkout Flow', () => {\n  test.beforeEach(async ({ page }) => {\n    await page.goto('/products');\n  });\n\n  test('completes purchase with valid card', async ({ page }) => {\n    await page.getByTestId('product-card').first().click();\n    await page.getByRole('button', { name: 'Add to Cart' }).click();\n    await page.getByRole('link', { name: 'Checkout' }).click();\n    \n    await expect(page.getByRole('heading', { name: 'Order Summary' })).toBeVisible();\n  });\n});\n```\n\n## Page Object Model\n\n```typescript\n// pages/checkout.page.ts\nexport class CheckoutPage {\n  constructor(private page: Page) {}\n\n  readonly cartItems = this.page.getByTestId('cart-item');\n  readonly checkoutButton = this.page.getByRole('button', { name: 'Checkout' });\n  readonly totalPrice = this.page.getByTestId('total-price');\n\n  async removeItem(name: string) {\n    await this.cartItems\n      .filter({ hasText: name })\n      .getByRole('button', { name: 'Remove' })\n      .click();\n  }\n\n  async expectTotal(amount: string) {\n    await expect(this.totalPrice).toHaveText(amount);\n  }\n}\n\n// tests/checkout.spec.ts\ntest('removes item from cart', async ({ page }) => {\n  const checkout = new CheckoutPage(page);\n  await checkout.removeItem('Product A');\n  await checkout.expectTotal('$0.00');\n});\n```\n\n## Fixtures\n\n```typescript\n// fixtures.ts\nimport { test as base } from '@playwright/test';\nimport { CheckoutPage } from './pages/checkout.page';\n\ntype Fixtures = {\n  checkoutPage: CheckoutPage;\n};\n\nexport const test = base.extend<Fixtures>({\n  checkoutPage: async ({ page }, use) => {\n    await page.goto('/checkout');\n    await use(new CheckoutPage(page));\n  },\n});\n```\n\n## API Mocking\n\n```typescript\ntest('shows error on API failure', async ({ page }) => {\n  await page.route('**/api/checkout', route => {\n    route.fulfill({\n      status: 500,\n      body: JSON.stringify({ error: 'Payment failed' }),\n    });\n  });\n\n  await page.goto('/checkout');\n  await page.getByRole('button', { name: 'Pay' }).click();\n  await expect(page.getByText('Payment failed')).toBeVisible();\n});\n```\n\n## Visual Regression\n\n```typescript\ntest('matches snapshot', async ({ page }) => {\n  await page.goto('/dashboard');\n  await expect(page).toHaveScreenshot('dashboard.png', {\n    maxDiffPixels: 100,\n  });\n});\n\n// Component snapshot\nawait expect(page.getByTestId('header')).toHaveScreenshot();\n```\n\n## Parallelization\n\n```typescript\n// playwright.config.ts\nexport default defineConfig({\n  workers: process.env.CI ? 4 : undefined,\n  fullyParallel: true,\n});\n\n// Per-file control\ntest.describe.configure({ mode: 'parallel' });\ntest.describe.configure({ mode: 'serial' });  // dependent tests\n```\n\n## Authentication State\n\n```typescript\n// Save auth state\nawait page.context().storageState({ path: 'auth.json' });\n\n// Reuse across tests\ntest.use({ storageState: 'auth.json' });\n```\n\n## Assertions\n\n```typescript\n// Visibility\nawait expect(locator).toBeVisible();\nawait expect(locator).toBeHidden();\nawait expect(locator).toBeAttached();\n\n// Content\nawait expect(locator).toHaveText('Expected');\nawait expect(locator).toContainText('partial');\nawait expect(locator).toHaveValue('input value');\n\n// State\nawait expect(locator).toBeEnabled();\nawait expect(locator).toBeChecked();\nawait expect(locator).toHaveAttribute('href', '/path');\n\n// Polling (for async state)\nawait expect.poll(async () => {\n  return await page.evaluate(() => window.dataLoaded);\n}).toBe(true);\n```\n\nArchive v1.0.1: 7 files, 10162 bytes\n\nFiles: ci-cd.md (3438b), debugging.md (3865b), scraping.md (3454b), selectors.md (2130b), SKILL.md (5346b), testing.md (3538b), _meta.json (129b)\n\nFile v1.0.1:SKILL.md\n\n---\nname: Playwright (Automation + MCP + Scraper)\nslug: playwright\nversion: 1.0.1\nhomepage: https://playwright.dev\ndescription: Browser automation and web scraping with Playwright. Forms, screenshots, data extraction. Works standalone or via MCP. Testing included.\nchangelog: Rewritten with MCP integration and scraping focus. Now covers standalone use and MCP server setup.\n---\n\n## When to Use\n\nUse this skill when you need to:\n- **Scrape a website** (static or JavaScript-rendered)\n- **Automate form filling** (login, checkout, data entry)\n- **Test a web application** (E2E tests, visual regression)\n- **Take screenshots or PDFs** of web pages\n- **Extract data** from tables, lists, or dynamic content\n\n## Decision Matrix\n\n| Scenario | Method | Speed |\n|----------|--------|-------|\n| Static HTML | `web_fetch` tool | ⚡ Fastest |\n| JavaScript-rendered | Playwright direct | 🚀 Fast |\n| AI agent automation | MCP server | 🤖 Integrated |\n| E2E testing | @playwright/test | ✅ Full framework |\n\n## Quick Reference\n\n| Task | File |\n|------|------|\n| E2E testing patterns | `testing.md` |\n| CI/CD integration | `ci-cd.md` |\n| Debugging failures | `debugging.md` |\n| Web scraping patterns | `scraping.md` |\n| Selector strategies | `selectors.md` |\n\n## Core Rules\n\n1. **Never use `waitForTimeout()`** - always wait for specific conditions (element, URL, network)\n2. **Always close the browser** - call `browser.close()` to prevent memory leaks\n3. **Prefer role selectors** - `getByRole()` survives UI changes better than CSS\n4. **Handle dynamic content** - use `waitFor()` before interacting with elements\n5. **Persist auth state** - use `storageState` to save and reuse login sessions\n\n## Quick Start - Common Tasks\n\n### Scrape a Page\n```javascript\nconst { chromium } = require('playwright');\nconst browser = await chromium.launch();\nconst page = await browser.newPage();\nawait page.goto('https://example.com');\nconst text = await page.locator('body').textContent();\nawait browser.close();\n```\n\n### Fill a Form and Submit\n```javascript\nawait page.goto('https://example.com/login');\nawait page.getByLabel('Email').fill('user@example.com');\nawait page.getByLabel('Password').fill('secret');\nawait page.getByRole('button', { name: 'Sign in' }).click();\nawait page.waitForURL('**/dashboard');\n```\n\n### Take a Screenshot\n```javascript\nawait page.goto('https://example.com');\nawait page.screenshot({ path: 'screenshot.png', fullPage: true });\n```\n\n### Extract Table Data\n```javascript\nconst rows = await page.locator('table tr').all();\nconst data = [];\nfor (const row of rows) {\n  const cells = await row.locator('td').allTextContents();\n  data.push(cells);\n}\n```\n\n## Selector Priority\n\n| Priority | Method | Example |\n|----------|--------|---------|\n| 1 | `getByRole()` | `getByRole('button', { name: 'Submit' })` |\n| 2 | `getByLabel()` | `getByLabel('Email')` |\n| 3 | `getByPlaceholder()` | `getByPlaceholder('Search...')` |\n| 4 | `getByTestId()` | `getByTestId('submit-btn')` |\n| 5 | `locator()` | `locator('.class')` - last resort |\n\n## Common Traps\n\n| Trap | Fix |\n|------|-----|\n| Element not found | Add `await locator.waitFor()` before interacting |\n| Flaky clicks | Use `click({ force: true })` or wait for `state: 'visible'` |\n| Timeout in CI | Increase timeout, check viewport size matches local |\n| Auth lost between tests | Use `storageState` to persist cookies |\n| SPA never reaches networkidle | Wait for specific DOM element instead |\n| 403 Forbidden | Check if site blocks headless; try `headless: false` |\n| Blank page after load | Increase wait time or use `waitUntil: 'networkidle'` |\n\n## Handling Sessions\n\n```javascript\n// Save session after login\nawait page.context().storageState({ path: 'auth.json' });\n\n// Reuse session in new context\nconst context = await browser.newContext({ storageState: 'auth.json' });\n```\n\n## MCP Integration\n\nFor AI agents using Model Context Protocol:\n\n```bash\nnpm install -g @playwright/mcp\nnpx @playwright/mcp --headless\n```\n\n### MCP Tools Reference\n\n| Tool | Description |\n|------|-------------|\n| `browser_navigate` | Navigate to URL |\n| `browser_click` | Click element by selector |\n| `browser_type` | Type text into input |\n| `browser_select_option` | Select dropdown option |\n| `browser_get_text` | Get text content |\n| `browser_evaluate` | Execute JavaScript |\n| `browser_snapshot` | Get accessible page snapshot |\n| `browser_close` | Close browser context |\n| `browser_choose_file` | Upload file |\n| `browser_press` | Press keyboard key |\n\n### MCP Server Options\n\n```bash\n--headless              # Run without UI\n--browser chromium      # chromium|firefox|webkit\n--viewport-size 1920x1080\n--timeout-action 10000  # Action timeout (ms)\n--timeout-navigation 30000\n--allowed-hosts example.com,api.example.com\n--save-trace            # Save trace for debugging\n--save-video 1280x720   # Record video\n```\n\n## Installation\n\n```bash\nnpm init playwright@latest\n# Or add to existing project\nnpm install -D @playwright/test\nnpx playwright install chromium\n```\n\n## Related Skills\nInstall with `clawhub install <slug>` if user confirms:\n- `puppeteer` - Alternative browser automation (Chrome-focused)\n- `scrape` - General web scraping patterns and strategies\n- `web` - Web development fundamentals and HTTP handling\n\n## Feedback\n\n- If useful: `clawhub star playwright`\n- Stay updated: `clawhub sync`\n\nFile v1.0.1:_meta.json\n\n{\n  \"ownerId\": \"kn73vp5rarc3b14rc7wjcw8f8580t5d1\",\n  \"slug\": \"playwright\",\n  \"version\": \"1.0.1\",\n  \"publishedAt\": 1772107609007\n}\n\nFile v1.0.1:ci-cd.md\n\n# CI/CD Integration\n\n## GitHub Actions\n\n```yaml\nname: Playwright Tests\non: [push, pull_request]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      \n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: 'npm'\n      \n      - name: Install dependencies\n        run: npm ci\n      \n      - name: Install Playwright browsers\n        run: npx playwright install --with-deps\n      \n      - name: Run tests\n        run: npx playwright test\n      \n      - uses: actions/upload-artifact@v4\n        if: failure()\n        with:\n          name: playwright-report\n          path: playwright-report/\n          retention-days: 7\n```\n\n## GitLab CI\n\n```yaml\nplaywright:\n  image: mcr.microsoft.com/playwright:v1.40.0-jammy\n  stage: test\n  script:\n    - npm ci\n    - npx playwright test\n  artifacts:\n    when: on_failure\n    paths:\n      - playwright-report/\n    expire_in: 7 days\n```\n\n## Docker Setup\n\n```dockerfile\nFROM mcr.microsoft.com/playwright:v1.40.0-jammy\n\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\n\nCMD [\"npx\", \"playwright\", \"test\"]\n```\n\n## Test Sharding\n\n```yaml\n# GitHub Actions matrix\njobs:\n  test:\n    strategy:\n      matrix:\n        shard: [1, 2, 3, 4]\n    steps:\n      - name: Run tests\n        run: npx playwright test --shard=${{ matrix.shard }}/4\n```\n\n## playwright.config.ts for CI\n\n```typescript\nimport { defineConfig, devices } from '@playwright/test';\n\nexport default defineConfig({\n  testDir: './tests',\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 4 : undefined,\n  reporter: process.env.CI \n    ? [['html'], ['github']] \n    : [['html']],\n  \n  use: {\n    trace: 'on-first-retry',\n    screenshot: 'only-on-failure',\n    video: 'on-first-retry',\n  },\n\n  projects: [\n    {\n      name: 'chromium',\n      use: { ...devices['Desktop Chrome'] },\n    },\n    {\n      name: 'firefox',\n      use: { ...devices['Desktop Firefox'] },\n    },\n    {\n      name: 'webkit',\n      use: { ...devices['Desktop Safari'] },\n    },\n  ],\n});\n```\n\n## Caching Browsers\n\n```yaml\n# GitHub Actions\n- name: Cache Playwright browsers\n  uses: actions/cache@v4\n  with:\n    path: ~/.cache/ms-playwright\n    key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}\n```\n\n## Environment Variables\n\n```yaml\nenv:\n  BASE_URL: https://staging.example.com\n  CI: true\n```\n\n```typescript\n// playwright.config.ts\nuse: {\n  baseURL: process.env.BASE_URL || 'http://localhost:3000',\n}\n```\n\n## Flaky Test Management\n\n```typescript\n// Mark known flaky test\ntest('sometimes fails', {\n  annotation: { type: 'flaky', description: 'Network timing issue' },\n}, async ({ page }) => {\n  // test code\n});\n\n// Retry configuration\nexport default defineConfig({\n  retries: 2,\n  expect: {\n    timeout: 10000,  // Increase assertion timeout\n  },\n});\n```\n\n## Report Hosting\n\n```yaml\n# Deploy to GitHub Pages\n- name: Deploy report\n  if: always()\n  uses: peaceiris/actions-gh-pages@v3\n  with:\n    github_token: ${{ secrets.GITHUB_TOKEN }}\n    publish_dir: ./playwright-report\n```\n\n## Common CI Issues\n\n| Issue | Fix |\n|-------|-----|\n| Browsers not found | Use official Playwright Docker image |\n| Display errors | Headless mode or `xvfb-run` |\n| Out of memory | Reduce workers, close contexts |\n| Timeouts | Increase `actionTimeout`, add retries |\n| Inconsistent screenshots | Set fixed viewport, disable animations |\n\nFile v1.0.1:debugging.md\n\n# Debugging Guide\n\n## Playwright Inspector\n\n```bash\n# Run in debug mode\nnpx playwright test --debug\n\n# Debug specific test\nnpx playwright test my-test.spec.ts --debug\n\n# Headed mode (see browser)\nnpx playwright test --headed\n```\n\n```typescript\n// Pause in test\nawait page.pause();\n```\n\n## Trace Viewer\n\n```bash\n# Record trace\nnpx playwright test --trace on\n\n# View trace file\nnpx playwright show-trace trace.zip\n```\n\n```typescript\n// Config for traces\nuse: {\n  trace: 'on-first-retry',  // Only on failures\n  trace: 'retain-on-failure',  // Keep failed traces\n}\n```\n\n## Common Errors\n\n### Element Not Found\n\n```\nError: Timeout 30000ms exceeded waiting for selector\n```\n\n**Causes:**\n- Element doesn't exist in DOM\n- Element is inside iframe\n- Element is in shadow DOM\n- Page hasn't loaded\n\n**Fixes:**\n```typescript\n// Wait for element\nawait page.waitForSelector('.element');\n\n// Check frame context\nconst frame = page.frameLocator('iframe');\nawait frame.locator('.element').click();\n\n// Increase timeout\nawait page.click('.element', { timeout: 60000 });\n```\n\n### Flaky Click\n\n```\nError: Element is not visible\nError: Element is outside viewport\n```\n\n**Fixes:**\n```typescript\n// Ensure visible\nawait page.locator('.btn').waitFor({ state: 'visible' });\nawait page.locator('.btn').click();\n\n// Scroll into view\nawait page.locator('.btn').scrollIntoViewIfNeeded();\n\n// Force click (bypass checks)\nawait page.locator('.btn').click({ force: true });\n```\n\n### Timeout in CI\n\n**Causes:**\n- Slower CI environment\n- Network latency\n- Resource constraints\n\n**Fixes:**\n```typescript\n// Increase global timeout\nexport default defineConfig({\n  timeout: 60000,\n  expect: { timeout: 10000 },\n});\n\n// Use polling assertions\nawait expect.poll(async () => {\n  return await page.locator('.items').count();\n}, { timeout: 30000 }).toBeGreaterThan(5);\n```\n\n\n\nArchive v1.0.0: 7 files, 8864 bytes\n\nFiles: ci-cd.md (3438b), debugging.md (3865b), scraping.md (3819b), selectors.md (2130b), SKILL.md (1781b), testing.md (3538b), _meta.json (129b)","readmeExcerpt":"Skill: Playwright (Automation + MCP + Scraper) Owner: ivangdavila Summary: Automates, tests, and debugs browsers with Playwright: locators, auto-waiting, traces, CI runs, and MCP browser control. Use when a test is flaky, times out, or fails only in CI or headless; when a locator matches multiple elements or the wrong one (strict mode violation); when clicks need force, waits become sleeps, or networkidle never settl","codeSnippets":[],"executableExamples":[{"language":"typescript","snippet":"import AxeBuilder from '@axe-core/playwright';\n\ntest('dashboard has no critical a11y violations', async ({ page }) => {\n  await page.goto('/dashboard');\n  const results = await new AxeBuilder({ page })\n    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])\n    .analyze();\n  expect(results.violations).toEqual([]);\n});"},{"language":"typescript","snippet":"new AxeBuilder({ page })\n  .include('#main')                       // scan one region\n  .exclude('.third-party-widget')         // an embed you cannot fix\n  .disableRules(['color-contrast'])       // temporarily, with a linked issue"},{"language":"typescript","snippet":"await expect(page.getByRole('navigation')).toMatchAriaSnapshot(`\n  - list:\n    - listitem:\n      - link \"Home\"\n    - listitem:\n      - link /Sign (in|out)/\n`);\n\nawait expect(page.getByRole('dialog')).toMatchAriaSnapshot({ name: 'checkout-dialog.aria.yml' });"},{"language":"typescript","snippet":"await page.keyboard.press('Tab');\nawait expect(page.getByRole('link', { name: 'Skip to content' })).toBeFocused();\n\nfor (const name of ['Email', 'Password', 'Sign in']) {\n  await page.keyboard.press('Tab');\n  await expect(page.getByRole(/button|textbox/, { name })).toBeFocused();\n}\n\nawait page.getByRole('button', { name: 'Open settings' }).click();\nawait page.keyboard.press('Escape');\nawait expect(page.getByRole('button', { name: 'Open settings' })).toBeFocused();   // focus returned"},{"language":"typescript","snippet":"// global.setup.ts\nimport { test as setup, expect } from '@playwright/test';\n\nsetup('authenticate', async ({ page }) => {\n  await page.goto('/login');\n  await page.getByLabel('Email').fill(process.env.E2E_USER!);\n  await page.getByLabel('Password').fill(process.env.E2E_PASS!);\n  await page.getByRole('button', { name: 'Sign in' }).click();\n  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();\n  await page.context().storageState({ path: '.auth/user.json' });\n});"},{"language":"typescript","snippet":"// playwright.config.ts\nprojects: [\n  { name: 'setup', testMatch: /global\\.setup\\.ts/ },\n  { name: 'chromium', dependencies: ['setup'], use: { storageState: '.auth/user.json' } },\n]"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: playwright\nslug: playwright\nversion: 1.0.4\ndescription: >-\n  Automates, tests, and debugs browsers with Playwright: locators, auto-waiting, traces, CI runs, and MCP browser control.\n  Use when a test is flaky, times out, or fails only in CI or headless; when a locator matches multiple elements or the\n  wrong one (strict mode violation); when clicks need force, waits become sleeps, or networkidle never settles; for\n  storageState and login setup, request mocking and HAR replay, uploads and downloads, iframes and shadow DOM, popups and\n  dialogs, screenshot diffs that change per machine, trace and report artifacts, sharding a slow suite, device and\n  permission emulation, accessibility checks, driving a real browser through Playwright MCP, extracting data from\n  JS-rendered pages, or porting a Cypress, Puppeteer, or Selenium suite to Playwright. Not for maintaining an existing\n  Cypress or Puppeteer suite (cypress, puppeteer) or for work a plain HTTP request answers (http).\nhomepage: https://clawic.com/skills/playwright\nchangelog: \"Full coverage pass: deeper guides, situation-named files, and per-user configuration\"\nmetadata:\n  clawdbot:\n    emoji: P\n    requires:\n      bins:\n      - node\n      - npx\n    os:\n    - linux\n    - darwin\n    - win32\n    install:\n    - id: npm-playwright\n      kind: npm\n      package: playwright\n      bins:\n      - playwright\n      label: Install Playwright\n    - id: npm-playwright-mcp\n      kind: npm\n      package: '@playwright/mcp'\n      bins:\n      - playwright-mcp\n      label: Install Playwright MCP (optional)\n    displayName: Playwright (Automation + MCP + Scraper)\n    configPaths:\n    - ~/Clawic/data/playwright/\n---\n\nUser preferences and memory live in `~/Clawic/data/playwright/` (see `setup.md` on first use). If you have data at an old location (`~/playwright/` or `~/clawic/playwright/`), move it to `~/Clawic/data/playwright/`. Everything a run produces — specs, traces, reports, snapshots, `storageState` files — stays inside the repository or its temp dir, never in the preferences folder: auth state is a credential and belongs in a gitignored path.\n\n## When To Use\n\n- Writing or repairing Playwright specs, fixtures, `playwright.config.ts`, or one-off automation scripts\n- A run fails: timeout, strict mode violation, flaky click, passes locally and fails in CI or headless\n- Login, session reuse, uploads, downloads, iframes, popups, dialogs, or device emulation are in the way\n- Visual snapshots, accessibility checks, trace capture, sharding, or cutting suite wall time\n- Driving a live browser through Playwright MCP, or extracting data a plain fetch cannot render\n- Porting a Cypress, Puppeteer, or Selenium suite to Playwright, including deciding which tests not to port\n- Not for maintaining an existing Cypress or Puppeteer suite (→ `cypress`, `puppeteer`) or for anything an HTTP request already answers (→ `http`)\n\n## Quick Reference\n\n| Situation | Play |\n|---|---|\n| `strict mode violation: resolved to N eleme"},{"path":"_meta.json","content":"{\n  \"owner\": \"clawic\",\n  \"slug\": \"playwright\",\n  \"displayName\": \"Playwright (Automation + MCP + Scraper)\",\n  \"latest\": {\n    \"version\": \"1.0.4\",\n    \"publishedAt\": 1785066064433\n  },\n  \"history\": [\n    {\n      \"version\": \"1.0.3\",\n      \"publishedAt\": 1773251092810\n    },\n    {\n      \"version\": \"1.0.2\",\n      \"publishedAt\": 1772107672145\n    },\n    {\n      \"version\": \"1.0.0\",\n      \"publishedAt\": 1770982184555\n    }\n  ]\n}"},{"path":"accessibility.md","content":"# Accessibility — Automated Scans and Keyboard Paths\n\nAutomated scanning catches a minority of real barriers — Deque's own figure for axe-core is around 57% of issues found automatically. Treat the scan as the floor and keyboard plus screen-reader semantics as the actual test.\n\n## Scanning With axe\n\n```typescript\nimport AxeBuilder from '@axe-core/playwright';\n\ntest('dashboard has no critical a11y violations', async ({ page }) => {\n  await page.goto('/dashboard');\n  const results = await new AxeBuilder({ page })\n    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])\n    .analyze();\n  expect(results.violations).toEqual([]);\n});\n```\n\nScoping and triage:\n\n```typescript\nnew AxeBuilder({ page })\n  .include('#main')                       // scan one region\n  .exclude('.third-party-widget')         // an embed you cannot fix\n  .disableRules(['color-contrast'])       // temporarily, with a linked issue\n```\n\n- Scan **states**, not just routes: open the modal, expand the menu, trigger the error, then analyze. Most violations live in states a route-level scan never renders.\n- Adopting on a legacy app: snapshot the current violations as a baseline and fail only on new ones (`expect(results.violations.length).toBeLessThanOrEqual(baseline)`), then ratchet the number down. A first run demanding zero gets the whole check deleted.\n- Attach the violation list to the report: `testInfo.attach('axe', { body: JSON.stringify(results.violations, null, 2), contentType: 'application/json' })` — a failure that names the rule and the node is actionable; a bare count is not.\n\n## What The Scan Cannot See\n\n| Barrier | How to test |\n|---|---|\n| Focus order that jumps around the page | Tab through and assert the sequence (below) |\n| Focus lost after a modal closes | Assert focus returns to the trigger |\n| A keyboard trap inside a widget | Tab N times and assert you escaped |\n| A label that is present but meaningless (\"click here\") | Human review, or a lint rule on the copy |\n| Live-region announcements | Assert `role=\"status\"`/`aria-live` content changes |\n| Contrast on a background image | Human review |\n| Whether the flow is usable at all with a screen reader | Manual pass; no tool substitutes |\n\n## ARIA Snapshots — Structure As An Assertion\n\n`toMatchAriaSnapshot` (playwright >=1.49) freezes the accessibility tree of a region as YAML: roles, accessible names, and nesting.\n\n```typescript\nawait expect(page.getByRole('navigation')).toMatchAriaSnapshot(`\n  - list:\n    - listitem:\n      - link \"Home\"\n    - listitem:\n      - link /Sign (in|out)/\n`);\n\nawait expect(page.getByRole('dialog')).toMatchAriaSnapshot({ name: 'checkout-dialog.aria.yml' });\n```\n\n- It fails on the regressions a scan and a screenshot both miss: a heading demoted to a `div`, a button that lost its accessible name, a menu item that appeared, an order that changed. It passes through a pure restyle.\n- Scope it to the smallest meaningful region — nav, dialog, menu, table, a design-system component. A whole-page t"},{"path":"auth.md","content":"# Auth — Sessions, Roles, and Parallel-Safe Accounts\n\nLogging in through the UI in every test is the most expensive line in most suites: sign in once, reuse the state, and give every worker an account it owns.\n\nContents: [The Default Shape](#the-default-shape) · [What storageState Actually Captures](#what-storagestate-actually-captures) · [Faster: Skip The UI Entirely](#faster-skip-the-ui-entirely) · [Multiple Roles](#multiple-roles) · [Parallel-Safe Accounts](#parallel-safe-accounts) · [SSO, 2FA, And Magic Links](#sso-2fa-and-magic-links) · [Expiry And Rotation](#expiry-and-rotation) · [Handling Credentials](#handling-credentials)\n\n## The Default Shape\n\nA setup project signs in once and writes the storage state; every other project starts already authenticated.\n\n```typescript\n// global.setup.ts\nimport { test as setup, expect } from '@playwright/test';\n\nsetup('authenticate', async ({ page }) => {\n  await page.goto('/login');\n  await page.getByLabel('Email').fill(process.env.E2E_USER!);\n  await page.getByLabel('Password').fill(process.env.E2E_PASS!);\n  await page.getByRole('button', { name: 'Sign in' }).click();\n  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();\n  await page.context().storageState({ path: '.auth/user.json' });\n});\n```\n\n```typescript\n// playwright.config.ts\nprojects: [\n  { name: 'setup', testMatch: /global\\.setup\\.ts/ },\n  { name: 'chromium', dependencies: ['setup'], use: { storageState: '.auth/user.json' } },\n]\n```\n\n`.auth/` goes in `.gitignore`. A committed storage state is a committed live session.\n\n## What storageState Actually Captures\n\n| Captured | Not captured |\n|---|---|\n| Cookies (including httpOnly) | `sessionStorage` — per tab, never serialized by default |\n| `localStorage` per origin | IndexedDB, unless the Playwright version supports opting it in |\n| | Service-worker caches |\n| | Anything the server holds against a device fingerprint |\n\nIf the app keeps its token in `sessionStorage`, reuse fails silently: the test lands on a login page and the first assertion times out. Workarounds: copy the value in through an init script, or ask the app to accept the cookie path in test builds.\n\n```typescript\nawait context.addInitScript(([k, v]) => sessionStorage.setItem(k, v), ['token', token]);\n```\n\n## Faster: Skip The UI Entirely\n\nThe login form is a product feature that deserves **one** test. Everywhere else, mint the session through the API:\n\n```typescript\nconst res = await request.post('/api/login', { data: { email, password } });\nconst { token } = await res.json();\nawait context.addInitScript(t => localStorage.setItem('token', t), token);\n// or: await context.addCookies([{ name: 'session', value: token, domain: 'localhost', path: '/' }]);\n```\n\nSaves the whole login round trip per worker and removes the login page from the failure surface of 400 unrelated tests.\n\n## Multiple Roles\n\nOne storage-state file per role, one project per role:\n\n```typescript\nprojects: [\n  { name: 'setup', testMatch: /auth\\."},{"path":"browsers.md","content":"# Browsers — Engines, Channels, and Installs\n\nPlaywright ships its own browser builds, pinned to the package version. Most \"works on my machine\" browser problems are a version or channel mismatch, not a bug.\n\n## Engines\n\n| Engine | What it really is | Test it when |\n|---|---|---|\n| `chromium` | Playwright's Chromium build | Default; matches Chrome and Edge behavior closely enough for almost everything |\n| `firefox` | Playwright's Gecko build | You support Firefox; also catches spec-correct behavior Chromium tolerates |\n| `webkit` | WebKit engine, **not Safari** | Closest available proxy for Safari and iOS; shares the engine, not the browser |\n| `channel: 'chrome'` / `'msedge'` | Your installed branded browser | Widevine DRM, proprietary codecs, Chrome-specific APIs, or a bug reported only in real Chrome |\n\nWebKit on Linux differs from Safari on macOS in fonts, codecs, and media support. A WebKit-only failure is worth investigating, but \"passes in WebKit\" is not proof it works in Safari — for release-critical Safari behavior, run a real device or a hosted Safari.\n\nEngine differences that actually bite: date and number formatting via Intl, `input[type=date]` widgets, clipboard permissions (Chromium-only), `isMobile` (unsupported in Firefox), download handling, and font fallback affecting layout.\n\nExclude the affected case, not the engine: `test.skip(browserName === 'firefox', 'isMobile unsupported')` keeps the rest of the file running on Firefox and states why in the report (annotations: `testing.md`). Dropping the whole project to dodge one case is how cross-browser coverage quietly ends.\n\n## Headless\n\nChromium has two headless implementations: the old separate headless shell (fast, small) and the browser's own headless mode, which behaves like headed and supports extensions. Playwright >=1.49 exposes the browser-native one as `channel: 'chromium'`. When a bug reproduces headed but not headless, switching to the browser-native mode is the first thing to try — before rewriting the test.\n\nHeadless differences to expect regardless: no window manager (so window-size APIs report the configured viewport), a different default UA in older builds, no GPU by default, and no extensions in the old shell.\n\n## Install And Version Locking\n\n```bash\nnpx playwright install                      # all configured browsers\nnpx playwright install chromium --with-deps # one browser plus Linux system libraries (needs root)\nnpx playwright install-deps                 # libraries only\nnpx playwright --version                    # the number the CI image tag must match\n```\n\n- Browser builds are tied to the package version: upgrading `@playwright/test` without re-running `install` gives `Executable doesn't exist at ...`.\n- The Docker image tag must match exactly: `mcr.microsoft.com/playwright:v1.XX.Y-noble` with `@playwright/test@1.XX.Y`. A mismatched image either fails to find browsers or runs different builds than developers do.\n- Cache location: `~/.cache/ms-playwright` (L"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Automates, tests, and debugs browsers with Playwright: locators, auto-waiting, traces, CI runs, and MCP browser control. Use when a test is flaky, times out, or fails only in CI or headless; when a locator matches multiple elements or the wrong one (strict mode violation); when clicks need force, waits become sleeps, or networkidle never settles; for storageState and login setup, request mocking and HAR replay, uploads and downloads, iframes and shadow DOM, popups and dialogs, screenshot diffs that change per machine, trace and report artifacts, sharding a slow suite, device and permission emulation, accessibility checks, driving a real browser through Playwright MCP, extracting data from JS-rendered pages, or porting a Cypress, Puppeteer, or Selenium suite to Playwright. Not for maintaining an existing Cypress or Puppeteer suite (cypress, puppeteer) or for work a plain HTTP request answers (http). Skill: Playwright (Automation + MCP + Scraper) Owner: ivangdavila Summary: Automates, tests, and debugs browsers with Playwright: locators, auto-waiting, traces, CI runs, and MCP browser control. Use when a test is flaky, times out, or fails only in CI or headless; when a locator matches multiple elements or the wrong one (strict mode violation); when clicks need force, waits become sleeps, or networkidle never settl","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1629,"uniquenessScore":47,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-11-11T17:16:35.852Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-11-11T17:16:35.852Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-11-14T06:52:51.581Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}