{"id":"2d53eb88-00e1-4c91-be28-4619695b5c30","entityType":"agent","slug":"clawhub-ivangdavila-playwright-2","name":"Playwright (Automation + MCP + Scraper)","canonicalUrl":"https://www.xpersona.co/agent/clawhub-ivangdavila-playwright-2","canonicalPath":"/agent/clawhub-ivangdavila-playwright-2","generatedAt":"2026-10-09T13:27:33.285Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-05-14T06:29:47.299Z","emptyReason":null},"description":"Browser automation via Playwright MCP. Navigate websites, click elements, fill forms, take screenshots, extract data, and debug real browser workflows. Use w... Skill: Playwright (Automation + MCP + Scraper) Owner: ivangdavila Summary: Browser automation via Playwright MCP. Navigate websites, click elements, fill forms, take screenshots, extract data, and debug real browser workflows. Use w... Tags: latest:1.0.3 Version history: v1.0.3 | 2026-03-11T17:44:52.810Z | user Clarified the MCP-first browser automation flow and improved quick-start guidance for forms, screenshots, a","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 32.2K downloads reported by the source. Last updated 5/14/2026.","installCommand":"clawhub skill install s178jdk12x4qj3gs2se3etxf3h83h7ft:playwright","sourceUrl":"https://clawhub.ai/ivangdavila/playwright","homepage":"https://clawhub.ai/ivangdavila/playwright","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/ivangdavila/playwright","kind":"source"}],"safetyScore":84,"overallRank":62,"popularityScore":90,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Browser automation via Playwright MCP. Navigate websites, click elements, fill forms, take screenshots, extract data, and debug real browser workflows. Use w..."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-05-14T06:29:47.299Z","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-05-14T06:29:47.299Z","emptyReason":null},"stars":null,"forks":null,"downloads":32223,"packageName":null,"latestVersion":"1.0.3","tractionLabel":"32.2K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-05-14T06:29:47.228Z","emptyReason":null},"lastUpdatedAt":"2026-05-14T06:29:47.299Z","lastCrawledAt":"2026-05-14T06:29:47.228Z","lastIndexedAt":null,"nextCrawlAt":"2026-05-15T06:29:47.228Z","lastVerifiedAt":null,"highlights":[{"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":7,"zipByteSize":13268},{"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":10052},{"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":10050},{"version":"1.0.0","createdAt":"2026-02-13T11:29:44.555Z","changelog":"Initial release","fileCount":7,"zipByteSize":8752}]},"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-2/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-2/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-2/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-2/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-2/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-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-10-09T13:27:33.284Z"}},"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-2/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-2/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-2/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-ivangdavila-playwright-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-05-14T06:29:47.299Z","emptyReason":null},"readme":"Skill: Playwright (Automation + MCP + Scraper)\n\nOwner: ivangdavila\n\nSummary: Browser automation via Playwright MCP. Navigate websites, click elements, fill forms, take screenshots, extract data, and debug real browser workflows. Use w...\n\nTags: latest:1.0.3\n\nVersion history:\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.3: 7 files, 13268 bytes\n\nFiles: ci-cd.md (3501b), debugging.md (3624b), scraping.md (3968b), selectors.md (2384b), 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: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, 10052 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, 10050 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### 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.1: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.1: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.1: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.0: 7 files, 8752 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)\n\nFile v1.0.0:SKILL.md\n\n---\nname: Playwright\ndescription: Write, debug, and maintain Playwright tests and scrapers with resilient selectors, flaky test fixes, and CI/CD integration.\n---\n\n## Trigger\n\nUse when writing Playwright tests, debugging failures, scraping with Playwright, or setting up CI/CD pipelines.\n\n## Selector Priority (Always)\n\n1. `getByRole()` — accessible, resilient\n2. `getByTestId()` — explicit, stable\n3. `getByLabel()`, `getByPlaceholder()` — form elements\n4. `getByText()` — visible content (exact match preferred)\n5. CSS/XPath — last resort, avoid nth-child and generated classes\n\n## Core Capabilities\n\n| Task | Reference |\n|------|-----------|\n| Test scaffolding & POMs | `testing.md` |\n| Selector strategies | `selectors.md` |\n| Scraping patterns | `scraping.md` |\n| CI/CD integration | `ci-cd.md` |\n| Debugging failures | `debugging.md` |\n\n## Critical Rules\n\n- **Never use `page.waitForTimeout()`** — use `waitFor*` methods or `expect` with polling\n- **Always close contexts** — `browser.close()` or `context.close()` to prevent memory leaks\n- **Prefer `networkidle` with caution** — SPAs may never reach idle; use DOM-based waits instead\n- **Use `test.describe.configure({ mode: 'parallel' })`** — for independent tests\n- **Trace on failure only** — `trace: 'on-first-retry'` in config, not always-on\n\n## Quick Fixes\n\n| Symptom | Fix |\n|---------|-----|\n| Element not found | Use `waitFor()` before interaction, check frame context |\n| Flaky clicks | Use `click({ force: true })` or `waitFor({ state: 'visible' })` first |\n| Timeout in CI | Increase timeout, add `expect.poll()`, check viewport size |\n| Stale element | Re-query the locator, avoid storing element references |\n| Auth lost between tests | Use `storageState` to persist cookies/localStorage |\n\nFile v1.0.0:_meta.json\n\n{\n  \"ownerId\": \"kn73vp5rarc3b14rc7wjcw8f8580t5d1\",\n  \"slug\": \"playwright\",\n  \"version\": \"1.0.0\",\n  \"publishedAt\": 1770982184555\n}\n\nFile v1.0.0: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.0: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.0: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.waitForTimeout(1000);  // Allow content to load\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## Anti-Bot Evasion\n\n```typescript\nconst browser = await chromium.launch({\n  headless: false,  // Some sites detect headless\n});\n\nconst context = await browser.newContext({\n  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',\n  viewport: { width: 1920, height: 1080 },\n  locale: 'en-US',\n  timezoneId: 'America/New_York',\n});\n\n// Add realistic behavior\nawait page.mouse.move(100, 100);\nawait page.waitForTimeout(Math.random() * 2000 + 1000);\n```\n\n## Session Management\n\n```typescript\n// Save cookies\nawait context.storageState({ path: 'session.json' });\n\n// Restore session\nconst context = await browser.newContext({\n  storageState: 'session.json',\n});\n```\n\n## Error Handling\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\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## Proxy Rotation\n\n```typescript\nconst proxies = ['proxy1:8080', 'proxy2:8080', 'proxy3:8080'];\nlet proxyIndex = 0;\n\nasync function getNextProxy() {\n  const proxy = proxies[proxyIndex];\n  proxyIndex = (proxyIndex + 1) % proxies.length;\n  return proxy;\n}\n\nconst browser = await chromium.launch({\n  proxy: { server: await getNextProxy() },\n});\n```\n\nFile v1.0.0: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.0: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```","readmeExcerpt":"Skill: Playwright (Automation + MCP + Scraper) Owner: ivangdavila Summary: Browser automation via Playwright MCP. Navigate websites, click elements, fill forms, take screenshots, extract data, and debug real browser workflows. Use w... Tags: latest:1.0.3 Version history: v1.0.3 | 2026-03-11T17:44:52.810Z | user Clarified the MCP-first browser automation flow and improved quick-start guidance for forms, screenshots, a","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"npx @playwright/mcp --headless"},{"language":"bash","snippet":"npx playwright test\nnpx playwright test --headed\nnpx playwright test --trace on"},{"language":"bash","snippet":"npx playwright codegen https://example.com"},{"language":"javascript","snippet":"const { 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})();"},{"language":"yaml","snippet":"name: 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"},{"language":"yaml","snippet":"playwright:\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"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\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"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn73vp5rarc3b14rc7wjcw8f8580t5d1\",\n  \"slug\": \"playwright\",\n  \"version\": \"1.0.3\",\n  \"publishedAt\": 1773251092810\n}"},{"path":"ci-cd.md","content":"# 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\nexpor"},{"path":"debugging.md","content":"# 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(() "},{"path":"scraping.md","content":"# 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.lastReque"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Browser automation via Playwright MCP. Navigate websites, click elements, fill forms, take screenshots, extract data, and debug real browser workflows. Use w... Skill: Playwright (Automation + MCP + Scraper) Owner: ivangdavila Summary: Browser automation via Playwright MCP. Navigate websites, click elements, fill forms, take screenshots, extract data, and debug real browser workflows. Use w... Tags: latest:1.0.3 Version history: v1.0.3 | 2026-03-11T17:44:52.810Z | user Clarified the MCP-first browser automation flow and improved quick-start guidance for forms, screenshots, a","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1356,"uniquenessScore":45,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-05-14T06:29:47.299Z","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-05-14T06:29:47.299Z","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-10-09T13:27:33.285Z","emptyReason":null},"items":[{"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":"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-04-10T18:48:31.762Z","createdAt":"2026-02-25T03:38:16.584Z","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"}]}}}